From d3b62da2e83c69b0866af5bb2e29ac78dc8014cf Mon Sep 17 00:00:00 2001 From: falkTX Date: Mon, 15 Sep 2025 00:38:27 +0200 Subject: [PATCH] Add syndicate plugin, enable juce7 vst3 hosting Signed-off-by: falkTX --- libs/juce7/AppConfig.h | 13 +- libs/juce7/meson.build | 11 + meson.build | 4 + meson_options.txt | 1 + ports-juce7/changes.patch | 185 ++ ports-juce7/meson.build | 1 + ports-juce7/syndicate/AllCommon/AllUtils.h | 85 + .../syndicate/AllCommon/MainLogger.cpp | 62 + ports-juce7/syndicate/AllCommon/MainLogger.h | 16 + .../syndicate/AllCommon/NullLogger.hpp | 12 + ports-juce7/syndicate/AllCommon/README.md | 1 + ports-juce7/syndicate/JuceHeader.h | 21 + .../syndicate/JucePluginCharacteristics.h | 28 + ports-juce7/syndicate/LICENSE | 674 +++++++ .../PluginHosting/GuestPluginWindow.cpp | 77 + .../PluginHosting/GuestPluginWindow.h | 21 + .../PluginScanning/ConfigurePopover.cpp | 246 +++ .../PluginScanning/ConfigurePopover.hpp | 70 + .../PluginScanning/CustomScanner.hpp | 140 ++ .../PluginScanning/PluginScanClient.cpp | 303 +++ .../PluginScanning/PluginScanClient.h | 96 + .../PluginScanning/PluginScanStatusBar.cpp | 210 ++ .../PluginScanning/PluginScanStatusBar.h | 55 + .../PluginScanning/PluginScanStatusMessage.h | 27 + .../PluginSelectorComponent.cpp | 194 ++ .../PluginScanning/PluginSelectorComponent.h | 45 + .../PluginScanning/PluginSelectorList.cpp | 210 ++ .../PluginScanning/PluginSelectorList.h | 87 + .../PluginSelectorListParameters.h | 26 + .../PluginScanning/PluginSelectorState.cpp | 78 + .../PluginScanning/PluginSelectorState.h | 35 + .../PluginScanning/PluginSelectorWindow.cpp | 59 + .../PluginScanning/PluginSelectorWindow.h | 26 + .../PluginScanning/ScanConfiguration.cpp | 107 + .../PluginScanning/ScanConfiguration.hpp | 29 + .../PluginScanning/SelectorComponentStyle.h | 34 + .../syndicate/PluginCommon/PluginUtils.h | 59 + .../Processor/DataModel/ChainSlots.hpp | 186 ++ .../Processor/DataModel/ChainSlots_test.cpp | 104 + .../DataModel/CloneableDelayLine.cpp | 132 ++ .../DataModel/CloneableDelayLine.hpp | 257 +++ .../DataModel/CloneableDelayLine_test.cpp | 47 + .../Processor/DataModel/CloneableLRFilter.cpp | 118 ++ .../Processor/DataModel/CloneableLRFilter.hpp | 125 ++ .../DataModel/CloneableLRFilter_test.cpp | 48 + .../Processor/DataModel/CloneableSources.hpp | 83 + .../DataModel/CloneableSources_test.cpp | 107 + .../Processor/DataModel/CrossoverState.hpp | 93 + .../DataModel/CrossoverState_test.cpp | 108 + .../DataModel/DataModelInterface.hpp | 144 ++ .../Processor/DataModel/FFTProvider.cpp | 83 + .../Processor/DataModel/FFTProvider.hpp | 42 + .../Processor/DataModel/LatencyListener.cpp | 56 + .../Processor/DataModel/LatencyListener.hpp | 47 + .../DataModel/ModulationSourceDefinition.hpp | 128 ++ .../Processor/DataModel/PluginChain.hpp | 81 + .../Processor/DataModel/PluginChain_test.cpp | 86 + .../DataModel/PluginConfigurator.cpp | 66 + .../DataModel/PluginConfigurator.hpp | 56 + .../DataModel/PluginConfigurator_test.cpp | 153 ++ .../Processor/DataModel/PluginSplitter.hpp | 379 ++++ .../DataModel/PluginSplitter_test.cpp | 417 ++++ .../Processor/DataModel/SplitTypes.hpp | 9 + .../PluginCommon/Processor/ModelInterface.hpp | 5 + .../Processor/Mutators/ChainMutators.cpp | 216 ++ .../Processor/Mutators/ChainMutators.hpp | 113 ++ .../Processor/Mutators/ChainMutators_test.cpp | 529 +++++ .../Processor/Mutators/CrossoverMutators.cpp | 180 ++ .../Processor/Mutators/CrossoverMutators.hpp | 16 + .../Processor/Mutators/ModulationMutators.cpp | 733 +++++++ .../Processor/Mutators/ModulationMutators.hpp | 83 + .../Processor/Mutators/MutatorsInterface.cpp | 1767 +++++++++++++++++ .../Processor/Mutators/MutatorsInterface.hpp | 181 ++ .../Processor/Mutators/SplitterMutators.cpp | 474 +++++ .../Processor/Mutators/SplitterMutators.hpp | 64 + .../Mutators/SplitterMutators_test.cpp | 1300 ++++++++++++ .../Processor/Mutators/XmlConsts.hpp | 159 ++ .../Processor/Mutators/XmlReader.cpp | 752 +++++++ .../Processor/Mutators/XmlReader.hpp | 54 + .../Processor/Mutators/XmlReader_test.cpp | 926 +++++++++ .../Processor/Mutators/XmlWriter.cpp | 245 +++ .../Processor/Mutators/XmlWriter.hpp | 24 + .../Processor/Mutators/XmlWriter_test.cpp | 643 ++++++ .../Processor/Processing/ChainProcessors.cpp | 76 + .../Processor/Processing/ChainProcessors.hpp | 14 + .../Processing/ChainProcessors_test.cpp | 376 ++++ .../Processing/ChainSlotProcessors.cpp | 134 ++ .../Processing/ChainSlotProcessors.hpp | 19 + .../Processing/ChainSlotProcessors_test.cpp | 437 ++++ .../Processing/CrossoverProcessors.cpp | 106 + .../Processing/CrossoverProcessors.hpp | 9 + .../Processing/ModulationProcessors.cpp | 113 ++ .../Processing/ModulationProcessors.hpp | 15 + .../Processing/ProcessingInterface.cpp | 79 + .../Processing/ProcessingInterface.hpp | 15 + .../Processing/SplitterProcessors.cpp | 235 +++ .../Processing/SplitterProcessors.hpp | 14 + .../Processing/SplitterProcessors_test.cpp | 424 ++++ ports-juce7/syndicate/PluginCommon/README.md | 1 + .../syndicate/PluginScanServer/Main.cpp | 12 + .../PluginScanServer/ServerApplication.h | 76 + .../PluginScanServer/ServerProcess.h | 106 + ports-juce7/syndicate/README.md | 7 + .../syndicate/Syndicate/ParameterData.h | 19 + .../syndicate/Syndicate/PluginProcessor.cpp | 1171 +++++++++++ .../syndicate/Syndicate/PluginProcessor.h | 264 +++ .../syndicate/Syndicate/PresetMetadata.hpp | 10 + .../UI/HeaderComponents/ChainButton.cpp | 57 + .../UI/HeaderComponents/ChainButton.h | 42 + .../ChainButtonsComponent.cpp | 157 ++ .../HeaderComponents/ChainButtonsComponent.h | 35 + .../Crossover/CrossoverImagerComponent.cpp | 49 + .../Crossover/CrossoverImagerComponent.h | 19 + .../Crossover/CrossoverMouseListener.cpp | 84 + .../Crossover/CrossoverMouseListener.h | 33 + .../Crossover/CrossoverParameterComponent.cpp | 152 ++ .../Crossover/CrossoverParameterComponent.h | 42 + .../Crossover/CrossoverWrapperComponent.cpp | 28 + .../Crossover/CrossoverWrapperComponent.h | 27 + .../Crossover/UIUtilsCrossover.h | 52 + .../LeftrightSplitterSubComponent.cpp | 15 + .../LeftrightSplitterSubComponent.h | 15 + .../MidsideSplitterSubComponent.cpp | 15 + .../MidsideSplitterSubComponent.h | 15 + .../MultibandSplitterSubComponent.cpp | 90 + .../MultibandSplitterSubComponent.h | 36 + .../ParallelSplitterSubComponent.cpp | 80 + .../ParallelSplitterSubComponent.h | 27 + .../SeriesSplitterSubComponent.cpp | 11 + .../SeriesSplitterSubComponent.h | 15 + .../SplitterHeaderComponent.cpp | 150 ++ .../SplitterHeaderComponent.h | 38 + .../Syndicate/UI/ImportExportComponent.cpp | 158 ++ .../Syndicate/UI/ImportExportComponent.h | 41 + .../syndicate/Syndicate/UI/MacroComponent.cpp | 106 + .../syndicate/Syndicate/UI/MacroComponent.h | 40 + .../Syndicate/UI/MacrosComponent.cpp | 49 + .../syndicate/Syndicate/UI/MacrosComponent.h | 22 + .../Syndicate/UI/MetadataEditComponent.cpp | 129 ++ .../Syndicate/UI/MetadataEditComponent.hpp | 34 + .../UI/ModulationBar/ModulatableParameter.cpp | 100 + .../UI/ModulationBar/ModulatableParameter.hpp | 37 + .../UI/ModulationBar/ModulationBar.cpp | 415 ++++ .../UI/ModulationBar/ModulationBar.h | 63 + .../ModulationBar/ModulationBarEnvelope.cpp | 285 +++ .../UI/ModulationBar/ModulationBarEnvelope.h | 61 + .../UI/ModulationBar/ModulationBarLfo.cpp | 396 ++++ .../UI/ModulationBar/ModulationBarLfo.h | 65 + .../UI/ModulationBar/ModulationBarRandom.cpp | 123 ++ .../UI/ModulationBar/ModulationBarRandom.hpp | 27 + .../UI/ModulationBar/ModulationButton.cpp | 171 ++ .../UI/ModulationBar/ModulationButton.h | 57 + .../Syndicate/UI/ModulationTargetSlider.cpp | 50 + .../Syndicate/UI/ModulationTargetSlider.hpp | 20 + .../UI/ModulationTargetSourceSlider.cpp | 168 ++ .../UI/ModulationTargetSourceSlider.hpp | 55 + .../Syndicate/UI/OutputComponent.cpp | 156 ++ .../syndicate/Syndicate/UI/OutputComponent.h | 51 + .../syndicate/Syndicate/UI/PluginEditor.cpp | 350 ++++ .../syndicate/Syndicate/UI/PluginEditor.h | 110 + .../UI/PluginGraph/BaseSlotComponent.cpp | 51 + .../UI/PluginGraph/BaseSlotComponent.h | 41 + .../UI/PluginGraph/ChainViewComponent.cpp | 197 ++ .../UI/PluginGraph/ChainViewComponent.h | 44 + .../PluginGraph/EmptyPluginSlotComponent.cpp | 49 + .../UI/PluginGraph/EmptyPluginSlotComponent.h | 27 + .../UI/PluginGraph/GainStageSlotComponent.cpp | 180 ++ .../UI/PluginGraph/GainStageSlotComponent.h | 54 + .../UI/PluginGraph/GraphViewComponent.cpp | 120 ++ .../UI/PluginGraph/GraphViewComponent.h | 30 + .../PluginGraph/PluginModulationInterface.cpp | 129 ++ .../PluginGraph/PluginModulationInterface.h | 32 + .../UI/PluginGraph/PluginModulationTarget.cpp | 201 ++ .../UI/PluginGraph/PluginModulationTarget.h | 62 + .../PluginParameterSelectorComponent.cpp | 105 + .../PluginParameterSelectorComponent.h | 36 + .../PluginParameterSelectorList.cpp | 121 ++ .../PluginGraph/PluginParameterSelectorList.h | 61 + .../PluginParameterSelectorListParameters.h | 12 + .../PluginParameterSelectorState.cpp | 38 + .../PluginParameterSelectorState.h | 20 + .../PluginParameterSelectorWindow.cpp | 58 + .../PluginParameterSelectorWindow.h | 24 + .../PluginGraph/PluginSelectionInterface.cpp | 236 +++ .../UI/PluginGraph/PluginSelectionInterface.h | 50 + .../UI/PluginGraph/PluginSlotComponent.cpp | 174 ++ .../UI/PluginGraph/PluginSlotComponent.h | 35 + .../PluginGraph/PluginSlotModulationTray.cpp | 72 + .../UI/PluginGraph/PluginSlotModulationTray.h | 21 + .../Syndicate/UI/SplitterButtonsComponent.cpp | 144 ++ .../Syndicate/UI/SplitterButtonsComponent.h | 28 + .../syndicate/Syndicate/UI/UIUtils.cpp | 761 +++++++ ports-juce7/syndicate/Syndicate/UI/UIUtils.h | 360 ++++ .../Syndicate/UI/UndoRedoComponent.cpp | 72 + .../Syndicate/UI/UndoRedoComponent.h | 28 + ports-juce7/syndicate/Tests/TestUtils.hpp | 47 + ports-juce7/syndicate/Tests/catchMain.cpp | 2 + .../syndicate/WECore/CarveDSP/CarveDSPUnit.h | 262 +++ .../WECore/CarveDSP/CarveNoiseFilter.h | 169 ++ .../WECore/CarveDSP/CarveParameters.h | 57 + .../CarveDSP/Tests/CarveNoiseFilterTests.cpp | 44 + .../CarveDSP/Tests/DSPUnitParameterTests.cpp | 154 ++ .../CarveDSP/Tests/DSPUnitProcessingTests.cpp | 266 +++ .../WECore/CarveDSP/Tests/TestData.h | 55 + .../CoreJUCEPlugin/CoreAudioProcessor.h | 484 +++++ .../WECore/CoreJUCEPlugin/CoreLookAndFeel.h | 339 ++++ .../CoreJUCEPlugin/CoreProcessorEditor.h | 81 + .../WECore/CoreJUCEPlugin/CustomParameter.h | 78 + .../CoreJUCEPlugin/LabelReadoutSlider.h | 171 ++ .../LookAndFeelMixins/ButtonV2.h | 128 ++ .../LookAndFeelMixins/ComboBoxV2.h | 93 + .../LookAndFeelMixins/GroupComponentV2.h | 85 + .../LookAndFeelMixins/LinearSliderV2.h | 168 ++ .../LookAndFeelMixins/LookAndFeelMixins.h | 107 + .../MidAnchoredRotarySlider.h | 126 ++ .../LookAndFeelMixins/PopupMenuV2.h | 117 ++ .../LookAndFeelMixins/RotarySliderV2.h | 114 ++ .../CoreJUCEPlugin/ParameterUpdateHandler.h | 61 + .../CoreJUCEPlugin/TooltipLabelUpdater.h | 147 ++ .../syndicate/WECore/General/AudioSpinMutex.h | 165 ++ .../syndicate/WECore/General/CoreMath.h | 54 + .../WECore/General/ParameterDefinition.h | 131 ++ .../syndicate/WECore/General/UpdateChecker.h | 69 + .../WECore/MONSTRFilters/MONSTRParameters.h | 54 + .../syndicate/WECore/RichterLFO/RichterLFO.h | 417 ++++ .../WECore/RichterLFO/RichterLFOPair.h | 145 ++ .../WECore/RichterLFO/RichterParameters.h | 76 + .../WECore/RichterLFO/RichterWavetables.h | 129 ++ .../RichterLFO/Tests/RichterLFOPairTests.cpp | 187 ++ .../WECore/RichterLFO/UI/RichterWaveViewer.h | 99 + .../WECore/SongbirdFilters/Formant.h | 39 + .../SongbirdFilters/SongbirdFilterModule.h | 504 +++++ .../SongbirdFiltersParameters.h | 60 + .../SongbirdFilters/SongbirdFormantFilter.h | 149 ++ .../Tests/SongbirdFilterModuleTests.cpp | 208 ++ .../WECore/SongbirdFilters/Tests/TestData.h | 49 + .../WECore/Tests/PerformanceTests.cpp | 250 +++ .../syndicate/WECore/Tests/catchMain.cpp | 23 + .../WECore/WEFilters/AREnvelopeFollowerBase.h | 199 ++ .../WEFilters/AREnvelopeFollowerFullWave.h | 61 + .../WEFilters/AREnvelopeFollowerParameters.h | 43 + .../WEFilters/AREnvelopeFollowerSquareLaw.h | 61 + .../WECore/WEFilters/EffectsProcessor.h | 81 + .../WECore/WEFilters/ModulationSource.h | 103 + .../WECore/WEFilters/PerlinSource.hpp | 275 +++ .../WECore/WEFilters/SimpleCompressor.h | 306 +++ .../WEFilters/SimpleCompressorParameters.h | 42 + .../WECore/WEFilters/StereoWidthProcessor.h | 80 + .../StereoWidthProcessorParameters.h | 37 + .../syndicate/WECore/WEFilters/TPTSVFilter.h | 187 ++ .../WECore/WEFilters/TPTSVFilterParameters.h | 53 + .../Tests/AREnvelopeFollowerTests.cpp | 89 + .../WEFilters/Tests/SimpleCompressorTests.cpp | 122 ++ .../Tests/StereoWidthProcessorTests.cpp | 153 ++ .../WEFilters/Tests/TPTSVFilterTests.cpp | 155 ++ .../WECore/WEFilters/Tests/TestUtils.h | 35 + ports-juce7/syndicate/meson.build | 124 ++ scripts/install-syndicate-scanner.sh | 24 + 258 files changed, 35383 insertions(+), 5 deletions(-) create mode 100644 ports-juce7/changes.patch create mode 100644 ports-juce7/syndicate/AllCommon/AllUtils.h create mode 100644 ports-juce7/syndicate/AllCommon/MainLogger.cpp create mode 100644 ports-juce7/syndicate/AllCommon/MainLogger.h create mode 100644 ports-juce7/syndicate/AllCommon/NullLogger.hpp create mode 100644 ports-juce7/syndicate/AllCommon/README.md create mode 100644 ports-juce7/syndicate/JuceHeader.h create mode 100644 ports-juce7/syndicate/JucePluginCharacteristics.h create mode 100644 ports-juce7/syndicate/LICENSE create mode 100644 ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/CustomScanner.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusMessage.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorListParameters.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/PluginScanning/SelectorComponentStyle.h create mode 100644 ports-juce7/syndicate/PluginCommon/PluginUtils.h create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/DataModelInterface.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/ModulationSourceDefinition.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/DataModel/SplitTypes.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/ModelInterface.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlConsts.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.hpp create mode 100644 ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors_test.cpp create mode 100644 ports-juce7/syndicate/PluginCommon/README.md create mode 100644 ports-juce7/syndicate/PluginScanServer/Main.cpp create mode 100644 ports-juce7/syndicate/PluginScanServer/ServerApplication.h create mode 100644 ports-juce7/syndicate/PluginScanServer/ServerProcess.h create mode 100644 ports-juce7/syndicate/README.md create mode 100644 ports-juce7/syndicate/Syndicate/ParameterData.h create mode 100644 ports-juce7/syndicate/Syndicate/PluginProcessor.cpp create mode 100644 ports-juce7/syndicate/Syndicate/PluginProcessor.h create mode 100644 ports-juce7/syndicate/Syndicate/PresetMetadata.hpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButton.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButton.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButtonsComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButtonsComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/UIUtilsCrossover.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SplitterHeaderComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SplitterHeaderComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/ImportExportComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ImportExportComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/MacroComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/MacroComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/MacrosComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/MacrosComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/MetadataEditComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/MetadataEditComponent.hpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulatableParameter.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulatableParameter.hpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBar.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBar.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarEnvelope.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarEnvelope.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarLfo.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarLfo.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarRandom.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarRandom.hpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationButton.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationButton.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationTargetSlider.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationTargetSlider.hpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationTargetSourceSlider.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/ModulationTargetSourceSlider.hpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/OutputComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/OutputComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginEditor.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginEditor.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/BaseSlotComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/BaseSlotComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/ChainViewComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/ChainViewComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/GainStageSlotComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/GainStageSlotComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/GraphViewComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/GraphViewComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationInterface.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationInterface.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationTarget.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationTarget.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorList.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorList.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorListParameters.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorState.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorState.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSelectionInterface.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSelectionInterface.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotModulationTray.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotModulationTray.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/SplitterButtonsComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/SplitterButtonsComponent.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/UIUtils.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/UIUtils.h create mode 100644 ports-juce7/syndicate/Syndicate/UI/UndoRedoComponent.cpp create mode 100644 ports-juce7/syndicate/Syndicate/UI/UndoRedoComponent.h create mode 100644 ports-juce7/syndicate/Tests/TestUtils.hpp create mode 100644 ports-juce7/syndicate/Tests/catchMain.cpp create mode 100644 ports-juce7/syndicate/WECore/CarveDSP/CarveDSPUnit.h create mode 100644 ports-juce7/syndicate/WECore/CarveDSP/CarveNoiseFilter.h create mode 100644 ports-juce7/syndicate/WECore/CarveDSP/CarveParameters.h create mode 100644 ports-juce7/syndicate/WECore/CarveDSP/Tests/CarveNoiseFilterTests.cpp create mode 100644 ports-juce7/syndicate/WECore/CarveDSP/Tests/DSPUnitParameterTests.cpp create mode 100644 ports-juce7/syndicate/WECore/CarveDSP/Tests/DSPUnitProcessingTests.cpp create mode 100644 ports-juce7/syndicate/WECore/CarveDSP/Tests/TestData.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreAudioProcessor.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreLookAndFeel.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreProcessorEditor.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/CustomParameter.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LabelReadoutSlider.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/ButtonV2.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/ComboBoxV2.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/GroupComponentV2.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/LinearSliderV2.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/MidAnchoredRotarySlider.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/PopupMenuV2.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/RotarySliderV2.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/ParameterUpdateHandler.h create mode 100644 ports-juce7/syndicate/WECore/CoreJUCEPlugin/TooltipLabelUpdater.h create mode 100644 ports-juce7/syndicate/WECore/General/AudioSpinMutex.h create mode 100644 ports-juce7/syndicate/WECore/General/CoreMath.h create mode 100644 ports-juce7/syndicate/WECore/General/ParameterDefinition.h create mode 100644 ports-juce7/syndicate/WECore/General/UpdateChecker.h create mode 100644 ports-juce7/syndicate/WECore/MONSTRFilters/MONSTRParameters.h create mode 100644 ports-juce7/syndicate/WECore/RichterLFO/RichterLFO.h create mode 100644 ports-juce7/syndicate/WECore/RichterLFO/RichterLFOPair.h create mode 100644 ports-juce7/syndicate/WECore/RichterLFO/RichterParameters.h create mode 100644 ports-juce7/syndicate/WECore/RichterLFO/RichterWavetables.h create mode 100644 ports-juce7/syndicate/WECore/RichterLFO/Tests/RichterLFOPairTests.cpp create mode 100644 ports-juce7/syndicate/WECore/RichterLFO/UI/RichterWaveViewer.h create mode 100644 ports-juce7/syndicate/WECore/SongbirdFilters/Formant.h create mode 100644 ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFilterModule.h create mode 100644 ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFiltersParameters.h create mode 100644 ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFormantFilter.h create mode 100644 ports-juce7/syndicate/WECore/SongbirdFilters/Tests/SongbirdFilterModuleTests.cpp create mode 100644 ports-juce7/syndicate/WECore/SongbirdFilters/Tests/TestData.h create mode 100644 ports-juce7/syndicate/WECore/Tests/PerformanceTests.cpp create mode 100644 ports-juce7/syndicate/WECore/Tests/catchMain.cpp create mode 100644 ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerBase.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerFullWave.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerParameters.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerSquareLaw.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/EffectsProcessor.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/ModulationSource.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/PerlinSource.hpp create mode 100644 ports-juce7/syndicate/WECore/WEFilters/SimpleCompressor.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/SimpleCompressorParameters.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/StereoWidthProcessor.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/StereoWidthProcessorParameters.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/TPTSVFilter.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/TPTSVFilterParameters.h create mode 100644 ports-juce7/syndicate/WECore/WEFilters/Tests/AREnvelopeFollowerTests.cpp create mode 100644 ports-juce7/syndicate/WECore/WEFilters/Tests/SimpleCompressorTests.cpp create mode 100644 ports-juce7/syndicate/WECore/WEFilters/Tests/StereoWidthProcessorTests.cpp create mode 100644 ports-juce7/syndicate/WECore/WEFilters/Tests/TPTSVFilterTests.cpp create mode 100644 ports-juce7/syndicate/WECore/WEFilters/Tests/TestUtils.h create mode 100644 ports-juce7/syndicate/meson.build create mode 100644 scripts/install-syndicate-scanner.sh diff --git a/libs/juce7/AppConfig.h b/libs/juce7/AppConfig.h index d7fd269c..cc0d4e90 100755 --- a/libs/juce7/AppConfig.h +++ b/libs/juce7/AppConfig.h @@ -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 diff --git a/libs/juce7/meson.build b/libs/juce7/meson.build index 7154a88a..b790317c 100644 --- a/libs/juce7/meson.build +++ b/libs/juce7/meson.build @@ -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 ], diff --git a/meson.build b/meson.build index 245bd60a..feb0c7e7 100644 --- a/meson.build +++ b/meson.build @@ -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', diff --git a/meson_options.txt b/meson_options.txt index bcd6f9e8..75ee5d6f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -119,5 +119,6 @@ option('plugins', 'roth-air', 'swankyamp', # juce7 + 'syndicate', ], ) diff --git a/ports-juce7/changes.patch b/ports-juce7/changes.patch new file mode 100644 index 00000000..52b880f1 --- /dev/null +++ b/ports-juce7/changes.patch @@ -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& result) { + if (superprocess == nullptr) { +- superprocess = std::make_unique(); ++ superprocess = std::make_unique(_wrapperType); + } + + juce::MemoryBlock block; +@@ -132,6 +133,7 @@ + } + } + ++ const juce::AudioProcessor::WrapperType _wrapperType; + std::unique_ptr 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(); +- _pluginList->setCustomScanner(std::make_unique()); ++ _pluginList->setCustomScanner(std::make_unique(_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 getPluginTypes() const; + +@@ -64,6 +64,7 @@ + void changeListenerCallback(juce::ChangeBroadcaster* changed) override; + + private: ++ const juce::AudioProcessor::WrapperType _wrapperType; + std::unique_ptr _pluginList; + juce::File _scannedPluginsFile; + std::vector _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; + } + } diff --git a/ports-juce7/meson.build b/ports-juce7/meson.build index a6e0469a..bd5b78c7 100644 --- a/ports-juce7/meson.build +++ b/ports-juce7/meson.build @@ -5,6 +5,7 @@ if linux_headless ] else plugins = [ + 'syndicate' ] endif diff --git a/ports-juce7/syndicate/AllCommon/AllUtils.h b/ports-juce7/syndicate/AllCommon/AllUtils.h new file mode 100644 index 00000000..8eadc594 --- /dev/null +++ b/ports-juce7/syndicate/AllCommon/AllUtils.h @@ -0,0 +1,85 @@ +#pragma once + +#include + +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; + } +} diff --git a/ports-juce7/syndicate/AllCommon/MainLogger.cpp b/ports-juce7/syndicate/AllCommon/MainLogger.cpp new file mode 100644 index 00000000..859b7f6b --- /dev/null +++ b/ports-juce7/syndicate/AllCommon/MainLogger.cpp @@ -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"); + } +} diff --git a/ports-juce7/syndicate/AllCommon/MainLogger.h b/ports-juce7/syndicate/AllCommon/MainLogger.h new file mode 100644 index 00000000..2c449e76 --- /dev/null +++ b/ports-juce7/syndicate/AllCommon/MainLogger.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +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); +}; diff --git a/ports-juce7/syndicate/AllCommon/NullLogger.hpp b/ports-juce7/syndicate/AllCommon/NullLogger.hpp new file mode 100644 index 00000000..05dc603e --- /dev/null +++ b/ports-juce7/syndicate/AllCommon/NullLogger.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +class NullLogger : public juce::Logger { +public: + NullLogger() = default; + virtual ~NullLogger() = default; + +private: + virtual void logMessage(const juce::String& message) override { } +}; diff --git a/ports-juce7/syndicate/AllCommon/README.md b/ports-juce7/syndicate/AllCommon/README.md new file mode 100644 index 00000000..727c42f3 --- /dev/null +++ b/ports-juce7/syndicate/AllCommon/README.md @@ -0,0 +1 @@ +Contains code common to both plugins and the plugin scanning server. \ No newline at end of file diff --git a/ports-juce7/syndicate/JuceHeader.h b/ports-juce7/syndicate/JuceHeader.h new file mode 100644 index 00000000..d3d7269f --- /dev/null +++ b/ports-juce7/syndicate/JuceHeader.h @@ -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 diff --git a/ports-juce7/syndicate/JucePluginCharacteristics.h b/ports-juce7/syndicate/JucePluginCharacteristics.h new file mode 100644 index 00000000..9775dacc --- /dev/null +++ b/ports-juce7/syndicate/JucePluginCharacteristics.h @@ -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" diff --git a/ports-juce7/syndicate/LICENSE b/ports-juce7/syndicate/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/ports-juce7/syndicate/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.cpp b/ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.cpp new file mode 100644 index 00000000..62b8fc8d --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.cpp @@ -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 onCloseCallback, + std::shared_ptr newPlugin, + std::shared_ptr 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 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(); +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.h b/ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.h new file mode 100644 index 00000000..3c19532b --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginHosting/GuestPluginWindow.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "ChainSlots.hpp" + +class GuestPluginWindow : public juce::DocumentWindow +{ +public: + const std::shared_ptr plugin; + + GuestPluginWindow(std::function onCloseCallback, + std::shared_ptr newPlugin, + std::shared_ptr editorBounds); + ~GuestPluginWindow(); + + void closeButtonPressed() override; + +private: + std::function _onCloseCallback; + std::shared_ptr _editorBounds; +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.cpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.cpp new file mode 100644 index 00000000..af9a580b --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.cpp @@ -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 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(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( + "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 availableArea = getLocalBounds(); + + juce::Rectangle 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 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 availableArea = getLocalBounds().reduced(WINDOW_PADDING); + + _tooltipLabel->setBounds(availableArea.removeFromBottom(16)); + + juce::Rectangle 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(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); + } +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.hpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.hpp new file mode 100644 index 00000000..4ae69db4 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/ConfigurePopover.hpp @@ -0,0 +1,70 @@ +#pragma once + +#include +#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> _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 _defaultPathsButton; + std::unique_ptr _defaultPathsListLabel; + std::unique_ptr _customPathsButton; + std::unique_ptr _customPathsListComponent; + + std::unique_ptr _fileChooser; +}; + +class ConfigurePopover : public juce::Component { +public: + ConfigurePopover(ScanConfiguration& config, std::function 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 _onCloseCallback; + + UIUtils::StaticButtonLookAndFeel _buttonLookAndFeel; + + std::unique_ptr _vstConfigure; + std::unique_ptr _vst3Configure; + + std::unique_ptr _okButton; + std::unique_ptr _tooltipLabel; +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/CustomScanner.hpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/CustomScanner.hpp new file mode 100644 index 00000000..9d5dd936 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/CustomScanner.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#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 xml; + }; + + Response getResponse() { + std::unique_lock 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 lock { mutex }; + pluginDescription = parseXML (mb.toString()); + gotResult = true; + condvar.notify_one(); + } + + void handleConnectionLost() override { + juce::Logger::writeToLog("Connection lost"); + const std::lock_guard lock { mutex }; + connectionLost = true; + condvar.notify_one(); + } + + std::mutex mutex; + std::condition_variable condvar; + + std::unique_ptr 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& 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& result) { + if (superprocess == nullptr) { + superprocess = std::make_unique(_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(); + + if (desc->loadFromXml (*item)) { + result.add (std::move (desc)); + } + } + } + + return (response.state == Superprocess::State::gotResult); + } + } + + const juce::AudioProcessor::WrapperType _wrapperType; + std::unique_ptr superprocess; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomPluginScanner) +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.cpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.cpp new file mode 100644 index 00000000..4e2d2827 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.cpp @@ -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 PluginScanClient::getPluginTypes() const { + return _pluginList->getTypes(); +} + +void PluginScanClient::restore() { + _hasAttemptedRestore = true; + + _pluginList = std::make_unique(); + _pluginList->setCustomScanner(std::make_unique(_wrapperType)); + + const juce::File scannedPluginsFile(Utils::DataDirectory.getChildFile(Utils::SCANNED_PLUGINS_FILE_NAME)); + + if (scannedPluginsFile.existsAsFile()) { + std::unique_ptr 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 pluginsXml = std::unique_ptr(_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)); +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.h new file mode 100644 index 00000000..1ca74adc --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanClient.h @@ -0,0 +1,96 @@ +#pragma once + +#include +#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 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 _pluginList; + juce::File _scannedPluginsFile; + std::vector _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 _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); +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.cpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.cpp new file mode 100644 index 00000000..0ff515c8 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.cpp @@ -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& 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 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(&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()); +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.h new file mode 100644 index 00000000..37c59f18 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusBar.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#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 statusLbl; + std::unique_ptr startScanBtn; + std::unique_ptr stopScanBtn; + std::unique_ptr rescanAllBtn; + std::unique_ptr rescanCrashedBtn; + std::unique_ptr viewCrashedBtn; + std::unique_ptr configureBtn; + std::unique_ptr configurePopover; + std::unique_ptr crashedPluginsPopover; + PluginScanClient& _pluginScanClient; + + void _updateButtonState(bool isScanRunning); + + void _createCrashedPluginsDialogue(); + void _createConfigureDialogue(); +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusMessage.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusMessage.h new file mode 100644 index 00000000..28c30726 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginScanStatusMessage.h @@ -0,0 +1,27 @@ +/* + ============================================================================== + + PluginScanStatusMessage.h + Created: 15 Mar 2021 11:19:44pm + Author: Jack Devlin + + ============================================================================== +*/ + +#pragma once + +#include + +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) {} +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.cpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.cpp new file mode 100644 index 00000000..713552a6 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.cpp @@ -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 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 availableArea = getLocalBounds().reduced(MARGIN_SIZE); + + // Header + juce::Rectangle 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); +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.h new file mode 100644 index 00000000..211528d6 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorComponent.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#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 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 _onCloseCallback; + const juce::Colour _backgroundColour; + + std::unique_ptr searchEdt; + std::unique_ptr vstBtn; + std::unique_ptr vst3Btn; + std::unique_ptr auBtn; + std::unique_ptr pluginTableListBox; + std::unique_ptr statusBar; + + void _setupHeaderRow(const SelectorComponentStyle& style); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginSelectorComponent) +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.cpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.cpp new file mode 100644 index 00000000..47d75f57 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.cpp @@ -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 pluginList) { + _fullPluginList = pluginList; +} + +juce::Array PluginListSorter::getFilteredPluginList() const { + juce::Array 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 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(&message) + }; + + if (statusMessage != nullptr) { + _pluginTableListBoxModel.onPluginScanUpdate(); + updateContent(); + juce::Logger::writeToLog("PluginSelectorTableListBox: received PluginScanStatusMessage"); + } +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.h new file mode 100644 index 00000000..931571df --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorList.h @@ -0,0 +1,87 @@ +/* + ============================================================================== + + PluginSelectorList.h + Created: 23 May 2021 12:39:35am + Author: Jack Devlin + + ============================================================================== +*/ + +#pragma once + +#include +#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 pluginList); + + juce::Array getFilteredPluginList() const; + + int compareElements(juce::PluginDescription first, juce::PluginDescription second) const; + +private: + juce::Array _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 _pluginList; + std::function, const juce::String&, bool)> _pluginCreationCallback; + std::function _getSampleRateCallback; + std::function _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; +}; + diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorListParameters.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorListParameters.h new file mode 100644 index 00000000..b3526209 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorListParameters.h @@ -0,0 +1,26 @@ +/* + ============================================================================== + + PluginSelectorListParameters.h + Created: 28 May 2021 11:53:23pm + Author: Jack Devlin + + ============================================================================== +*/ + +#pragma once + +#include + +#include "PluginScanClient.h" +#include "PluginSelectorState.h" + +struct PluginSelectorListParameters { + PluginScanClient& scanner; + PluginSelectorState& state; + juce::AudioPluginFormatManager& formatManager; + std::function, const juce::String&, bool)> pluginCreationCallback; + std::function getSampleRate; + std::function getBlockSize; + bool isReplacingPlugin; +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.cpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.cpp new file mode 100644 index 00000000..4d851370 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.cpp @@ -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::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); +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.h new file mode 100644 index 00000000..78d7ae7a --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorState.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +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> 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; +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.cpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.cpp new file mode 100644 index 00000000..e315ad18 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.cpp @@ -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 onCloseCallback, + PluginSelectorListParameters selectorListParameters, + std::unique_ptr 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(); + } +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.h new file mode 100644 index 00000000..5953d5cc --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/PluginSelectorWindow.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include "PluginSelectorComponent.h" +#include "PluginSelectorListParameters.h" + +class PluginSelectorWindow : public juce::DocumentWindow { +public: + PluginSelectorWindow(std::function onCloseCallback, + PluginSelectorListParameters selectorListParameters, + std::unique_ptr style, + juce::String title); + virtual ~PluginSelectorWindow(); + + virtual void closeButtonPressed() override; + + void takeFocus(); + +private: + std::function _onCloseCallback; + PluginSelectorComponent* _content; + std::unique_ptr _style; + + // We need to keep a reference to state to update the bounds on resize + PluginSelectorState& _state; +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.cpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.cpp new file mode 100644 index 00000000..11186773 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.cpp @@ -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 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("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); +} diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.hpp b/ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.hpp new file mode 100644 index 00000000..64aacb73 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/ScanConfiguration.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +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; +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginScanning/SelectorComponentStyle.h b/ports-juce7/syndicate/PluginCommon/PluginScanning/SelectorComponentStyle.h new file mode 100644 index 00000000..e7eecd23 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginScanning/SelectorComponentStyle.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +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 searchBarLookAndFeel; + const std::unique_ptr headerButtonLookAndFeel; + const std::unique_ptr scanButtonLookAndFeel; + const std::unique_ptr tableHeaderLookAndFeel; + + SelectorComponentStyle(juce::Colour newBackgroundColour, + juce::Colour newButtonBackgroundColour, + juce::Colour newNeutralColour, + juce::Colour newHighlightColour, + juce::Colour newDisabledColour, + std::unique_ptr newSearchBarLookAndFeel, + std::unique_ptr newHeaderButtonLookAndFeel, + std::unique_ptr newScanButtonLookAndFeel, + std::unique_ptr 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)) { } +}; diff --git a/ports-juce7/syndicate/PluginCommon/PluginUtils.h b/ports-juce7/syndicate/PluginCommon/PluginUtils.h new file mode 100644 index 00000000..f7b8f5f8 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/PluginUtils.h @@ -0,0 +1,59 @@ +/* + ============================================================================== + + Utils.h + Created: 16 Mar 2021 9:09:05pm + Author: Jack Devlin + + ============================================================================== +*/ + +#pragma once + +#include + +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& 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()); + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots.hpp new file mode 100644 index 00000000..d7bc241d --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots.hpp @@ -0,0 +1,186 @@ +#pragma once + +#include +#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 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> 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(source->definition, source->modulationAmount)); + } + + return newConfig; + } +}; + +struct PluginModulationConfig { + bool isActive; + std::vector> 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(parameterConfig->clone())); + } + + return newConfig; + } +}; + +struct PluginEditorBoundsContainer { + juce::Rectangle editorBounds; + juce::Rectangle displayArea; + + PluginEditorBoundsContainer( + juce::Rectangle newEditorBounds, + juce::Rectangle newDisplayAreaBounds) : editorBounds(newEditorBounds), + displayArea(newDisplayAreaBounds) { } +}; + +typedef std::optional PluginEditorBounds; + +/** + * Represents a plugin in a slot in a processing chain. + */ +struct ChainSlotPlugin : ChainSlotBase { + std::shared_ptr plugin; + std::shared_ptr modulationConfig; + std::function getModulationValueCallback; + std::shared_ptr 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> spareSCBuffer; + + ChainSlotPlugin(std::shared_ptr newPlugin, + bool newIsBypassed, + std::function newGetModulationValueCallback, + HostConfiguration config) + : ChainSlotBase(newIsBypassed), + plugin(newPlugin), + modulationConfig(std::make_shared()), + getModulationValueCallback(newGetModulationValueCallback), + editorBounds(new PluginEditorBounds()), + spareSCBuffer(new juce::AudioBuffer(config.layout.getMainInputChannels() * 2, config.blockSize)) {} + + ~ChainSlotPlugin() = default; + + ChainSlotPlugin* clone() const override { + auto newSpareSCBuffer = std::make_unique>(spareSCBuffer->getNumChannels(), spareSCBuffer->getNumSamples()); + return new ChainSlotPlugin(plugin, isBypassed, modulationConfig, getModulationValueCallback, editorBounds, std::move(newSpareSCBuffer)); + } + +private: + ChainSlotPlugin( + std::shared_ptr newPlugin, + bool newIsBypassed, + std::shared_ptr newModulationConfig, + std::function newGetModulationValueCallback, + std::shared_ptr newEditorBounds, + std::unique_ptr> newSpareSCBuffer) + : ChainSlotBase(newIsBypassed), + plugin(newPlugin), + modulationConfig(std::shared_ptr(newModulationConfig->clone())), + getModulationValueCallback(newGetModulationValueCallback), + editorBounds(newEditorBounds), + spareSCBuffer(std::move(newSpareSCBuffer)) {} +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots_test.cpp new file mode 100644 index 00000000..811a3100 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ChainSlots_test.cpp @@ -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(); + auto modulationCallback = [](int, MODULATION_TYPE) { + // Return something unique we can test for later + return 1.2f; + }; + + auto pluginSlot = std::make_shared(plugin, false, modulationCallback, hostConfig); + + REQUIRE(pluginSlot->spareSCBuffer != nullptr); + REQUIRE(pluginSlot->spareSCBuffer->getNumChannels() == 4); + REQUIRE(pluginSlot->spareSCBuffer->getNumSamples() == 10); + + auto modulationConfig = std::make_shared(); + modulationConfig->isActive = true; + + auto parameterModulationConfig = std::make_shared(); + parameterModulationConfig->targetParameterName = "test param"; + parameterModulationConfig->restValue = 0.75f; + + auto source = std::make_shared(); + 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; + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.cpp new file mode 100644 index 00000000..76c5b0f2 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.cpp @@ -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 +CloneableDelayLine::CloneableDelayLine() + : CloneableDelayLine (0) +{ +} + +template +CloneableDelayLine::CloneableDelayLine (int maximumDelayInSamples) +{ + jassert (maximumDelayInSamples >= 0); + + sampleRate = 44100.0; + + setMaximumDelayInSamples (maximumDelayInSamples); +} + +//============================================================================== +template +void CloneableDelayLine::setDelay (SampleType newDelayInSamples) +{ + auto upperLimit = (SampleType) getMaximumDelayInSamples(); + jassert (juce::isPositiveAndNotGreaterThan (newDelayInSamples, upperLimit)); + + delay = juce::jlimit ((SampleType) 0, upperLimit, newDelayInSamples); + delayInt = static_cast (std::floor (delay)); + delayFrac = delay - (SampleType) delayInt; + + updateInternalVariables(); +} + +template +SampleType CloneableDelayLine::getDelay() const +{ + return delay; +} + +//============================================================================== +template +void CloneableDelayLine::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 +void CloneableDelayLine::setMaximumDelayInSamples (int maxDelayInSamples) +{ + jassert (maxDelayInSamples >= 0); + totalSize = juce::jmax (4, maxDelayInSamples + 2); + bufferData.setSize ((int) bufferData.getNumChannels(), totalSize, false, false, true); + reset(); +} + +template +void CloneableDelayLine::reset() +{ + for (auto vec : { &writePos, &readPos }) + std::fill (vec->begin(), vec->end(), 0); + + std::fill (v.begin(), v.end(), static_cast (0)); + + bufferData.clear(); +} + +//============================================================================== +template +void CloneableDelayLine::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 +SampleType CloneableDelayLine::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; +template class CloneableDelayLine; +template class CloneableDelayLine; +template class CloneableDelayLine; +template class CloneableDelayLine; +template class CloneableDelayLine; +template class CloneableDelayLine; +template class CloneableDelayLine; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.hpp new file mode 100644 index 00000000..d3fa3c9b --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine.hpp @@ -0,0 +1,257 @@ +#pragma once + +#include + +// 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 +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 + 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 bufferData; + std::vector v; + std::vector 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) + { + auto index = (readPos[(size_t) channel] + delayInt) % totalSize; + return bufferData.getSample (channel, index); + } + else if constexpr (std::is_same_v) + { + 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) + { + 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) + { + 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) + { + if (delayFrac < (SampleType) 2.0 && delayInt >= 1) + { + delayFrac++; + delayInt--; + } + } + else if constexpr (std::is_same_v) + { + if (delayFrac < (SampleType) 0.618 && delayInt >= 1) + { + delayFrac++; + delayInt--; + } + + alpha = (1 - delayFrac) / (1 + delayFrac); + } + } +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine_test.cpp new file mode 100644 index 00000000..612cf636 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableDelayLine_test.cpp @@ -0,0 +1,47 @@ +#include "catch.hpp" +#include "TestUtils.hpp" + +#include "CloneableDelayLine.hpp" + +SCENARIO("CloneableDelayLine: Clone works correctly") { + GIVEN("A CloneableDelayLine") { + CloneableDelayLine 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 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 block(juce::dsp::AudioBlock(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; + } + } +} \ No newline at end of file diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.cpp new file mode 100644 index 00000000..a4177192 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.cpp @@ -0,0 +1,118 @@ +#include "CloneableLRFilter.hpp" + +//============================================================================== +template +CloneableLRFilter::CloneableLRFilter() +{ + update(); +} + +//============================================================================== +template +void CloneableLRFilter::setType (Type newType) +{ + filterType = newType; +} + +template +void CloneableLRFilter::setCutoffFrequency (SampleType newCutoffFrequencyHz) +{ + jassert (juce::isPositiveAndBelow (newCutoffFrequencyHz, static_cast (sampleRate * 0.5))); + + cutoffFrequency = newCutoffFrequencyHz; + update(); +} + +//============================================================================== +template +void CloneableLRFilter::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 +void CloneableLRFilter::reset() +{ + for (auto s : { &s1, &s2, &s3, &s4 }) + std::fill (s->begin(), s->end(), static_cast (0)); +} + +template +void CloneableLRFilter::snapToZero() noexcept +{ + for (auto s : { &s1, &s2, &s3, &s4 }) + for (auto& element : *s) + juce::dsp::util::snapToZero (element); +} + +//============================================================================== +template +SampleType CloneableLRFilter::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 +void CloneableLRFilter::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 +void CloneableLRFilter::update() +{ + g = (SampleType) std::tan (juce::MathConstants::pi * cutoffFrequency / sampleRate); + R2 = (SampleType) std::sqrt (2.0); + h = (SampleType) (1.0 / (1.0 + R2 * g + g * g)); +} + +//============================================================================== +template class CloneableLRFilter; +template class CloneableLRFilter; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.hpp new file mode 100644 index 00000000..943fba20 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include + +// 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 +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 + 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* clone() const { + return new CloneableLRFilter(*this); + } + + // Public for tests + SampleType g, R2, h; + std::vector s1, s2, s3, s4; + double sampleRate = 44100.0; + SampleType cutoffFrequency = 2000.0; + Type filterType = Type::lowpass; + +private: + CloneableLRFilter(const CloneableLRFilter& 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(); +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter_test.cpp new file mode 100644 index 00000000..11e469a6 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableLRFilter_test.cpp @@ -0,0 +1,48 @@ +#include "catch.hpp" +#include "TestUtils.hpp" + +#include "CloneableLRFilter.hpp" + +SCENARIO("CloneableLRFilter: Clone works correctly") { + GIVEN("A CloneableLRFilter") { + CloneableLRFilter 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 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 block(juce::dsp::AudioBlock(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; + } + } +} \ No newline at end of file diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources.hpp new file mode 100644 index 00000000..47e8a9b4 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources.hpp @@ -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> sources) { + _freqModulationSources = sources; + } + + void setDepthModulationSources(std::vector> sources) { + _depthModulationSources = sources; + } + + void setPhaseModulationSources(std::vector> 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; + } + }; +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources_test.cpp new file mode 100644 index 00000000..2b324242 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CloneableSources_test.cpp @@ -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(); + const auto depthSourceLFO = std::make_shared(); + const auto phaseSourceLFO = std::make_shared(); + + // 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; + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState.hpp new file mode 100644 index 00000000..6dc39221 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include "PluginChain.hpp" +#include "CloneableLRFilter.hpp" + +struct BandState { + bool isSoloed; + std::shared_ptr chain; + + BandState() : isSoloed(false) { + } +}; + +class CrossoverState { +public: + // Num low/highpass filters = num bands - 1 (= num crossovers) + std::vector>> lowpassFilters; + std::vector>> highpassFilters; + + // Num allpass filters = num bands - 2 + std::vector>> allpassFilters; + + // Num buffers = num bands - 1 (= num crossovers) + std::vector> buffers; + + std::vector 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 createDefaultCrossoverState(HostConfiguration newConfig) { + auto state = std::make_shared(); + + // Initialise configuration for two bands + constexpr int DEFAULT_FREQ {1000}; + + state->lowpassFilters.emplace_back(new CloneableLRFilter()); + state->lowpassFilters[0]->setType(juce::dsp::LinkwitzRileyFilterType::lowpass); + state->lowpassFilters[0]->setCutoffFrequency(DEFAULT_FREQ); + + state->highpassFilters.emplace_back(new CloneableLRFilter()); + 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; +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState_test.cpp new file mode 100644 index 00000000..fa872322 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/CrossoverState_test.cpp @@ -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([](int, MODULATION_TYPE) { + // Return something unique we can test for later + return 1.2f; + }); + auto plugin = std::make_shared(); + 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; + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/DataModelInterface.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/DataModelInterface.hpp new file mode 100644 index 00000000..ad2aaefd --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/DataModelInterface.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#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 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> cachedcrossoverFrequencies; + + SplitterState(HostConfiguration config, + std::function getModulationValueCallback, + std::function 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 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> lfos; + std::vector> envelopes; + std::vector> randomSources; + + // Needed for the envelope followers to figure out which buffers to read from + HostConfiguration hostConfig; + + std::function getModulationValueCallback; + + ModulationSourcesState(std::function 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 lfo : other.lfos) { + lfos.emplace_back(lfo->clone()); + } + + for (std::shared_ptr env : other.envelopes) { + envelopes.emplace_back(env->clone()); + } + + for (std::shared_ptr source : other.randomSources) { + randomSources.emplace_back(source->clone()); + } + } + }; + + struct StateWrapper { + std::shared_ptr splitterState; + std::shared_ptr modulationSourcesState; + + // String representation of the operation that was performed to get to this state + juce::String operation; + + StateWrapper(HostConfiguration config, + std::function getModulationValueCallback, + std::function latencyChangeCallback) : + splitterState(new SplitterState(config, getModulationValueCallback, latencyChangeCallback)), + modulationSourcesState(new ModulationSourcesState(getModulationValueCallback)), + operation("") { } + + StateWrapper(std::shared_ptr newSplitterState, + std::shared_ptr newModulationSourcesState, + juce::String newOperation) : splitterState(newSplitterState), + modulationSourcesState(newModulationSourcesState), + operation(newOperation) { } + }; + + struct StateManager { + std::deque> undoHistory; + std::deque> 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 getModulationValueCallback, + std::function latencyChangeCallback) { + undoHistory.push_back(std::make_shared(config, getModulationValueCallback, latencyChangeCallback)); + } + + // TODO remove this once all functions are migrated + SplitterState& getSplitterStateUnsafe() { return *(undoHistory.back()->splitterState); } + std::shared_ptr getSourcesStateUnsafe() { return undoHistory.back()->modulationSourcesState; } + }; +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.cpp new file mode 100644 index 00000000..b90770e5 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.cpp @@ -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& buffer) { + WECore::AudioSpinTryLock lock(_fftMutex); + if (lock.isLocked()) { + const size_t numBuffersRequired {static_cast( + std::ceil(static_cast(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(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]); + } + } + } +} \ No newline at end of file diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.hpp new file mode 100644 index 00000000..bf379d53 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/FFTProvider.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#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& 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 _envs; + WECore::AudioSpinMutex _fftMutex; + bool _isStereo; + float _binWidth; +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.cpp new file mode 100644 index 00000000..505c1502 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.cpp @@ -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(_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; +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.hpp new file mode 100644 index 00000000..9b541336 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/LatencyListener.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include + +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; +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ModulationSourceDefinition.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ModulationSourceDefinition.hpp new file mode 100644 index 00000000..2f956b38 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/ModulationSourceDefinition.hpp @@ -0,0 +1,128 @@ +#pragma once + +#include + +#include + +#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 fromVariant(juce::var variant) { + std::optional 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 { +public: + ModulationSourceDefinition definition; + + ModulationSourceProvider( + ModulationSourceDefinition newDefinition, + std::function getModulationValueCallback): + definition(newDefinition), _getModulationValueCallback(getModulationValueCallback) { } + + double getLastOutput() const override { + return _getModulationValueCallback(definition.id, definition.type); + } + +private: + std::function _getModulationValueCallback; + + virtual double _getNextOutputImpl(double /*inSample*/) override { return 0.0; } + + virtual void _resetImpl() override {} +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain.hpp new file mode 100644 index 00000000..520862f7 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +#include "ChainSlots.hpp" +#include "LatencyListener.hpp" +#include "General/AudioSpinMutex.h" +#include "CloneableDelayLine.hpp" + +typedef CloneableDelayLine CloneableDelayLineType; + +class PluginChain { +public: + std::vector> chain; + + bool isChainBypassed; + bool isChainMuted; + + std::function getModulationValueCallback; + + std::unique_ptr latencyCompLine; + WECore::AudioSpinMutex latencyCompLineMutex; + + PluginChainLatencyListener latencyListener; + + juce::String customName; + + PluginChain(std::function 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 plugin = std::dynamic_pointer_cast(slot)) { + plugin->plugin->removeListener(&latencyListener); + } + } + } + + PluginChain* clone() const { + return new PluginChain( + chain, + isChainBypassed, + isChainMuted, + getModulationValueCallback, + std::unique_ptr(latencyCompLine->clone()), + customName + ); + } + +private: + PluginChain( + std::vector> newChain, + bool newIsChainBypassed, + bool newIsChainMuted, + std::function newGetModulationValueCallback, + std::unique_ptr 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(slot->clone())); + + if (auto plugin = std::dynamic_pointer_cast(slot)) { + plugin->plugin->addListener(&latencyListener); + } + } + + latencyListener.onPluginChainUpdate(); + } +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain_test.cpp new file mode 100644 index 00000000..edd2ec85 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginChain_test.cpp @@ -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(); + auto modulationCallback = [](int, MODULATION_TYPE) { + // Return something unique we can test for later + return 1.2f; + }; + auto pluginSlot = std::make_shared(plugin, false, modulationCallback, hostConfig); + + auto chain = std::make_shared(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(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(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(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(); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.cpp new file mode 100644 index 00000000..8e2d4f0a --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.cpp @@ -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 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 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; +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.hpp new file mode 100644 index 00000000..bf366048 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include + +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 plugin, + HostConfiguration configuration) const; + +private: + juce::AudioProcessor::BusesLayout monoInMonoOut; + juce::AudioProcessor::BusesLayout monoInMonoOutSC; + juce::AudioProcessor::BusesLayout stereoInStereoOut; + juce::AudioProcessor::BusesLayout stereoInStereoOutSC; +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator_test.cpp new file mode 100644 index 00000000..3157332c --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginConfigurator_test.cpp @@ -0,0 +1,153 @@ +#include "catch.hpp" + +#include "TestUtils.hpp" + +namespace { + class ConfigTestPluginInstance : public TestUtils::TestPluginInstance { + public: + bool isPrepared; + std::function onIsBusesLayoutSupported; + + ConfigTestPluginInstance(std::function 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 getExpectedLayouts(std::string name) { + std::vector 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 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 testedLayouts; + auto plugin = std::make_shared([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); + } + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter.hpp new file mode 100644 index 00000000..6f5453ed --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter.hpp @@ -0,0 +1,379 @@ +#pragma once + +#include + +#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 newChain, bool newIsSoloed) + : chain(newChain), isSoloed(newIsSoloed) {} + + std::shared_ptr 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 chains; + size_t numChainsSoloed; + HostConfiguration config; + std::function getModulationValueCallback; + std::function notifyProcessorOnLatencyChange; + bool shouldNotifyProcessorOnLatencyChange; + + PluginSplitter(int defaultNumChains, + HostConfiguration newConfig, + std::function newGetModulationValueCallback, + std::function 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(getModulationValueCallback), false); + chains[chains.size() - 1].chain->latencyListener.setSplitter(this); + } + onLatencyChange(); + } + + PluginSplitter(std::shared_ptr 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(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 newChains, + HostConfiguration newConfig, + std::function newGetModulationValueCallback, + std::function newNotifyProcessorOnLatencyChange) : + numChainsSoloed(0), + config(newConfig), + getModulationValueCallback(newGetModulationValueCallback), + notifyProcessorOnLatencyChange(newNotifyProcessorOnLatencyChange), + shouldNotifyProcessorOnLatencyChange(true) { + for (auto& chain : newChains) { + std::shared_ptr 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 getModulationValueCallback, + std::function latencyChangeCallback) + : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback) { + juce::Logger::writeToLog("Constructed PluginSplitterSeries"); + } + + PluginSplitterSeries(std::shared_ptr 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 newChains, + HostConfiguration newConfig, + std::function newGetModulationValueCallback, + std::function 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> inputBuffer; + std::unique_ptr> outputBuffer; + + PluginSplitterParallel(HostConfiguration newConfig, + std::function getModulationValueCallback, + std::function latencyChangeCallback) + : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback) { + juce::Logger::writeToLog("Constructed PluginSplitterParallel"); + } + + PluginSplitterParallel(std::shared_ptr 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 newChains, + HostConfiguration newConfig, + std::function newGetModulationValueCallback, + std::function newNotifyProcessorOnLatencyChange, + const juce::AudioBuffer& newInputBuffer, + const juce::AudioBuffer& newOutputBuffer) : + PluginSplitter(newChains, newConfig, newGetModulationValueCallback, newNotifyProcessorOnLatencyChange) { + // We need to copy the buffers as well + inputBuffer.reset(new juce::AudioBuffer(newInputBuffer)); + outputBuffer.reset(new juce::AudioBuffer(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 crossover; + FFTProvider fftProvider; + + PluginSplitterMultiband(HostConfiguration newConfig, + std::function getModulationValueCallback, + std::function 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 otherSplitter, std::optional> 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 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(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 newChains, + HostConfiguration newConfig, + std::function newGetModulationValueCallback, + std::function newNotifyProcessorOnLatencyChange, + std::shared_ptr 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> leftBuffer; + std::unique_ptr> rightBuffer; + + PluginSplitterLeftRight(HostConfiguration newConfig, + std::function getModulationValueCallback, + std::function latencyChangeCallback) + : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback) { + juce::Logger::writeToLog("Constructed PluginSplitterLeftRight"); + } + + PluginSplitterLeftRight(std::shared_ptr 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 newChains, + HostConfiguration newConfig, + std::function newGetModulationValueCallback, + std::function newNotifyProcessorOnLatencyChange, + const juce::AudioBuffer& newLeftBuffer, + const juce::AudioBuffer& newRightBuffer) : + PluginSplitter(newChains, newConfig, newGetModulationValueCallback, newNotifyProcessorOnLatencyChange) { + // We need to copy the buffers as well + leftBuffer.reset(new juce::AudioBuffer(newLeftBuffer)); + rightBuffer.reset(new juce::AudioBuffer(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> midBuffer; + std::unique_ptr> sideBuffer; + + PluginSplitterMidSide(HostConfiguration newConfig, + std::function getModulationValueCallback, + std::function latencyChangeCallback) + : PluginSplitter(DEFAULT_NUM_CHAINS, newConfig, getModulationValueCallback, latencyChangeCallback) { + juce::Logger::writeToLog("Constructed PluginSplitterMidSide"); + } + + PluginSplitterMidSide(std::shared_ptr 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 newChains, + HostConfiguration newConfig, + std::function newGetModulationValueCallback, + std::function newNotifyProcessorOnLatencyChange, + const juce::AudioBuffer& newMidBuffer, + const juce::AudioBuffer& newSideBuffer) : + PluginSplitter(newChains, newConfig, newGetModulationValueCallback, newNotifyProcessorOnLatencyChange) { + // We need to copy the buffers as well + midBuffer.reset(new juce::AudioBuffer(newMidBuffer)); + sideBuffer.reset(new juce::AudioBuffer(newSideBuffer)); + } +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter_test.cpp new file mode 100644 index 00000000..9624a5b1 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/PluginSplitter_test.cpp @@ -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 originalSlot = originalChain.chain->chain[slotIndex]; + std::shared_ptr clonedSlot = clonedChain.chain->chain[slotIndex]; + + if (std::shared_ptr originalPluginSlot = std::dynamic_pointer_cast(originalSlot)) { + // It's a plugin slot, check the plugin + auto clonedPluginSlot = std::dynamic_pointer_cast(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(originalSlot); + auto clonedGainStage = std::dynamic_pointer_cast(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& buffer1, juce::AudioBuffer& 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(hostConfig, modulationCallback, latencyCallback); + + auto plugin = std::make_shared(); + 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(hostConfig, modulationCallback, latencyCallback); + + auto plugin = std::make_shared(); + 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(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(); + 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 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(hostConfig, modulationCallback, latencyCallback); + + auto plugin = std::make_shared(); + 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(hostConfig, modulationCallback, latencyCallback); + + auto plugin = std::make_shared(); + 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(); +} \ No newline at end of file diff --git a/ports-juce7/syndicate/PluginCommon/Processor/DataModel/SplitTypes.hpp b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/SplitTypes.hpp new file mode 100644 index 00000000..120c8bbe --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/DataModel/SplitTypes.hpp @@ -0,0 +1,9 @@ +#pragma once + +enum class SPLIT_TYPE { + SERIES, + PARALLEL, + MULTIBAND, + LEFTRIGHT, + MIDSIDE +}; diff --git a/ports-juce7/syndicate/PluginCommon/Processor/ModelInterface.hpp b/ports-juce7/syndicate/PluginCommon/Processor/ModelInterface.hpp new file mode 100644 index 00000000..f5533744 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/ModelInterface.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include "DataModelInterface.hpp" +#include "MutatorsInterface.hpp" +#include "ProcessingInterface.hpp" \ No newline at end of file diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.cpp new file mode 100644 index 00000000..6541ae66 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.cpp @@ -0,0 +1,216 @@ +#include "ChainMutators.hpp" +#include "ChainSlotProcessors.hpp" + +namespace ChainMutators { + void insertPlugin(std::shared_ptr chain, std::shared_ptr plugin, int position, HostConfiguration config) { + if (chain->chain.size() > position) { + chain->chain.insert(chain->chain.begin() + position, std::make_shared(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(plugin, false, chain->getModulationValueCallback, config)); + } + + plugin->addListener(&chain->latencyListener); + chain->latencyListener.onPluginChainUpdate(); + } + + void replacePlugin(std::shared_ptr chain, std::shared_ptr 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(chain->chain[position])) { + oldPluginSlot->plugin->removeListener(&chain->latencyListener); + } + + chain->chain[position] = std::make_unique(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(plugin, false, chain->getModulationValueCallback, config)); + } + + plugin->addListener(&chain->latencyListener); + chain->latencyListener.onPluginChainUpdate(); + } + + bool removeSlot(std::shared_ptr 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(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 chain, int position, HostConfiguration config) { + auto gainStage = std::make_shared(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 getPlugin(std::shared_ptr chain, int position) { + if (chain->chain.size() > position) { + if (const auto pluginSlot = std::dynamic_pointer_cast(chain->chain[position])) { + return pluginSlot->plugin; + } + } + + return nullptr; + } + + bool setPluginModulationConfig(std::shared_ptr chain, + PluginModulationConfig config, + int position) { + if (chain->chain.size() > position) { + if (const auto pluginSlot = std::dynamic_pointer_cast(chain->chain[position])) { + pluginSlot->modulationConfig = std::make_shared(config); + return true; + } + } + + return false; + } + + PluginModulationConfig getPluginModulationConfig(std::shared_ptr chain, int position) { + PluginModulationConfig retVal; + + if (chain->chain.size() > position) { + if (const auto pluginSlot = std::dynamic_pointer_cast(chain->chain[position])) { + retVal = *pluginSlot->modulationConfig.get(); + } + } + + return retVal; + } + + bool setSlotBypass(std::shared_ptr 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 chain, int position) { + if (chain->chain.size() > position) { + return chain->chain[position]->isBypassed; + } + + return false; + } + + void setChainBypass(std::shared_ptr chain, bool val) { + chain->isChainBypassed = val; + + // Trigger an update to the latency compensation + chain->latencyListener.onPluginChainUpdate(); + } + + void setChainMute(std::shared_ptr chain, bool val) { + chain->isChainMuted = val; + } + + bool setGainLinear(std::shared_ptr chain, int position, float gain) { + if (chain->chain.size() > position) { + if (const auto gainStage = std::dynamic_pointer_cast(chain->chain[position])) { + // TODO bounds check + gainStage->gain = gain; + return true; + } + } + + return false; + } + + float getGainLinear(std::shared_ptr chain, int position) { + if (chain->chain.size() > position) { + if (const auto gainStage = std::dynamic_pointer_cast(chain->chain[position])) { + return gainStage->gain; + } + } + + return 0.0f; + } + + float getGainStageOutputAmplitude(std::shared_ptr chain, int position, int channelNumber) { + if (chain->chain.size() > position) { + if (const auto gainStage = std::dynamic_pointer_cast(chain->chain[position])) { + if (channelNumber < gainStage->meterEnvelopes.size()) { + return gainStage->meterEnvelopes[channelNumber].getLastOutput(); + } + } + } + + return 0.0f; + } + + bool setPan(std::shared_ptr chain, int position, float pan) { + if (chain->chain.size() > position) { + if (const auto gainStage = std::dynamic_pointer_cast(chain->chain[position])) { + // TODO bounds check + gainStage->pan = pan; + return true; + } + } + + return false; + } + + float getPan(std::shared_ptr chain, int position) { + if (chain->chain.size() > position) { + if (const auto gainStage = std::dynamic_pointer_cast(chain->chain[position])) { + return gainStage->pan; + } + } + + return 0.0f; + } + + void setRequiredLatency(std::shared_ptr 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(config.blockSize), + static_cast(getTotalNumInputChannels(config.layout)) + }); + chain->latencyCompLine->setDelay(compensation); + } + + std::shared_ptr getPluginEditorBounds(std::shared_ptr chain, int position) { + std::shared_ptr retVal(new PluginEditorBounds()); + + if (chain->chain.size() > position) { + if (const auto pluginSlot = std::dynamic_pointer_cast(chain->chain[position])) { + retVal = pluginSlot->editorBounds; + } + } + + return retVal; + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.hpp new file mode 100644 index 00000000..005cbc31 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include +#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 chain, + std::shared_ptr plugin, + int position, + HostConfiguration config); + + /** + * Replaces an existing plugin in the chain. + */ + void replacePlugin(std::shared_ptr chain, + std::shared_ptr plugin, + int position, + HostConfiguration config); + + /** + * Removes the plugin or gain stage at the given position in the chain. + */ + bool removeSlot(std::shared_ptr 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 chain, int position, HostConfiguration config); + + /** + * Returns a pointer to the plugin at the given position. + */ + std::shared_ptr getPlugin(std::shared_ptr chain, + int position); + + /** + * Set the modulation config for the given plugin to the one provided. + */ + bool setPluginModulationConfig(std::shared_ptr chain, + PluginModulationConfig config, + int position); + + /** + * Returns the modulation config for the given plugin. + */ + PluginModulationConfig getPluginModulationConfig(std::shared_ptr chain, + int position); + + /** + * Returns the number of plugins and gain stages in this chain. + */ + inline size_t getNumSlots(std::shared_ptr chain) { return chain->chain.size(); } + + /** + * Bypasses or enables the slot at the given position. + */ + bool setSlotBypass(std::shared_ptr chain, int position, bool isBypassed); + + /** + * Returns true if the slot is bypassed. + */ + bool getSlotBypass(std::shared_ptr chain, int position); + + /** + * Bypasses the entire chain if set to true. + */ + void setChainBypass(std::shared_ptr chain, bool val); + + /** + * Mutes the entire chain if set to true. + */ + void setChainMute(std::shared_ptr chain, bool val); + + /** + * Sets the gain for the gain stage at the given position. + */ + bool setGainLinear(std::shared_ptr chain, int position, float gain); + + /** + * Returns the gain for the gain stage at the given position. + */ + float getGainLinear(std::shared_ptr chain, int position); + + float getGainStageOutputAmplitude(std::shared_ptr chain, int position, int channelNumber); + + /** + * Sets the pan/balance for the gain stage at the given position. + */ + bool setPan(std::shared_ptr chain, int position, float pan); + + /** + * Returns the pan/balance for the gain stage at the given position. + */ + float getPan(std::shared_ptr 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 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 getPluginEditorBounds(std::shared_ptr chain, int position); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators_test.cpp new file mode 100644 index 00000000..3fda88df --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ChainMutators_test.cpp @@ -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("param1")); + addHostedParameter(std::make_unique("param2")); + addHostedParameter(std::make_unique("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(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(); + 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(); + 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(); + 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(); + plugin->setLatencySamples(25); + + auto oldPlugin = std::dynamic_pointer_cast(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(); + 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(); + 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(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(modulationCallback); + ChainMutators::insertPlugin(chain, std::make_shared(), 0, hostConfig); + ChainMutators::insertGainStage(chain, 1, {layout, SAMPLE_RATE, NUM_SAMPLES}); + ChainMutators::insertPlugin(chain, std::make_shared(), 2, hostConfig); + ChainMutators::insertPlugin(chain, std::make_shared(), 3, hostConfig); + + WHEN("The config is set for a plugin") { + PluginModulationConfig config; + config.isActive = true; + + auto paramConfig = std::make_shared(); + 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(); + 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(modulationCallback); + + REQUIRE(chain->latencyListener.calculatedTotalPluginLatency == 0); + + { + auto plugin = std::make_shared(); + plugin->setLatencySamples(10); + ChainMutators::insertPlugin(chain, plugin, 0, hostConfig); + } + + ChainMutators::insertGainStage(chain, 1, {layout, SAMPLE_RATE, NUM_SAMPLES}); + + { + auto plugin = std::make_shared(); + 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(modulationCallback); + + REQUIRE(chain->latencyListener.calculatedTotalPluginLatency == 0); + + auto plugin = std::make_shared(); + 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(modulationCallback); + auto plugin = std::make_shared(); + + 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); + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.cpp new file mode 100644 index 00000000..b0b38f6a --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.cpp @@ -0,0 +1,180 @@ +#include "CrossoverMutators.hpp" +#include "CrossoverProcessors.hpp" + +namespace { + constexpr int MAX_FREQ {20000}; +} + +namespace CrossoverMutators { + void setIsSoloed(std::shared_ptr 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 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 state, size_t bandNumber, std::shared_ptr chain) { + if (state->bands.size() > bandNumber) { + state->bands[bandNumber].chain = chain; + } + } + + bool getIsSoloed(std::shared_ptr state, size_t bandNumber) { + if (state->bands.size() > bandNumber) { + return state->bands[bandNumber].isSoloed; + } + + return false; + } + + double getCrossoverFrequency(std::shared_ptr state, size_t crossoverNumber) { + if (state->lowpassFilters.size() > crossoverNumber) { + return state->lowpassFilters[crossoverNumber]->getCutoffFrequency(); + } + + return 0; + } + + size_t getNumBands(std::shared_ptr state) { + return state->bands.size(); + } + + void addBand(std::shared_ptr state) { + const double oldHighestCrossover {CrossoverMutators::getCrossoverFrequency(state, state->lowpassFilters.size() - 1)}; + + // Add all the new state + state->lowpassFilters.emplace_back(new CloneableLRFilter()); + state->lowpassFilters[state->lowpassFilters.size() - 1]->setType(juce::dsp::LinkwitzRileyFilterType::lowpass); + + state->highpassFilters.emplace_back(new CloneableLRFilter()); + state->highpassFilters[state->highpassFilters.size() - 1]->setType(juce::dsp::LinkwitzRileyFilterType::highpass); + + state->allpassFilters.emplace_back(new CloneableLRFilter()); + 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 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 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; + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.hpp new file mode 100644 index 00000000..d5afad0d --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/CrossoverMutators.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "CrossoverState.hpp" + +namespace CrossoverMutators { + void setIsSoloed(std::shared_ptr state, size_t bandNumber, bool isSoloed); + bool setCrossoverFrequency(std::shared_ptr state, size_t crossoverNumber, double val); + void setPluginChain(std::shared_ptr state, size_t bandNumber, std::shared_ptr chain); + + bool getIsSoloed(std::shared_ptr state, size_t bandNumber); + double getCrossoverFrequency(std::shared_ptr state, size_t crossoverNumber); + size_t getNumBands(std::shared_ptr state); + + void addBand(std::shared_ptr state); + bool removeBand(std::shared_ptr state, size_t bandNumber); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.cpp new file mode 100644 index 00000000..976e8e39 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.cpp @@ -0,0 +1,733 @@ +#include "ModulationMutators.hpp" + +namespace { + std::vector> deleteSourceFromTargetSources( + std::vector> sources, + ModulationSourceDefinition definition, + std::function 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(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 newSource; + + newSource.source = std::make_shared( + 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 sources) { + std::shared_ptr newLfo {new ModelInterface::CloneableLFO()}; + newLfo->setBypassSwitch(true); + newLfo->setSampleRate(sources->hostConfig.sampleRate); + sources->lfos.push_back(newLfo); + } + + bool setLfoTempoSyncSwitch(std::shared_ptr sources, int lfoIndex, bool val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setTempoSyncSwitch(val); + return true; + } + + return false; + } + + bool setLfoInvertSwitch(std::shared_ptr sources, int lfoIndex, bool val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setInvertSwitch(val); + return true; + } + + return false; + } + + bool setLfoOutputMode(std::shared_ptr sources, int lfoIndex, int val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setOutputMode(val); + return true; + } + + return false; + } + + bool setLfoWave(std::shared_ptr sources, int lfoIndex, int val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setWave(val); + return true; + } + + return false; + } + + bool setLfoTempoNumer(std::shared_ptr sources, int lfoIndex, int val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setTempoNumer(val); + return true; + } + + return false; + } + + bool setLfoTempoDenom(std::shared_ptr sources, int lfoIndex, int val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setTempoDenom(val); + return true; + } + + return false; + } + + bool setLfoFreq(std::shared_ptr sources, int lfoIndex, double val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setFreq(val); + return true; + } + + return false; + } + + bool setLfoDepth(std::shared_ptr sources, int lfoIndex, double val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setDepth(val); + return true; + } + + return false; + } + + bool setLfoManualPhase(std::shared_ptr sources, int lfoIndex, double val) { + if (sources->lfos.size() > lfoIndex) { + sources->lfos[lfoIndex]->setManualPhase(val); + return true; + } + + return false; + } + + bool addSourceToLFOFreq(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source) { + if (sources->lfos.size() <= lfoIndex) { + return false; + } + + auto sourceProvider = std::make_shared(source, sources->getModulationValueCallback); + return sources->lfos[lfoIndex]->addFreqModulationSource(std::dynamic_pointer_cast>(sourceProvider)); + } + + bool removeSourceFromLFOFreq(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source) { + if (sources->lfos.size() <= lfoIndex) { + return false; + } + + std::vector> existingSources = sources->lfos[lfoIndex]->getFreqModulationSources(); + for (const auto& existingSource : existingSources) { + auto thisSource = std::dynamic_pointer_cast(existingSource.source); + if (thisSource != nullptr && thisSource->definition == source) { + return sources->lfos[lfoIndex]->removeFreqModulationSource(existingSource.source); + } + } + + return false; + } + + bool setLFOFreqModulationAmount(std::shared_ptr sources, int lfoIndex, int sourceIndex, double val) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->setFreqModulationAmount(sourceIndex, val); + } + + return false; + } + + std::vector> getLFOFreqModulationSources(std::shared_ptr sources, int lfoIndex) { + // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig + std::vector> retVal; + + if (sources->lfos.size() > lfoIndex) { + for (const auto& source : sources->lfos[lfoIndex]->getFreqModulationSources()) { + auto thisSource = std::dynamic_pointer_cast(source.source); + retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); + } + } + + return retVal; + } + + bool addSourceToLFODepth(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source) { + if (sources->lfos.size() <= lfoIndex) { + return false; + } + + auto sourceProvider = std::make_shared(source, sources->getModulationValueCallback); + return sources->lfos[lfoIndex]->addDepthModulationSource(std::dynamic_pointer_cast>(sourceProvider)); + } + + bool removeSourceFromLFODepth(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source) { + if (sources->lfos.size() <= lfoIndex) { + return false; + } + + std::vector> existingSources = sources->lfos[lfoIndex]->getDepthModulationSources(); + for (const auto& existingSource : existingSources) { + auto thisSource = std::dynamic_pointer_cast(existingSource.source); + if (thisSource != nullptr && thisSource->definition == source) { + return sources->lfos[lfoIndex]->removeDepthModulationSource(existingSource.source); + } + } + + return false; + } + + bool setLFODepthModulationAmount(std::shared_ptr sources, int lfoIndex, int sourceIndex, double val) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->setDepthModulationAmount(sourceIndex, val); + } + + return false; + } + + std::vector> getLFODepthModulationSources(std::shared_ptr sources, int lfoIndex) { + // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig + std::vector> retVal; + + if (sources->lfos.size() > lfoIndex) { + for (const auto& source : sources->lfos[lfoIndex]->getDepthModulationSources()) { + auto thisSource = std::dynamic_pointer_cast(source.source); + retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); + } + } + + return retVal; + } + + bool addSourceToLFOPhase(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source) { + if (sources->lfos.size() <= lfoIndex) { + return false; + } + + auto sourceProvider = std::make_shared(source, sources->getModulationValueCallback); + return sources->lfos[lfoIndex]->addPhaseModulationSource(std::dynamic_pointer_cast>(sourceProvider)); + } + + bool removeSourceFromLFOPhase(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source) { + if (sources->lfos.size() <= lfoIndex) { + return false; + } + + std::vector> existingSources = sources->lfos[lfoIndex]->getPhaseModulationSources(); + for (const auto& existingSource : existingSources) { + auto thisSource = std::dynamic_pointer_cast(existingSource.source); + if (thisSource != nullptr && thisSource->definition == source) { + return sources->lfos[lfoIndex]->removePhaseModulationSource(existingSource.source); + } + } + + return false; + } + + bool setLFOPhaseModulationAmount(std::shared_ptr sources, int lfoIndex, int sourceIndex, double val) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->setPhaseModulationAmount(sourceIndex, val); + } + + return false; + } + + std::vector> getLFOPhaseModulationSources(std::shared_ptr sources, int lfoIndex) { + // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig + std::vector> retVal; + + if (sources->lfos.size() > lfoIndex) { + for (const auto& source : sources->lfos[lfoIndex]->getPhaseModulationSources()) { + auto thisSource = std::dynamic_pointer_cast(source.source); + retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); + } + } + + return retVal; + } + + bool getLfoTempoSyncSwitch(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getTempoSyncSwitch(); + } + + return false; + } + + bool getLfoInvertSwitch(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getInvertSwitch(); + } + + return false; + } + + int getLfoOutputMode(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getOutputMode(); + } + + return 0; + } + + int getLfoWave(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getWave(); + } + + return 0; + } + + double getLfoTempoNumer(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getTempoNumer(); + } + + return 0; + } + + double getLfoTempoDenom(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getTempoDenom(); + } + + return 0; + } + + double getLfoFreq(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getFreq(); + } + + return 0; + } + + double getLFOModulatedFreqValue(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getModulatedFreqValue(); + } + + return 0; + } + + double getLfoDepth(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getDepth(); + } + + return 0; + } + + double getLFOModulatedDepthValue(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getModulatedDepthValue(); + } + + return 0; + } + + double getLfoManualPhase(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getManualPhase(); + } + + return 0; + } + + double getLFOModulatedPhaseValue(std::shared_ptr sources, int lfoIndex) { + if (sources->lfos.size() > lfoIndex) { + return sources->lfos[lfoIndex]->getModulatedPhaseValue(); + } + + return 0; + } + + // + // Envelopes + // + void addEnvelope(std::shared_ptr sources) { + std::shared_ptr newEnv(new ModelInterface::EnvelopeWrapper()); + newEnv->envelope->setSampleRate(sources->hostConfig.sampleRate); + sources->envelopes.push_back(newEnv); + } + + bool setEnvAttackTimeMs(std::shared_ptr 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 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 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 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 sources, int envIndex, float val) { + if (sources->envelopes.size() > envIndex) { + sources->envelopes[envIndex]->amount = val; + return true; + } + + return false; + } + + bool setEnvUseSidechainInput(std::shared_ptr sources, int envIndex, bool val) { + if (sources->envelopes.size() > envIndex) { + sources->envelopes[envIndex]->useSidechainInput = val; + return true; + } + + return false; + } + + double getEnvAttackTimeMs(std::shared_ptr sources, int envIndex) { + if (sources->envelopes.size() > envIndex) { + return sources->envelopes[envIndex]->envelope->getAttackTimeMs(); + } + + return 0; + } + + double getEnvReleaseTimeMs(std::shared_ptr sources, int envIndex) { + if (sources->envelopes.size() > envIndex) { + return sources->envelopes[envIndex]->envelope->getReleaseTimeMs(); + } + + return 0; + } + + bool getEnvFilterEnabled(std::shared_ptr sources, int envIndex) { + if (sources->envelopes.size() > envIndex) { + return sources->envelopes[envIndex]->envelope->getFilterEnabled(); + } + + return false; + } + + double getEnvLowCutHz(std::shared_ptr sources, int envIndex) { + if (sources->envelopes.size() > envIndex) { + return sources->envelopes[envIndex]->envelope->getLowCutHz(); + } + + return 0; + } + + double getEnvHighCutHz(std::shared_ptr sources, int envIndex) { + if (sources->envelopes.size() > envIndex) { + return sources->envelopes[envIndex]->envelope->getHighCutHz(); + } + + return 0; + } + + float getEnvAmount(std::shared_ptr sources, int envIndex) { + if (sources->envelopes.size() > envIndex) { + return sources->envelopes[envIndex]->amount; + } + + return 0; + } + + bool getEnvUseSidechainInput(std::shared_ptr sources, int envIndex) { + if (sources->envelopes.size() > envIndex) { + return sources->envelopes[envIndex]->useSidechainInput; + } + + return false; + } + + double getEnvLastOutput(std::shared_ptr sources, int envIndex) { + if (sources->envelopes.size() > envIndex) { + return sources->envelopes[envIndex]->envelope->getLastOutput(); + } + + return 0; + } + + // + // Random + // + void addRandom(std::shared_ptr sources) { + std::shared_ptr newRandom(new WECore::Perlin::PerlinSource()); + newRandom->setSampleRate(sources->hostConfig.sampleRate); + sources->randomSources.push_back(newRandom); + } + + bool setRandomOutputMode(std::shared_ptr sources, int randomIndex, int val) { + if (sources->randomSources.size() > randomIndex) { + sources->randomSources[randomIndex]->setOutputMode(val); + return true; + } + + return false; + } + + bool setRandomFreq(std::shared_ptr sources, int randomIndex, double val) { + if (sources->randomSources.size() > randomIndex) { + sources->randomSources[randomIndex]->setFreq(val); + return true; + } + + return false; + } + + bool setRandomDepth(std::shared_ptr sources, int randomIndex, double val) { + if (sources->randomSources.size() > randomIndex) { + sources->randomSources[randomIndex]->setDepth(val); + return true; + } + + return false; + } + + bool addSourceToRandomFreq(std::shared_ptr sources, int randomIndex, ModulationSourceDefinition source) { + if (sources->randomSources.size() <= randomIndex) { + return false; + } + + auto sourceProvider = std::make_shared(source, sources->getModulationValueCallback); + return sources->randomSources[randomIndex]->addFreqModulationSource(std::dynamic_pointer_cast>(sourceProvider)); + } + + bool removeSourceFromRandomFreq(std::shared_ptr sources, int randomIndex, ModulationSourceDefinition source) { + if (sources->randomSources.size() <= randomIndex) { + return false; + } + + std::vector> existingSources = sources->randomSources[randomIndex]->getFreqModulationSources(); + for (const auto& existingSource : existingSources) { + auto thisSource = std::dynamic_pointer_cast(existingSource.source); + if (thisSource != nullptr && thisSource->definition == source) { + return sources->randomSources[randomIndex]->removeFreqModulationSource(existingSource.source); + } + } + + return false; + } + + bool setRandomFreqModulationAmount(std::shared_ptr sources, int randomIndex, int sourceIndex, double val) { + if (sources->randomSources.size() > randomIndex) { + return sources->randomSources[randomIndex]->setFreqModulationAmount(sourceIndex, val); + } + + return false; + } + + std::vector> getRandomFreqModulationSources(std::shared_ptr sources, int randomIndex) { + // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig + std::vector> retVal; + + if (sources->randomSources.size() > randomIndex) { + for (const auto& source : sources->randomSources[randomIndex]->getFreqModulationSources()) { + auto thisSource = std::dynamic_pointer_cast(source.source); + retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); + } + } + + return retVal; + } + + bool addSourceToRandomDepth(std::shared_ptr sources, int randomIndex, ModulationSourceDefinition source) { + if (sources->randomSources.size() <= randomIndex) { + return false; + } + + auto sourceProvider = std::make_shared(source, sources->getModulationValueCallback); + return sources->randomSources[randomIndex]->addDepthModulationSource(std::dynamic_pointer_cast>(sourceProvider)); + } + + bool removeSourceFromRandomDepth(std::shared_ptr sources, int randomIndex, ModulationSourceDefinition source) { + if (sources->randomSources.size() <= randomIndex) { + return false; + } + + std::vector> existingSources = sources->randomSources[randomIndex]->getDepthModulationSources(); + for (const auto& existingSource : existingSources) { + auto thisSource = std::dynamic_pointer_cast(existingSource.source); + if (thisSource != nullptr && thisSource->definition == source) { + return sources->randomSources[randomIndex]->removeDepthModulationSource(existingSource.source); + } + } + + return false; + } + + bool setRandomDepthModulationAmount(std::shared_ptr sources, int randomIndex, int sourceIndex, double val) { + if (sources->randomSources.size() > randomIndex) { + return sources->randomSources[randomIndex]->setDepthModulationAmount(sourceIndex, val); + } + + return false; + } + + std::vector> getRandomDepthModulationSources(std::shared_ptr sources, int randomIndex) { + // Use std::shared_ptr to be consistent with usage in PluginParameterModulationConfig + std::vector> retVal; + + if (sources->randomSources.size() > randomIndex) { + for (const auto& source : sources->randomSources[randomIndex]->getDepthModulationSources()) { + auto thisSource = std::dynamic_pointer_cast(source.source); + retVal.emplace_back(new PluginParameterModulationSource(thisSource->definition, source.amount)); + } + } + + return retVal; + } + + int getRandomOutputMode(std::shared_ptr sources, int randomIndex) { + if (sources->randomSources.size() > randomIndex) { + return sources->randomSources[randomIndex]->getOutputMode(); + } + + return 0; + } + + double getRandomFreq(std::shared_ptr sources, int randomIndex) { + if (sources->randomSources.size() > randomIndex) { + return sources->randomSources[randomIndex]->getFreq(); + } + + return 0; + } + + double getRandomModulatedFreqValue(std::shared_ptr sources, int randomIndex) { + if (sources->randomSources.size() > randomIndex) { + return sources->randomSources[randomIndex]->getModulatedFreqValue(); + } + + return 0; + } + + double getRandomDepth(std::shared_ptr sources, int randomIndex) { + if (sources->randomSources.size() > randomIndex) { + return sources->randomSources[randomIndex]->getDepth(); + } + + return 0; + } + + double getRandomModulatedDepthValue(std::shared_ptr sources, int randomIndex) { + if (sources->randomSources.size() > randomIndex) { + return sources->randomSources[randomIndex]->getModulatedDepthValue(); + } + + return 0; + } + + double getRandomLastOutput(std::shared_ptr 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 lfo : state.lfos) { + // Freq + std::vector> lfoFreqSources = lfo->getFreqModulationSources(); + lfoFreqSources = deleteSourceFromTargetSources(lfoFreqSources, definition, state.getModulationValueCallback); + lfo->setFreqModulationSources(lfoFreqSources); + + // Depth + std::vector> lfoDepthSources = lfo->getDepthModulationSources(); + lfoDepthSources = deleteSourceFromTargetSources(lfoDepthSources, definition, state.getModulationValueCallback); + lfo->setDepthModulationSources(lfoDepthSources); + + // Phase + std::vector> lfoPhaseSources = lfo->getPhaseModulationSources(); + lfoPhaseSources = deleteSourceFromTargetSources(lfoPhaseSources, definition, state.getModulationValueCallback); + lfo->setPhaseModulationSources(lfoPhaseSources); + } + + for (std::shared_ptr random : state.randomSources) { + // Freq + std::vector> randomFreqSources = random->getFreqModulationSources(); + randomFreqSources = deleteSourceFromTargetSources(randomFreqSources, definition, state.getModulationValueCallback); + random->setFreqModulationSources(randomFreqSources); + + // Depth + std::vector> 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; + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.hpp new file mode 100644 index 00000000..9910b682 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/ModulationMutators.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "DataModelInterface.hpp" + +namespace ModulationMutators { + // LFOs + void addLfo(std::shared_ptr sources); + bool setLfoTempoSyncSwitch(std::shared_ptr sources, int lfoIndex, bool val); + bool setLfoInvertSwitch(std::shared_ptr sources, int lfoIndex, bool val); + bool setLfoOutputMode(std::shared_ptr sources, int lfoIndex, int val); + bool setLfoWave(std::shared_ptr sources, int lfoIndex, int val); + bool setLfoTempoNumer(std::shared_ptr sources, int lfoIndex, int val); + bool setLfoTempoDenom(std::shared_ptr sources, int lfoIndex, int val); + bool setLfoFreq(std::shared_ptr sources, int lfoIndex, double val); + bool setLfoDepth(std::shared_ptr sources, int lfoIndex, double val); + bool setLfoManualPhase(std::shared_ptr sources, int lfoIndex, double val); + bool addSourceToLFOFreq(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source); + bool removeSourceFromLFOFreq(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source); + bool setLFOFreqModulationAmount(std::shared_ptr sources, int lfoIndex, int sourceIndex, double val); + std::vector> getLFOFreqModulationSources(std::shared_ptr sources, int lfoIndex); + bool addSourceToLFODepth(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source); + bool removeSourceFromLFODepth(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source); + bool setLFODepthModulationAmount(std::shared_ptr sources, int lfoIndex, int sourceIndex, double val); + std::vector> getLFODepthModulationSources(std::shared_ptr sources, int lfoIndex); + bool addSourceToLFOPhase(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source); + bool removeSourceFromLFOPhase(std::shared_ptr sources, int lfoIndex, ModulationSourceDefinition source); + bool setLFOPhaseModulationAmount(std::shared_ptr sources, int lfoIndex, int sourceIndex, double val); + std::vector> getLFOPhaseModulationSources(std::shared_ptr sources, int lfoIndex); + + bool getLfoTempoSyncSwitch(std::shared_ptr sources, int lfoIndex); + bool getLfoInvertSwitch(std::shared_ptr sources, int lfoIndex); + int getLfoOutputMode(std::shared_ptr sources, int lfoIndex); + int getLfoWave(std::shared_ptr sources, int lfoIndex); + double getLfoTempoNumer(std::shared_ptr sources, int lfoIndex); + double getLfoTempoDenom(std::shared_ptr sources, int lfoIndex); + double getLfoFreq(std::shared_ptr sources, int lfoIndex); + double getLFOModulatedFreqValue(std::shared_ptr sources, int lfoIndex); + double getLfoDepth(std::shared_ptr sources, int lfoIndex); + double getLFOModulatedDepthValue(std::shared_ptr sources, int lfoIndex); + double getLfoManualPhase(std::shared_ptr sources, int lfoIndex); + double getLFOModulatedPhaseValue(std::shared_ptr sources, int lfoIndex); + + // Envelopes + void addEnvelope(std::shared_ptr sources); + bool setEnvAttackTimeMs(std::shared_ptr sources, int envIndex, double val); + bool setEnvReleaseTimeMs(std::shared_ptr sources, int envIndex, double val); + bool setEnvFilterEnabled(std::shared_ptr sources, int envIndex, bool val); + bool setEnvFilterHz(std::shared_ptr sources, int envIndex, double lowCut, double highCut); + bool setEnvAmount(std::shared_ptr sources, int envIndex, float val); + bool setEnvUseSidechainInput(std::shared_ptr sources, int envIndex, bool val); + + double getEnvAttackTimeMs(std::shared_ptr sources, int envIndex); + double getEnvReleaseTimeMs(std::shared_ptr sources, int envIndex); + bool getEnvFilterEnabled(std::shared_ptr sources, int envIndex); + double getEnvLowCutHz(std::shared_ptr sources, int envIndex); + double getEnvHighCutHz(std::shared_ptr sources, int envIndex); + float getEnvAmount(std::shared_ptr sources, int envIndex); + bool getEnvUseSidechainInput(std::shared_ptr sources, int envIndex); + double getEnvLastOutput(std::shared_ptr sources, int envIndex); + + // Random + void addRandom(std::shared_ptr sources); + bool setRandomOutputMode(std::shared_ptr sources, int randomIndex, int val); + bool setRandomFreq(std::shared_ptr sources, int randomIndex, double val); + bool setRandomDepth(std::shared_ptr sources, int randomIndex, double val); + bool addSourceToRandomFreq(std::shared_ptr sources, int randomIndex, ModulationSourceDefinition source); + bool removeSourceFromRandomFreq(std::shared_ptr sources, int randomIndex, ModulationSourceDefinition source); + bool setRandomFreqModulationAmount(std::shared_ptr sources, int randomIndex, int sourceIndex, double val); + std::vector> getRandomFreqModulationSources(std::shared_ptr sources, int randomIndex); + bool addSourceToRandomDepth(std::shared_ptr sources, int randomIndex, ModulationSourceDefinition source); + bool removeSourceFromRandomDepth(std::shared_ptr sources, int randomIndex, ModulationSourceDefinition source); + bool setRandomDepthModulationAmount(std::shared_ptr sources, int randomIndex, int sourceIndex, double val); + std::vector> getRandomDepthModulationSources(std::shared_ptr sources, int randomIndex); + + int getRandomOutputMode(std::shared_ptr sources, int randomIndex); + double getRandomFreq(std::shared_ptr sources, int randomIndex); + double getRandomModulatedFreqValue(std::shared_ptr sources, int randomIndex); + double getRandomDepth(std::shared_ptr sources, int randomIndex); + double getRandomModulatedDepthValue(std::shared_ptr sources, int randomIndex); + double getRandomLastOutput(std::shared_ptr sources, int randomIndex); + + bool removeModulationSource(ModelInterface::ModulationSourcesState& state, ModulationSourceDefinition definition); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.cpp new file mode 100644 index 00000000..2322a4ba --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.cpp @@ -0,0 +1,1767 @@ +#include "MutatorsInterface.hpp" + +#include + +#include "SplitterMutators.hpp" +#include "ModulationMutators.hpp" +#include "XmlConsts.hpp" +#include "XmlReader.hpp" +#include "XmlWriter.hpp" + +namespace { + std::vector> deleteSourceFromTargetSources(std::vector> sources, ModulationSourceDefinition definition) { + bool needsToDelete {false}; + int indexToDelete {0}; + + // Iterate through each configured source + for (int sourceIndex {0}; sourceIndex < sources.size(); sourceIndex++) { + std::shared_ptr thisSource = sources[sourceIndex]; + + 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 + thisSource->definition.id--; + } + } + + if (needsToDelete) { + sources.erase(sources.begin() + indexToDelete); + } + + return sources; + } + + constexpr int MAX_HISTORY_SIZE = 20; + + void pushState(ModelInterface::StateManager& manager, + std::shared_ptr state) { + WECore::AudioSpinLock sharedLock(manager.sharedMutex); + + // Disable the latency change callback from the previous state, make sure the new one is enabled + manager.undoHistory.back()->splitterState->splitter->shouldNotifyProcessorOnLatencyChange = false; + state->splitterState->splitter->shouldNotifyProcessorOnLatencyChange = true; + + manager.undoHistory.push_back(state); + + while (manager.undoHistory.size() > MAX_HISTORY_SIZE) { + manager.undoHistory.pop_front(); + } + + // If the state is updated, the redo history is no longer valid + manager.redoHistory.clear(); + } + + /* Splitter */ + + std::shared_ptr cloneSplitterState(const ModelInterface::StateManager& manager) { + if (manager.undoHistory.size() == 0) { + return nullptr; + } + + return std::shared_ptr(manager.undoHistory.back()->splitterState->clone()); + } + + // Use this instead of manager.cloneSplitterState() for parameter changes + // It will only clone the splitter state if we're not already in the middle of a change + std::shared_ptr cloneSplitterStateIfNeeded(const ModelInterface::StateManager& manager, const juce::String& operation) { + const std::optional previousOperation = ModelInterface::getUndoOperation(manager); + + if (manager.undoHistory.size() == 0) { + return nullptr; + } + + if (previousOperation.has_value() && previousOperation.value() == operation) { + // We're already in the middle of this change, return the current state + return manager.undoHistory.back()->splitterState; + } + + // We're not in the middle of this change, clone the current state + return cloneSplitterState(manager); + } + + void pushSplitter(ModelInterface::StateManager& manager, + std::shared_ptr splitterState, + juce::String operation) { + if (manager.undoHistory.size() == 0) { + return; + } + + if (manager.undoHistory.back()->splitterState == splitterState) { + // This is a parameter change that we're already in the middle of, don't push a new state + return; + } + + auto newState = std::make_shared( + splitterState, manager.undoHistory.back()->modulationSourcesState, operation); + pushState(manager, newState); + } + + /* Sources */ + + std::shared_ptr cloneSourcesState(const ModelInterface::StateManager& manager) { + if (manager.undoHistory.size() == 0) { + return nullptr; + } + + return std::shared_ptr(manager.undoHistory.back()->modulationSourcesState->clone()); + } + + // Use this instead of manager.cloneSourcesState() for parameter changes + // It will only clone the sources state if we're not already in the middle of a change + std::shared_ptr cloneSourcesStateIfNeeded(const ModelInterface::StateManager& manager, const juce::String& operation) { + const std::optional previousOperation = ModelInterface::getUndoOperation(manager); + + if (manager.undoHistory.size() == 0) { + return nullptr; + } + + if (previousOperation.has_value() && previousOperation.value() == operation) { + // We're already in the middle of this change, return the current state + return manager.undoHistory.back()->modulationSourcesState; + } + + // We're not in the middle of this change, clone the current state + return cloneSourcesState(manager); + } + + void pushSources(ModelInterface::StateManager& manager, + std::shared_ptr sourcesState, + juce::String operation) { + if (manager.undoHistory.size() == 0) { + return; + } + + if (manager.undoHistory.back()->modulationSourcesState == sourcesState) { + // This is a parameter change that we're already in the middle of, don't push a new state + return; + } + + auto newState = std::make_shared( + manager.undoHistory.back()->splitterState, sourcesState, operation); + pushState(manager, newState); + } +} + +namespace ModelInterface { + bool setSplitType(StateManager& manager, SPLIT_TYPE splitType, HostConfiguration config) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + const SPLIT_TYPE previousSplitType = SplitterMutators::getSplitType(splitter->splitter); + + if (splitType != previousSplitType) { + if (auto multibandSplitter = std::dynamic_pointer_cast(splitter->splitter)) { + splitter->cachedcrossoverFrequencies = std::vector(); + + for (int index {0}; index < CrossoverMutators::getNumBands(multibandSplitter->crossover); index++) { + splitter->cachedcrossoverFrequencies.value().push_back(CrossoverMutators::getCrossoverFrequency(multibandSplitter->crossover, index)); + } + } + + switch (splitType) { + case SPLIT_TYPE::SERIES: + splitter->splitter.reset(new PluginSplitterSeries(splitter->splitter)); + break; + case SPLIT_TYPE::PARALLEL: + splitter->splitter.reset(new PluginSplitterParallel(splitter->splitter)); + break; + case SPLIT_TYPE::MULTIBAND: + splitter->splitter.reset(new PluginSplitterMultiband(splitter->splitter, splitter->cachedcrossoverFrequencies)); + break; + case SPLIT_TYPE::LEFTRIGHT: + if (canDoStereoSplitTypes(config.layout)) { + splitter->splitter.reset(new PluginSplitterLeftRight(splitter->splitter)); + } else { + juce::Logger::writeToLog("SyndicateAudioProcessor::setSplitType: Attempted to use left/right split while not in 2in2out configuration"); + assert(false); + } + break; + case SPLIT_TYPE::MIDSIDE: + if (canDoStereoSplitTypes(config.layout)) { + splitter->splitter.reset(new PluginSplitterMidSide(splitter->splitter)); + } else { + juce::Logger::writeToLog("SyndicateAudioProcessor::setSplitType: Attempted to use mid/side split while not in 2in2out configuration"); + assert(false); + } + break; + } + + // Make sure prepareToPlay has been called on the splitter as we don't actually know if the host + // will call it via the PluginProcessor + if (splitter->splitter != nullptr) { + SplitterProcessors::prepareToPlay(*splitter->splitter.get(), config.sampleRate, config.blockSize, config.layout); + } + + pushSplitter(manager, splitter, "change split type"); + return true; + } + + return false; + } + + SPLIT_TYPE getSplitType(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getSplitType(splitter.splitter); + } + + return SPLIT_TYPE::SERIES; + } + + bool replacePlugin(StateManager& manager, std::shared_ptr plugin, int chainNumber, int positionInChain) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + + if (SplitterMutators::replacePlugin(splitter->splitter, std::move(plugin), chainNumber, positionInChain)) { + pushSplitter(manager, splitter, "replace plugin " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1)); + return true; + } + + return false; + } + + bool removeSlot(StateManager& manager, int chainNumber, int positionInChain) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + if (SplitterMutators::removeSlot(splitter->splitter, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, "remove slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1)); + return true; + } + + return false; + } + + bool insertGainStage(StateManager& manager, int chainNumber, int positionInChain) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + if (SplitterMutators::insertGainStage(splitter->splitter, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, "insert gain stage in chain " + juce::String(chainNumber + 1)); + return true; + } + + return false; + } + + std::shared_ptr getPlugin(StateManager& manager, int chainNumber, int positionInChain) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getPlugin(splitter.splitter, chainNumber, positionInChain); + } + + return nullptr; + } + + bool setGainLinear(StateManager& manager, int chainNumber, int positionInChain, float gain) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set gain in chain " + juce::String(chainNumber + 1) + " slot " + juce::String(positionInChain + 1); + std::shared_ptr splitter = cloneSplitterStateIfNeeded(manager, operation); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + if (SplitterMutators::setGainLinear(splitter->splitter, chainNumber, positionInChain, gain)) { + pushSplitter(manager, splitter, operation); + return true; + } + + return false; + } + + bool setPan(StateManager& manager, int chainNumber, int positionInChain, float pan) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set pan in chain " + juce::String(chainNumber + 1) + " slot " + juce::String(positionInChain + 1); + std::shared_ptr splitter = cloneSplitterStateIfNeeded(manager, operation); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + if (SplitterMutators::setPan(splitter->splitter, chainNumber, positionInChain, pan)) { + pushSplitter(manager, splitter, operation); + return true; + } + + return false; + } + + std::tuple getGainLinearAndPan(StateManager& manager, int chainNumber, int positionInChain) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + const float gain {SplitterMutators::getGainLinear(splitter.splitter, chainNumber, positionInChain)}; + const float pan {SplitterMutators::getPan(splitter.splitter, chainNumber, positionInChain)}; + + return std::make_tuple(gain, pan); + } + + return std::make_tuple(0, 0); + } + + float getGainStageOutputAmplitude(StateManager& manager, int chainNumber, int positionInChain, int channelNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getGainStageOutputAmplitude(splitter.splitter, chainNumber, positionInChain, channelNumber); + } + + return 0.0f; + } + + PluginModulationConfig getPluginModulationConfig(StateManager& manager, int chainNumber, int positionInChain) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getPluginModulationConfig(splitter.splitter, chainNumber, positionInChain); + } + + return PluginModulationConfig(); + } + + void setPluginModulationIsActive(StateManager& manager, int chainNumber, int positionInChain, bool val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + PluginModulationConfig config = SplitterMutators::getPluginModulationConfig(splitter->splitter, chainNumber, positionInChain); + config.isActive = val; + + if (SplitterMutators::setPluginModulationConfig(splitter->splitter, config, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, "toggle modulation for slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1)); + } + } + + void setModulationTarget(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, juce::String targetName) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + PluginModulationConfig config = SplitterMutators::getPluginModulationConfig(splitter->splitter, chainNumber, positionInChain); + + // Increase the number of configs if needed + while (config.parameterConfigs.size() <= targetNumber) { + config.parameterConfigs.push_back(std::make_shared()); + } + + config.parameterConfigs[targetNumber]->targetParameterName = targetName; + + if (SplitterMutators::setPluginModulationConfig(splitter->splitter, config, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, "set modulation target for slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1)); + } + } + + void removeModulationTarget(StateManager& manager, int chainNumber, int positionInChain, int targetNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + PluginModulationConfig config = SplitterMutators::getPluginModulationConfig(splitter->splitter, chainNumber, positionInChain); + + if (config.parameterConfigs.size() <= targetNumber) { + return; + } + + config.parameterConfigs.erase(config.parameterConfigs.begin() + targetNumber); + + if (SplitterMutators::setPluginModulationConfig(splitter->splitter, config, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, "remove modulation target from slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1)); + } + } + + void addModulationSourceToTarget(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + PluginModulationConfig config = SplitterMutators::getPluginModulationConfig(splitter->splitter, chainNumber, positionInChain); + + if (config.parameterConfigs.size() <= targetNumber) { + return; + } + + config.parameterConfigs[targetNumber]->sources.emplace_back(std::make_shared(source, 0)); + + if (SplitterMutators::setPluginModulationConfig(splitter->splitter, config, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, "add modulation source to slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1)); + } + } + + void removeModulationSourceFromTarget(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + PluginModulationConfig config = SplitterMutators::getPluginModulationConfig(splitter->splitter, chainNumber, positionInChain); + + if (config.parameterConfigs.size() <= targetNumber) { + return; + } + + std::vector> updatedSources = config.parameterConfigs[targetNumber]->sources; + for (int index {0}; index < updatedSources.size(); index++) { + if (updatedSources[index]->definition == source) { + updatedSources.erase(updatedSources.begin() + index); + break; + } + } + + config.parameterConfigs[targetNumber]->sources = updatedSources; + + if (SplitterMutators::setPluginModulationConfig(splitter->splitter, config, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, "remove modulation source from slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1)); + } + } + + void setModulationTargetValue(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, float val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set value for target " + juce::String(targetNumber + 1) + " in slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1); + std::shared_ptr splitter = cloneSplitterStateIfNeeded(manager, operation); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + PluginModulationConfig config = SplitterMutators::getPluginModulationConfig(splitter->splitter, chainNumber, positionInChain); + + if (config.parameterConfigs.size() <= targetNumber) { + return; + } + + config.parameterConfigs[targetNumber]->restValue = val; + + if (SplitterMutators::setPluginModulationConfig(splitter->splitter, config, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, operation); + } + } + + void setModulationSourceValue(StateManager& manager, int chainNumber, int positionInChain, int targetNumber, int sourceNumber, float val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set amount for source " + juce::String(sourceNumber + 1) + " on target " + juce::String(targetNumber + 1) + " in slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1); + std::shared_ptr splitter = cloneSplitterStateIfNeeded(manager, operation); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + PluginModulationConfig config = SplitterMutators::getPluginModulationConfig(splitter->splitter, chainNumber, positionInChain); + + if (config.parameterConfigs.size() <= targetNumber) { + return; + } + + if (config.parameterConfigs[targetNumber]->sources.size() <= sourceNumber) { + return; + } + + config.parameterConfigs[targetNumber]->sources[sourceNumber]->modulationAmount = val; + + if (SplitterMutators::setPluginModulationConfig(splitter->splitter, config, chainNumber, positionInChain)) { + pushSplitter(manager, splitter, operation); + } + } + + void setSlotBypass(StateManager& manager, int chainNumber, int positionInChain, bool isBypassed) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + if (SplitterMutators::setSlotBypass(splitter->splitter, chainNumber, positionInChain, isBypassed)) { + pushSplitter(manager, splitter, "set bypass slot " + juce::String(positionInChain + 1) + " in chain " + juce::String(chainNumber + 1)); + } + } + + bool getSlotBypass(StateManager& manager, int chainNumber, int positionInChain) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getSlotBypass(splitter.splitter, chainNumber, positionInChain); + } + + return false; + } + + void setChainBypass(StateManager& manager, int chainNumber, bool val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + if (chainNumber >= splitter->splitter->chains.size()) { + return; + } + + ChainMutators::setChainBypass(splitter->splitter->chains[chainNumber].chain, val); + pushSplitter(manager, splitter, "set chain " + juce::String(chainNumber + 1) + " bypass"); + } + + void setChainMute(StateManager& manager, int chainNumber, bool val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + if (chainNumber >= splitter->splitter->chains.size()) { + return; + } + + ChainMutators::setChainMute(splitter->splitter->chains[chainNumber].chain, val); + pushSplitter(manager, splitter, "set chain " + juce::String(chainNumber + 1) + " mute"); + } + + void setChainSolo(StateManager& manager, int chainNumber, bool val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + if (SplitterMutators::setChainSolo(splitter->splitter, chainNumber, val)) { + pushSplitter(manager, splitter, "set chain " + juce::String(chainNumber + 1) + " solo"); + } + } + + bool getChainBypass(StateManager& manager, int chainNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return splitter.splitter->chains[chainNumber].chain->isChainBypassed; + } + + return false; + } + + bool getChainMute(StateManager& manager, int chainNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return splitter.splitter->chains[chainNumber].chain->isChainMuted; + } + + return false; + } + + bool getChainSolo(StateManager& manager, int chainNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getChainSolo(splitter.splitter, chainNumber); + } + + return false; + } + + void moveSlot(StateManager& manager, int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + if (SplitterMutators::moveSlot(splitter->splitter, fromChainNumber, fromSlotNumber, toChainNumber, toSlotNumber)) { + pushSplitter(manager, splitter, "move slot " + juce::String(fromSlotNumber + 1) + " in chain " + juce::String(fromChainNumber + 1) + " to slot " + juce::String(toSlotNumber + 1) + " in chain " + juce::String(toChainNumber + 1)); + } + } + + void copySlot(StateManager& manager, + std::function onSuccess, + juce::AudioPluginFormatManager& formatManager, + int fromChainNumber, + int fromSlotNumber, + int toChainNumber, + int toSlotNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + const juce::String operation = "copy slot " + juce::String(fromSlotNumber + 1) + " in chain " + juce::String(fromChainNumber + 1) + " to slot " + juce::String(toSlotNumber + 1) + " in chain " + juce::String(toChainNumber + 1); + auto wrappedOnSuccess = [onSuccess, &manager, splitter, operation](void) { + pushSplitter(manager, splitter, operation); + onSuccess(); + }; + + auto insertPlugin = [splitter, wrappedOnSuccess, toChainNumber, toSlotNumber](std::shared_ptr sharedPlugin, juce::MemoryBlock sourceState, bool isBypassed, PluginModulationConfig sourceConfig) { + // Hand the plugin over to the splitter + if (SplitterMutators::insertPlugin(splitter->splitter, sharedPlugin, toChainNumber, toSlotNumber)) { + // Apply plugin state + sharedPlugin->setStateInformation(sourceState.getData(), sourceState.getSize()); + + // Apply bypass + SplitterMutators::setSlotBypass(splitter->splitter, toChainNumber, toSlotNumber, isBypassed); + + // Apply modulation + SplitterMutators::setPluginModulationConfig(splitter->splitter, sourceConfig, toChainNumber, toSlotNumber); + + wrappedOnSuccess(); + } else { + juce::Logger::writeToLog("SyndicateAudioProcessor::copySlot: Failed to insert plugin"); + } + }; + + SplitterMutators::copySlot(splitter->splitter, insertPlugin, wrappedOnSuccess, formatManager, fromChainNumber, fromSlotNumber, toChainNumber, toSlotNumber); + } + + void moveChain(StateManager& manager, int fromChainNumber, int toChainNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + if (SplitterMutators::moveChain(splitter->splitter, fromChainNumber, toChainNumber)) { + pushSplitter(manager, splitter, "move chain " + juce::String(fromChainNumber + 1) + " to " + juce::String(toChainNumber + 1)); + } + } + + void copyChain(StateManager& manager, + std::function onSuccess, + juce::AudioPluginFormatManager& formatManager, + int fromChainNumber, + int toChainNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + const juce::String operation = "copy chain " + juce::String(fromChainNumber + 1) + " to " + juce::String(toChainNumber + 1); + + auto wrappedOnSuccess = [onSuccess, &manager, splitter, operation]() { + pushSplitter(manager, splitter, operation); + onSuccess(); + }; + + SplitterMutators::copyChain(splitter->splitter, wrappedOnSuccess, formatManager, fromChainNumber, toChainNumber); + } + + size_t getNumChains(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getNumChains(splitter.splitter); + } + + return 0; + } + + bool addParallelChain(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + auto parallelSplitter = std::dynamic_pointer_cast(splitter->splitter); + + if (parallelSplitter != nullptr) { + SplitterMutators::addChain(parallelSplitter); + pushSplitter(manager, splitter, "add chain"); + return true; + } + + return false; + } + + bool removeParallelChain(StateManager& manager, int chainNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + auto parallelSplitter = std::dynamic_pointer_cast(splitter->splitter); + + if (parallelSplitter != nullptr) { + if (SplitterMutators::removeChain(parallelSplitter, chainNumber)) { + pushSplitter(manager, splitter, "remove chain " + juce::String(chainNumber + 1)); + return true; + } + } + + return false; + } + + void addCrossoverBand(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return; + } + + auto multibandSplitter = std::dynamic_pointer_cast(splitter->splitter); + + if (multibandSplitter != nullptr) { + SplitterMutators::addBand(multibandSplitter); + pushSplitter(manager, splitter, "add band"); + } + } + + bool removeCrossoverBand(StateManager& manager, int bandNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + auto multibandSplitter = std::dynamic_pointer_cast(splitter->splitter); + + if (multibandSplitter != nullptr) { + if (SplitterMutators::removeBand(multibandSplitter, bandNumber)) { + pushSplitter(manager, splitter, "remove band " + juce::String(bandNumber + 1)); + return true; + } + } + + return false; + } + + bool setCrossoverFrequency(StateManager& manager, size_t index, float val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set crossover frequency"; + std::shared_ptr splitter = cloneSplitterStateIfNeeded(manager, operation); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + auto multibandSplitter = std::dynamic_pointer_cast(splitter->splitter); + + if (multibandSplitter != nullptr) { + if (index < SplitterMutators::getNumBands(multibandSplitter) - 1) { + if (SplitterMutators::setCrossoverFrequency(multibandSplitter, index, val)) { + pushSplitter(manager, splitter, operation); + return true; + } + } + } + + return false; + } + + float getCrossoverFrequency(StateManager& manager, size_t index) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + auto multibandSplitter = std::dynamic_pointer_cast(splitter.splitter); + + if (multibandSplitter != nullptr) { + if (index < SplitterMutators::getNumBands(multibandSplitter) - 1) { + return SplitterMutators::getCrossoverFrequency(multibandSplitter, index); + } + } + + return 0.0f; + } + + bool setChainCustomName(StateManager& manager, int chainNumber, const juce::String& name) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr splitter = cloneSplitterState(manager); + + if (splitter == nullptr || splitter->splitter == nullptr) { + return false; + } + + if (SplitterMutators::setChainCustomName(splitter->splitter, chainNumber, name)) { + pushSplitter(manager, splitter, "set chain " + juce::String(chainNumber + 1) + " name"); + return true; + } + + return false; + } + + juce::String getChainCustomName(StateManager& manager, int chainNumber) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getChainCustomName(splitter.splitter, chainNumber); + } + + return ""; + } + + std::pair, float> getFFTOutputs(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + auto multibandSplitter = std::dynamic_pointer_cast(splitter.splitter); + + if (multibandSplitter != nullptr) { + std::array bins; + const float* outputs = multibandSplitter->fftProvider.getOutputs(); + + std::copy(outputs, outputs + FFTProvider::NUM_OUTPUTS, bins.begin()); + + return std::make_pair(bins, multibandSplitter->fftProvider.getBinWidth()); + } + + std::array bins; + std::fill(bins.begin(), bins.end(), 0.0f); + + return std::make_pair(bins, 0); + } + + std::shared_ptr getPluginEditorBounds(StateManager& manager, int chainNumber, int positionInChain) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + return SplitterMutators::getPluginEditorBounds(splitter.splitter, chainNumber, positionInChain); + } + + return nullptr; + } + + void forEachChain(StateManager& manager, std::function)> callback) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + if (splitter.splitter != nullptr) { + for (int chainNumber {0}; chainNumber < splitter.splitter->chains.size(); chainNumber++) { + callback(chainNumber, splitter.splitter->chains[chainNumber].chain); + } + } + } + + void forEachCrossover(StateManager& manager, std::function callback) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + auto multibandSplitter = std::dynamic_pointer_cast(splitter.splitter); + + if (multibandSplitter != nullptr) { + for (size_t crossoverNumber {0}; crossoverNumber < SplitterMutators::getNumBands(multibandSplitter) - 1; crossoverNumber++) { + callback(SplitterMutators::getCrossoverFrequency(multibandSplitter, crossoverNumber)); + } + } + } + + void writeSplitterToXml(StateManager& manager, juce::XmlElement* element) { + std::scoped_lock lock(manager.mutatorsMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + XmlWriter::write(splitter.splitter, element); + + // Store the cached crossover frequencies + if (splitter.cachedcrossoverFrequencies.has_value()) { + juce::XmlElement* frequenciesElement = element->createNewChildElement(XML_CACHED_CROSSOVER_FREQUENCIES_STR); + for (int index {0}; index < splitter.cachedcrossoverFrequencies.value().size(); index++) { + frequenciesElement->setAttribute(getCachedCrossoverFreqXMLName(index), splitter.cachedcrossoverFrequencies.value()[index]); + } + } + } + + void restoreSplitterFromXml(StateManager& manager, + juce::XmlElement* element, + std::function getModulationValueCallback, + std::function latencyChangeCallback, + HostConfiguration config, + const PluginConfigurator& pluginConfigurator, + juce::Array availableTypes, + std::function onErrorCallback) { + std::scoped_lock lock(manager.mutatorsMutex); + WECore::AudioSpinLock sharedLock(manager.sharedMutex); + SplitterState& splitter = manager.getSplitterStateUnsafe(); + + // Restore the cached crossover frequencies first, we need to allow for them to be overwritten + // by the later call to setSplitType() in the case that we're restoring a multiband split + juce::XmlElement* frequenciesElement = element->getChildByName(XML_CACHED_CROSSOVER_FREQUENCIES_STR); + if (frequenciesElement != nullptr) { + splitter.cachedcrossoverFrequencies = std::vector(); + const int numFrequencies {frequenciesElement->getNumAttributes()}; + for (int index {0}; index < numFrequencies; index++) { + if (frequenciesElement->hasAttribute(getCachedCrossoverFreqXMLName(index))) { + splitter.cachedcrossoverFrequencies.value().push_back( + frequenciesElement->getDoubleAttribute(getCachedCrossoverFreqXMLName(index))); + } + } + } + + splitter.splitter = XmlReader::restoreSplitterFromXml( + element, + getModulationValueCallback, + latencyChangeCallback, + config, + pluginConfigurator, + availableTypes, + onErrorCallback); + + // Make sure prepareToPlay has been called on the splitter as we don't actually know if the host + // will call it via the PluginProcessor + if (splitter.splitter != nullptr) { + SplitterProcessors::prepareToPlay(*splitter.splitter.get(), config.sampleRate, config.blockSize, config.layout); + } + } + + void createDefaultSources(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + WECore::AudioSpinLock sharedLock(manager.sharedMutex); + + // No undo/redo needed here + ModulationMutators::addLfo(manager.getSourcesStateUnsafe()); + ModulationMutators::addEnvelope(manager.getSourcesStateUnsafe()); + ModulationMutators::addRandom(manager.getSourcesStateUnsafe()); + } + + void addLfo(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + ModulationMutators::addLfo(sources); + pushSources(manager, sources, "add LFO"); + } + + void addEnvelope(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + ModulationMutators::addEnvelope(sources); + pushSources(manager, sources, "add ENV"); + } + + void addRandom(StateManager& manager) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + ModulationMutators::addRandom(sources); + pushSources(manager, sources, "add RND"); + } + + void removeModulationSource(StateManager& manager, ModulationSourceDefinition definition) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr newState = std::make_shared( + cloneSplitterState(manager), cloneSourcesState(manager), "remove modulation source"); + + if (newState->splitterState == nullptr || + newState->splitterState->splitter == nullptr || + newState->modulationSourcesState == nullptr) { + return; + } + + if (!ModulationMutators::removeModulationSource(*newState->modulationSourcesState, definition)) { + return; + } + + // Iterate through each plugin, remove the source if it has been assigned and renumber ones that + // are numbered higher + for (PluginChainWrapper& chain : newState->splitterState->splitter->chains) { + for (int slotIndex {0}; slotIndex < ChainMutators::getNumSlots(chain.chain); slotIndex++) { + PluginModulationConfig thisPluginConfig = ChainMutators::getPluginModulationConfig(chain.chain, slotIndex); + + // Iterate through each configured parameter + for (std::shared_ptr parameterConfig : thisPluginConfig.parameterConfigs) { + parameterConfig->sources = deleteSourceFromTargetSources(parameterConfig->sources, definition); + } + + ChainMutators::setPluginModulationConfig(chain.chain, thisPluginConfig, slotIndex); + } + } + + pushState(manager, newState); + } + + void forEachLfo(StateManager& manager, std::function callback) { + std::scoped_lock lock(manager.mutatorsMutex); + ModulationSourcesState& state = *manager.getSourcesStateUnsafe(); + + for (int lfoIndex {0}; lfoIndex < state.lfos.size(); lfoIndex++) { + callback(lfoIndex + 1); + } + } + + void forEachEnvelope(StateManager& manager, std::function callback) { + std::scoped_lock lock(manager.mutatorsMutex); + ModulationSourcesState& state = *manager.getSourcesStateUnsafe(); + + for (int envIndex {0}; envIndex < state.envelopes.size(); envIndex++) { + callback(envIndex + 1); + } + } + + void forEachRandom(StateManager& manager, std::function callback) { + std::scoped_lock lock(manager.mutatorsMutex); + ModulationSourcesState& state = *manager.getSourcesStateUnsafe(); + + for (int rndIndex {0}; rndIndex < state.randomSources.size(); rndIndex++) { + callback(rndIndex + 1); + } + } + + void setLfoTempoSyncSwitch(StateManager& manager, int lfoIndex, bool val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoTempoSyncSwitch(sources, lfoIndex, val)) { + pushSources(manager, sources, "set LFO " + juce::String(lfoIndex + 1) + " tempo sync"); + } + } + + void setLfoInvertSwitch(StateManager& manager, int lfoIndex, bool val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoInvertSwitch(sources, lfoIndex, val)) { + pushSources(manager, sources, "set LFO " + juce::String(lfoIndex + 1) + " invert"); + } + } + + void setLfoOutputMode(StateManager& manager, int lfoIndex, int val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoOutputMode(sources, lfoIndex, val)) { + pushSources(manager, sources, "set LFO " + juce::String(lfoIndex + 1) + " output mode"); + } + } + + void setLfoWave(StateManager& manager, int lfoIndex, int val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoWave(sources, lfoIndex, val)) { + pushSources(manager, sources, "set LFO " + juce::String(lfoIndex + 1) + " wave"); + } + } + + void setLfoTempoNumer(StateManager& manager, int lfoIndex, int val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set LFO " + juce::String(lfoIndex + 1) + " tempo numerator"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoTempoNumer(sources, lfoIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void setLfoTempoDenom(StateManager& manager, int lfoIndex, int val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set LFO " + juce::String(lfoIndex + 1) + " tempo denominator"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoTempoDenom(sources, lfoIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void setLfoFreq(StateManager& manager, int lfoIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set LFO " + juce::String(lfoIndex + 1) + " frequency"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoFreq(sources, lfoIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void setLfoDepth(StateManager& manager, int lfoIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set LFO " + juce::String(lfoIndex + 1) + " depth"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoDepth(sources, lfoIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void setLfoManualPhase(StateManager& manager, int lfoIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set LFO " + juce::String(lfoIndex + 1) + " phase"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLfoManualPhase(sources, lfoIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void addSourceToLFOFreq(StateManager& manager, int lfoIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::addSourceToLFOFreq(sources, lfoIndex, source)) { + pushSources(manager, sources, "add modulation source to LFO " + juce::String(lfoIndex + 1) + " rate"); + } + } + + void removeSourceFromLFOFreq(StateManager& manager, int lfoIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::removeSourceFromLFOFreq(sources, lfoIndex, source)) { + pushSources(manager, sources, "remove modulation source from LFO " + juce::String(lfoIndex + 1) + " rate"); + } + } + + void setLFOFreqModulationAmount(StateManager& manager, int lfoIndex, int sourceIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set LFO " + juce::String(lfoIndex + 1) + " rate modulation amount"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLFOFreqModulationAmount(sources, lfoIndex, sourceIndex, val)) { + pushSources(manager, sources, operation); + } + } + + std::vector> getLFOFreqModulationSources(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLFOFreqModulationSources(manager.getSourcesStateUnsafe(), lfoIndex); + } + + void addSourceToLFODepth(StateManager& manager, int lfoIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::addSourceToLFODepth(sources, lfoIndex, source)) { + pushSources(manager, sources, "add modulation source to LFO " + juce::String(lfoIndex + 1) + " depth"); + } + } + + void removeSourceFromLFODepth(StateManager& manager, int lfoIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::removeSourceFromLFODepth(sources, lfoIndex, source)) { + pushSources(manager, sources, "remove modulation source from LFO " + juce::String(lfoIndex + 1) + " depth"); + } + } + + void setLFODepthModulationAmount(StateManager& manager, int lfoIndex, int sourceIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set LFO " + juce::String(lfoIndex + 1) + " depth modulation amount"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLFODepthModulationAmount(sources, lfoIndex, sourceIndex, val)) { + pushSources(manager, sources, operation); + } + } + + std::vector> getLFODepthModulationSources(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLFODepthModulationSources(manager.getSourcesStateUnsafe(), lfoIndex); + } + + void addSourceToLFOPhase(StateManager& manager, int lfoIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::addSourceToLFOPhase(sources, lfoIndex, source)) { + pushSources(manager, sources, "add modulation source to LFO " + juce::String(lfoIndex + 1) + " phase"); + } + } + + void removeSourceFromLFOPhase(StateManager& manager, int lfoIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::removeSourceFromLFOPhase(sources, lfoIndex, source)) { + pushSources(manager, sources, "remove modulation source from LFO " + juce::String(lfoIndex + 1) + " phase"); + } + } + + void setLFOPhaseModulationAmount(StateManager& manager, int lfoIndex, int sourceIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set LFO " + juce::String(lfoIndex + 1) + " phase modulation amount"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setLFOPhaseModulationAmount(sources, lfoIndex, sourceIndex, val)) { + pushSources(manager, sources, operation); + } + } + + std::vector> getLFOPhaseModulationSources(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLFOPhaseModulationSources(manager.getSourcesStateUnsafe(), lfoIndex); + } + + bool getLfoTempoSyncSwitch(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoTempoSyncSwitch(manager.getSourcesStateUnsafe(), lfoIndex); + } + + bool getLfoInvertSwitch(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoInvertSwitch(manager.getSourcesStateUnsafe(), lfoIndex); + } + + int getLfoOutputMode(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoOutputMode(manager.getSourcesStateUnsafe(), lfoIndex); + } + + int getLfoWave(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoWave(manager.getSourcesStateUnsafe(), lfoIndex); + } + + double getLfoTempoNumer(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoTempoNumer(manager.getSourcesStateUnsafe(), lfoIndex); + } + + double getLfoTempoDenom(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoTempoDenom(manager.getSourcesStateUnsafe(), lfoIndex); + } + + double getLfoFreq(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoFreq(manager.getSourcesStateUnsafe(), lfoIndex); + } + + double getLFOModulatedFreqValue(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLFOModulatedFreqValue(manager.getSourcesStateUnsafe(), lfoIndex); + } + + double getLfoDepth(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoDepth(manager.getSourcesStateUnsafe(), lfoIndex); + } + + double getLFOModulatedDepthValue(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLFOModulatedDepthValue(manager.getSourcesStateUnsafe(), lfoIndex); + } + + double getLfoManualPhase(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLfoManualPhase(manager.getSourcesStateUnsafe(), lfoIndex); + } + + double getLFOModulatedPhaseValue(StateManager& manager, int lfoIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getLFOModulatedPhaseValue(manager.getSourcesStateUnsafe(), lfoIndex); + } + + void setEnvAttackTimeMs(StateManager& manager, int envIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set ENV " + juce::String(envIndex + 1) + " attack time"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setEnvAttackTimeMs(sources, envIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void setEnvReleaseTimeMs(StateManager& manager, int envIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set ENV " + juce::String(envIndex + 1) + " release time"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setEnvReleaseTimeMs(sources, envIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void setEnvFilterEnabled(StateManager& manager, int envIndex, bool val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setEnvFilterEnabled(sources, envIndex, val)) { + pushSources(manager, sources, "set ENV " + juce::String(envIndex + 1) + " filter enabled"); + } + } + + void setEnvFilterHz(StateManager& manager, int envIndex, double lowCut, double highCut) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set ENV " + juce::String(envIndex + 1) + " filter cutoff"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setEnvFilterHz(sources, envIndex, lowCut, highCut)) { + pushSources(manager, sources, operation); + } + } + + void setEnvAmount(StateManager& manager, int envIndex, float val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set ENV " + juce::String(envIndex + 1) + " amount"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setEnvAmount(sources, envIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void setEnvUseSidechainInput(StateManager& manager, int envIndex, bool val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setEnvUseSidechainInput(sources, envIndex, val)) { + pushSources(manager, sources, "set ENV " + juce::String(envIndex + 1) + " sidechain input"); + } + } + + double getEnvAttackTimeMs(StateManager& manager, int envIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getEnvAttackTimeMs(manager.getSourcesStateUnsafe(), envIndex); + } + + double getEnvReleaseTimeMs(StateManager& manager, int envIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getEnvReleaseTimeMs(manager.getSourcesStateUnsafe(), envIndex); + } + + bool getEnvFilterEnabled(StateManager& manager, int envIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getEnvFilterEnabled(manager.getSourcesStateUnsafe(), envIndex); + } + + double getEnvLowCutHz(StateManager& manager, int envIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getEnvLowCutHz(manager.getSourcesStateUnsafe(), envIndex); + } + + double getEnvHighCutHz(StateManager& manager, int envIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getEnvHighCutHz(manager.getSourcesStateUnsafe(), envIndex); + } + + float getEnvAmount(StateManager& manager, int envIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getEnvAmount(manager.getSourcesStateUnsafe(), envIndex); + } + + bool getEnvUseSidechainInput(StateManager& manager, int envIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getEnvUseSidechainInput(manager.getSourcesStateUnsafe(), envIndex); + } + + double getEnvLastOutput(StateManager& manager, int envIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getEnvLastOutput(manager.getSourcesStateUnsafe(), envIndex); + } + + void setRandomOutputMode(StateManager& manager, int randomIndex, int val) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setRandomOutputMode(sources, randomIndex, val)) { + pushSources(manager, sources, "set RND " + juce::String(randomIndex + 1) + " output mode"); + } + } + + void setRandomFreq(StateManager& manager, int randomIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set RND " + juce::String(randomIndex + 1) + " frequency"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setRandomFreq(sources, randomIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void setRandomDepth(StateManager& manager, int randomIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set RND " + juce::String(randomIndex + 1) + " depth"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setRandomDepth(sources, randomIndex, val)) { + pushSources(manager, sources, operation); + } + } + + void addSourceToRandomFreq(StateManager& manager, int randomIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::addSourceToRandomFreq(sources, randomIndex, source)) { + pushSources(manager, sources, "add modulation source to RND " + juce::String(randomIndex + 1) + " frequency"); + } + } + + void removeSourceFromRandomFreq(StateManager& manager, int randomIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::removeSourceFromRandomFreq(sources, randomIndex, source)) { + pushSources(manager, sources, "remove modulation source from RND " + juce::String(randomIndex + 1) + " frequency"); + } + } + + void setRandomFreqModulationAmount(StateManager& manager, int randomIndex, int sourceIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set RND " + juce::String(randomIndex + 1) + " frequency modulation amount"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setRandomFreqModulationAmount(sources, randomIndex, sourceIndex, val)) { + pushSources(manager, sources, operation); + } + } + + std::vector> getRandomFreqModulationSources(StateManager& manager, int randomIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getRandomFreqModulationSources(manager.getSourcesStateUnsafe(), randomIndex); + } + + void addSourceToRandomDepth(StateManager& manager, int randomIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::addSourceToRandomDepth(sources, randomIndex, source)) { + pushSources(manager, sources, "add modulation source to RND " + juce::String(randomIndex + 1) + " depth"); + } + } + + void removeSourceFromRandomDepth(StateManager& manager, int randomIndex, ModulationSourceDefinition source) { + std::scoped_lock lock(manager.mutatorsMutex); + std::shared_ptr sources = cloneSourcesState(manager); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::removeSourceFromRandomDepth(sources, randomIndex, source)) { + pushSources(manager, sources, "remove modulation source from RND " + juce::String(randomIndex + 1) + " depth"); + } + } + + void setRandomDepthModulationAmount(StateManager& manager, int randomIndex, int sourceIndex, double val) { + std::scoped_lock lock(manager.mutatorsMutex); + + const juce::String operation = "set RND " + juce::String(randomIndex + 1) + " depth modulation amount"; + std::shared_ptr sources = cloneSourcesStateIfNeeded(manager, operation); + + if (sources == nullptr) { + return; + } + + if (ModulationMutators::setRandomDepthModulationAmount(sources, randomIndex, sourceIndex, val)) { + pushSources(manager, sources, operation); + } + } + + std::vector> getRandomDepthModulationSources(StateManager& manager, int randomIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getRandomDepthModulationSources(manager.getSourcesStateUnsafe(), randomIndex); + } + + int getRandomOutputMode(StateManager& manager, int randomIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getRandomOutputMode(manager.getSourcesStateUnsafe(), randomIndex); + } + + double getRandomFreq(StateManager& manager, int randomIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getRandomFreq(manager.getSourcesStateUnsafe(), randomIndex); + } + + double getRandomModulatedFreqValue(StateManager& manager, int randomIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getRandomModulatedFreqValue(manager.getSourcesStateUnsafe(), randomIndex); + } + + double getRandomDepth(StateManager& manager, int randomIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getRandomDepth(manager.getSourcesStateUnsafe(), randomIndex); + } + + double getRandomModulatedDepthValue(StateManager& manager, int randomIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getRandomModulatedDepthValue(manager.getSourcesStateUnsafe(), randomIndex); + } + + double getRandomLastOutput(StateManager& manager, int randomIndex) { + std::scoped_lock lock(manager.mutatorsMutex); + return ModulationMutators::getRandomLastOutput(manager.getSourcesStateUnsafe(), randomIndex); + } + + void resetAllState(StateManager& manager, + HostConfiguration config, + std::function getModulationValueCallback, + std::function latencyChangeCallback) { + std::scoped_lock lock(manager.mutatorsMutex); + + auto splitterState = std::make_shared(config, getModulationValueCallback, latencyChangeCallback); + auto sourcesState = std::make_shared(getModulationValueCallback); + + pushState(manager, std::make_shared(splitterState, sourcesState, "reset all")); + } + + void writeSourcesToXml(StateManager& manager, juce::XmlElement* element) { + std::scoped_lock lock(manager.mutatorsMutex); + XmlWriter::write(*manager.getSourcesStateUnsafe(), element); + } + + void restoreSourcesFromXml(StateManager& manager, juce::XmlElement* element, HostConfiguration config) { + std::scoped_lock lock(manager.mutatorsMutex); + WECore::AudioSpinLock sharedLock(manager.sharedMutex); + + XmlReader::restoreModulationSourcesFromXml(*manager.getSourcesStateUnsafe(), element, config); + } + + void undo(StateManager& manager, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout) { + std::scoped_lock lock(manager.mutatorsMutex); + WECore::AudioSpinLock sharedLock(manager.sharedMutex); + + if (manager.undoHistory.size() <= 1) { + return; + } + + manager.redoHistory.push_back(manager.undoHistory.back()); + manager.undoHistory.pop_back(); + + // Enable the latency notifications from the current state + manager.undoHistory.back()->splitterState->splitter->shouldNotifyProcessorOnLatencyChange = true; + manager.redoHistory.back()->splitterState->splitter->shouldNotifyProcessorOnLatencyChange = false; + + SplitterProcessors::prepareToPlay(*(manager.undoHistory.back()->splitterState->splitter), sampleRate, samplesPerBlock, layout); + } + + void redo(StateManager& manager, double sampleRate, int samplesPerBlock, juce::AudioProcessor::BusesLayout layout) { + std::scoped_lock lock(manager.mutatorsMutex); + WECore::AudioSpinLock sharedLock(manager.sharedMutex); + + if (manager.redoHistory.size() == 0) { + return; + } + + manager.undoHistory.push_back(manager.redoHistory.back()); + manager.redoHistory.pop_back(); + + // Enable the latency notifications from the current state + manager.undoHistory.back()->splitterState->splitter->shouldNotifyProcessorOnLatencyChange = true; + if (manager.redoHistory.size() > 0) { + manager.redoHistory.back()->splitterState->splitter->shouldNotifyProcessorOnLatencyChange = false; + } + + SplitterProcessors::prepareToPlay(*(manager.undoHistory.back()->splitterState->splitter), sampleRate, samplesPerBlock, layout); + } + + std::optional getUndoOperation(const StateManager& manager) { + if (manager.undoHistory.size() < 2) { + return {}; + } + + return manager.undoHistory.back()->operation; + } + + std::optional getRedoOperation(const StateManager& manager) { + if (manager.redoHistory.size() < 1) { + return {}; + } + + return manager.redoHistory.back()->operation; + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.hpp new file mode 100644 index 00000000..58a6ac37 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/MutatorsInterface.hpp @@ -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 plugin, int chainNumber, int positionInChain); + bool removeSlot(StateManager& manager, int chainNumber, int positionInChain); + bool insertGainStage(StateManager& manager, int chainNumber, int positionInChain); + + std::shared_ptr 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 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 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 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, float> getFFTOutputs(StateManager& manager); + + std::shared_ptr getPluginEditorBounds(StateManager& manager, int chainNumber, int positionInChain); + + void forEachChain(StateManager& manager, std::function)> callback); + void forEachCrossover(StateManager& manager, std::function callback); + + void writeSplitterToXml(StateManager& manager, juce::XmlElement* element); + void restoreSplitterFromXml( + StateManager& manager, juce::XmlElement* element, + std::function getModulationValueCallback, + std::function latencyChangeCallback, + HostConfiguration config, + const PluginConfigurator& pluginConfigurator, + juce::Array availableTypes, + std::function 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 callback); + void forEachEnvelope(StateManager& manager, std::function callback); + void forEachRandom(StateManager& manager, std::function 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> 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> 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> 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> 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> 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 getModulationValueCallback, + std::function 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 getUndoOperation(const StateManager& manager); + std::optional getRedoOperation(const StateManager& manager); +} \ No newline at end of file diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.cpp new file mode 100644 index 00000000..181feabf --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.cpp @@ -0,0 +1,474 @@ +#include "SplitterMutators.hpp" +#include "ChainMutators.hpp" +#include "ChainProcessors.hpp" +#include "MONSTRFilters/MONSTRParameters.h" + +namespace { + void copyNextSlot(std::shared_ptr splitter, std::function 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 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 splitter, std::shared_ptr 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 splitter, std::shared_ptr 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 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 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 getPlugin(std::shared_ptr 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 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 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 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 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 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(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 splitter, int chainNumber) { + if (auto multibandSplitter = std::dynamic_pointer_cast(splitter)) { + return CrossoverMutators::getIsSoloed(multibandSplitter->crossover, chainNumber); + } + + if (chainNumber < splitter->chains.size()) { + return splitter->chains[chainNumber].isSoloed; + } + + return false; + } + + bool moveSlot(std::shared_ptr splitter, int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber) { + // Copy everything we need + std::shared_ptr 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 splitter, + std::function sharedPlugin, juce::MemoryBlock sourceState, bool isBypassed, PluginModulationConfig sourceConfig)> insertPlugin, + std::function onSuccess, + juce::AudioPluginFormatManager& formatManager, + int fromChainNumber, + int fromSlotNumber, + int toChainNumber, + int toSlotNumber) { + std::shared_ptr 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 plugin, const juce::String& error) { + if (plugin != nullptr) { + // Create the shared pointer here as we need it for the window + std::shared_ptr 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 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 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(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 splitter, std::function 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 chainToCopy = splitter->chains[fromChainNumber].chain; + const bool isSoloed = splitter->chains[fromChainNumber].isSoloed; + + if (auto multibandSplitter = std::dynamic_pointer_cast(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 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 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 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 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 splitter, int chainNumber, int positionInChain) { + if (chainNumber < splitter->chains.size()) { + return ChainMutators::getPan(splitter->chains[chainNumber].chain, positionInChain); + } + + return 0.0f; + } + + std::shared_ptr getPluginEditorBounds(std::shared_ptr splitter, int chainNumber, int positionInChain) { + if (chainNumber < splitter->chains.size()) { + return ChainMutators::getPluginEditorBounds(splitter->chains[chainNumber].chain, positionInChain); + } + + return std::make_shared(); + } + + SPLIT_TYPE getSplitType(const std::shared_ptr splitter) { + if (std::dynamic_pointer_cast(splitter)) { + return SPLIT_TYPE::SERIES; + } + + if (std::dynamic_pointer_cast(splitter)) { + return SPLIT_TYPE::PARALLEL; + } + + if (std::dynamic_pointer_cast(splitter)) { + return SPLIT_TYPE::MULTIBAND; + } + + if (std::dynamic_pointer_cast(splitter)) { + return SPLIT_TYPE::LEFTRIGHT; + } + + if (std::dynamic_pointer_cast(splitter)) { + return SPLIT_TYPE::MIDSIDE; + } + + return SPLIT_TYPE::SERIES; + } + + void addChain(std::shared_ptr splitter) { + splitter->chains.emplace_back(std::make_shared(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 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 splitter) { + // Create the chain first, then add the band and set the processor + splitter->chains.emplace_back(std::make_unique(splitter->getModulationValueCallback), false); + CrossoverMutators::addBand(splitter->crossover); + + std::shared_ptr 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 splitter) { + return CrossoverMutators::getNumBands(splitter->crossover); + } + + bool removeBand(std::shared_ptr 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 splitter, size_t index, double val) { + return CrossoverMutators::setCrossoverFrequency(splitter->crossover, index, val); + } + + double getCrossoverFrequency(std::shared_ptr splitter, size_t index) { + return CrossoverMutators::getCrossoverFrequency(splitter->crossover, index); + } + + bool setChainCustomName(std::shared_ptr 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 splitter, int chainNumber) { + if (chainNumber >= splitter->chains.size()) { + return juce::String(); + } + + return splitter->chains[chainNumber].chain->customName; + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.hpp new file mode 100644 index 00000000..4d0c2be6 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include "PluginSplitter.hpp" +#include "SplitTypes.hpp" + +namespace SplitterMutators { + // PluginSplitter + bool insertPlugin(std::shared_ptr splitter, std::shared_ptr plugin, int chainNumber, int positionInChain); + bool replacePlugin(std::shared_ptr splitter, std::shared_ptr plugin, int chainNumber, int positionInChain); + bool removeSlot(std::shared_ptr splitter, int chainNumber, int positionInChain); + bool insertGainStage(std::shared_ptr splitter, int chainNumber, int positionInChain); + + std::shared_ptr getPlugin(std::shared_ptr splitter, int chainNumber, int positionInChain); + + bool setPluginModulationConfig(std::shared_ptr splitter, PluginModulationConfig config, int chainNumber, int positionInChain); + PluginModulationConfig getPluginModulationConfig(std::shared_ptr splitter, int chainNumber, int positionInChain); + + inline size_t getNumChains(std::shared_ptr splitter) { return splitter->chains.size(); } + + bool setSlotBypass(std::shared_ptr splitter, int chainNumber, int positionInChain, bool isBypassed); + bool getSlotBypass(std::shared_ptr splitter, int chainNumber, int positionInChain); + + bool setChainSolo(std::shared_ptr splitter, int chainNumber, bool val); + bool getChainSolo(std::shared_ptr splitter, int chainNumber); + + bool moveSlot(std::shared_ptr splitter, int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber); + void copySlot(std::shared_ptr splitter, + std::function sharedPlugin, juce::MemoryBlock sourceState, bool isBypassed, PluginModulationConfig sourceConfig)> insertPlugin, + std::function onSuccess, + juce::AudioPluginFormatManager& formatManager, + int fromChainNumber, + int fromSlotNumber, + int toChainNumber, + int toSlotNumber); + + bool moveChain(std::shared_ptr splitter, int fromChainNumber, int toChainNumber); + void copyChain(std::shared_ptr splitter, std::function onSuccess, juce::AudioPluginFormatManager& formatManager, int fromChainNumber, int toChainNumber); + + bool setGainLinear(std::shared_ptr splitter, int chainNumber, int positionInChain, float gain); + float getGainLinear(std::shared_ptr splitter, int chainNumber, int positionInChain); + float getGainStageOutputAmplitude(std::shared_ptr splitter, int chainNumber, int positionInChain, int channelNumber); + + bool setPan(std::shared_ptr splitter, int chainNumber, int positionInChain, float pan); + float getPan(std::shared_ptr splitter, int chainNumber, int positionInChain); + + std::shared_ptr getPluginEditorBounds(std::shared_ptr splitter, int chainNumber, int positionInChain); + + SPLIT_TYPE getSplitType(const std::shared_ptr splitter); + + // PluginSplitterParallel + void addChain(std::shared_ptr splitter); + bool removeChain(std::shared_ptr splitter, int chainNumber); + + // PluginSplitterMultiband + void addBand(std::shared_ptr splitter); + bool removeBand(std::shared_ptr splitter, int bandNumber); + size_t getNumBands(std::shared_ptr splitter); + bool setCrossoverFrequency(std::shared_ptr splitter, size_t index, double val); + double getCrossoverFrequency(std::shared_ptr splitter, size_t index); + + bool setChainCustomName(std::shared_ptr splitter, int chainNumber, const juce::String& name); + juce::String getChainCustomName(std::shared_ptr splitter, int chainNumber); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators_test.cpp new file mode 100644 index 00000000..00799ffe --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/SplitterMutators_test.cpp @@ -0,0 +1,1300 @@ +#include "catch.hpp" +#include "TestUtils.hpp" +#include "SplitterMutators.hpp" + +namespace { + constexpr int SAMPLE_RATE {44100}; + constexpr int NUM_SAMPLES {64}; + + class MutatorTestPluginInstance : public TestUtils::TestPluginInstance { + public: + MutatorTestPluginInstance() = default; + }; +} + +SCENARIO("SplitterMutators: Chains and slots can be added, replaced, and removed") { + auto messageManager = juce::MessageManager::getInstance(); + + GIVEN("An empty splitter") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + return 0.0f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + auto splitterSeries = std::make_shared(config, modulationCallback, latencyCallback); + + REQUIRE(splitterSeries->chains[0].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 splitter is modified") { + // WHEN("A plugin is added") + { + auto plugin = std::make_shared(); + plugin->setLatencySamples(10); + + auto splitter = std::dynamic_pointer_cast(splitterSeries); + bool isSuccess = SplitterMutators::insertPlugin(splitter, plugin, 0, 0); + + // THEN("The splitter contains a single chain with a single plugin") + CHECK(isSuccess); + CHECK(splitterSeries->chains.size() == 1); + CHECK(splitterSeries->chains[0].chain->chain.size() == 1); + CHECK(SplitterMutators::getPlugin(splitter, 0, 0) == plugin); + CHECK(latencyCalled); + CHECK(receivedLatency == 10); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A plugin is added at chainNumber > chains.size()") + { + auto plugin = std::make_shared(); + plugin->setLatencySamples(10); + + auto splitter = std::dynamic_pointer_cast(splitterSeries); + bool isSuccess = SplitterMutators::insertPlugin(splitter, plugin, 10, 0); + + // THEN("Nothing changes") + CHECK(!isSuccess); + CHECK(splitterSeries->chains.size() == 1); + CHECK(splitterSeries->chains[0].chain->chain.size() == 1); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A gain stage is added") + { + auto splitter = std::dynamic_pointer_cast(splitterSeries); + bool isSuccess = SplitterMutators::insertGainStage(splitter, 0, 1); + + // THEN("The splitter contains a single chain with a plugin and gain stage") + CHECK(isSuccess); + CHECK(splitterSeries->chains.size() == 1); + CHECK(splitterSeries->chains[0].chain->chain.size() == 2); + CHECK(SplitterMutators::getPlugin(splitter, 0, 1) == nullptr); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A gain stage is added at chainNumber > chains.size()") + { + auto splitter = std::dynamic_pointer_cast(splitterSeries); + bool isSuccess = SplitterMutators::insertGainStage(splitter, 10, 0); + + // THEN("Nothing changes") + CHECK(!isSuccess); + CHECK(splitterSeries->chains.size() == 1); + CHECK(splitterSeries->chains[0].chain->chain.size() == 2); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A two chains are added") + auto splitterParallel = std::make_shared(std::dynamic_pointer_cast(splitterSeries)); + splitterSeries.reset(); + { + SplitterMutators::addChain(splitterParallel); + SplitterMutators::addChain(splitterParallel); + + // THEN("The splitter contains a chain with two slots, and two empty chains") + CHECK(splitterParallel->chains.size() == 3); + CHECK(splitterParallel->chains[0].chain->chain.size() == 2); + CHECK(splitterParallel->chains[1].chain->chain.size() == 0); + CHECK(splitterParallel->chains[2].chain->chain.size() == 0); + CHECK(latencyCalled); + CHECK(receivedLatency == 10); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A plugin is replaced in the middle chain") + { + auto plugin = std::make_shared(); + plugin->setLatencySamples(15); + + auto splitter = std::dynamic_pointer_cast(splitterParallel); + bool isSuccess = SplitterMutators::replacePlugin(splitter, plugin, 1, 0); + + // THEN("The plugin is added to the middle chain") + CHECK(isSuccess); + CHECK(splitterParallel->chains.size() == 3); + CHECK(splitterParallel->chains[0].chain->chain.size() == 2); + CHECK(splitterParallel->chains[1].chain->chain.size() == 1); + CHECK(splitterParallel->chains[2].chain->chain.size() == 0); + CHECK(latencyCalled); + CHECK(receivedLatency == 15); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A plugin changes latency") + { + auto splitter = std::dynamic_pointer_cast(splitterParallel); + auto plugin = SplitterMutators::getPlugin(splitter, 1, 0); + plugin->setLatencySamples(30); + + // Allow the latency message to be sent + messageManager->runDispatchLoopUntil(10); + + // THEN("The latency is updated") + CHECK(latencyCalled); + CHECK(receivedLatency == 30); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A gain stage is removed in the first chain") + { + auto splitter = std::dynamic_pointer_cast(splitterParallel); + bool isSuccess = SplitterMutators::removeSlot(splitter, 0, 1); + + // THEN("The first chain has only one plugin left") + CHECK(isSuccess); + CHECK(splitterParallel->chains.size() == 3); + CHECK(splitterParallel->chains[0].chain->chain.size() == 1); + CHECK(splitterParallel->chains[1].chain->chain.size() == 1); + CHECK(splitterParallel->chains[2].chain->chain.size() == 0); + CHECK(latencyCalled); + CHECK(receivedLatency == 30); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A plugin is removed in the middle chain") + { + auto splitter = std::dynamic_pointer_cast(splitterParallel); + bool isSuccess = SplitterMutators::removeSlot(splitter, 1, 0); + + // THEN("The middle chain is empty") + CHECK(isSuccess); + CHECK(splitterParallel->chains.size() == 3); + CHECK(splitterParallel->chains[0].chain->chain.size() == 1); + CHECK(splitterParallel->chains[1].chain->chain.size() == 0); + CHECK(splitterParallel->chains[2].chain->chain.size() == 0); + CHECK(latencyCalled); + CHECK(receivedLatency == 10); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A chain with a gain stage is removed") + { + auto splitter = std::dynamic_pointer_cast(splitterParallel); + bool isSuccess = SplitterMutators::insertGainStage(splitter, 1, 0); + + REQUIRE(isSuccess); + REQUIRE(splitterParallel->chains.size() == 3); + REQUIRE(splitterParallel->chains[1].chain->chain.size() == 1); + + // Reset + latencyCalled = false; + receivedLatency = 0; + + isSuccess = SplitterMutators::removeChain(splitterParallel, 1); + + // THEN("The correct chain is removed") + CHECK(isSuccess); + CHECK(splitterParallel->chains.size() == 2); + CHECK(splitterParallel->chains[0].chain->chain.size() == 1); + CHECK(splitterParallel->chains[1].chain->chain.size() == 0); + CHECK(latencyCalled); + CHECK(receivedLatency == 10); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + // WHEN("A chain is removed at chainNumber > chains.size()") + { + bool isSuccess = SplitterMutators::removeChain(splitterParallel, 10); + + // THEN("The nothing changes") + CHECK(!isSuccess); + CHECK(splitterParallel->chains.size() == 2); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + + WHEN("A too many chains are removed") + { + bool isSuccess = SplitterMutators::removeChain(splitterParallel, 1); + + REQUIRE(isSuccess); + REQUIRE(splitterParallel->chains.size() == 1); + + // Reset + latencyCalled = false; + receivedLatency = 0; + + isSuccess = SplitterMutators::removeChain(splitterParallel, 0); + + // THEN("The nothing changes") + CHECK(!isSuccess); + CHECK(splitterParallel->chains.size() == 1); + CHECK(splitterParallel->chains[0].chain->chain.size() == 1); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Reset + latencyCalled = false; + receivedLatency = 0; + } + } + } + + juce::MessageManager::deleteInstance(); +} + +SCENARIO("SplitterMutators: Slots can be moved") { + auto messageManager = juce::MessageManager::getInstance(); + + GIVEN("A splitter with a single plugin in a single chain") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + return 0.0f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + auto splitterSeries = std::make_shared(config, modulationCallback, latencyCallback); + + auto plugin = std::make_shared(); + plugin->setLatencySamples(10); + + auto splitter = std::dynamic_pointer_cast(splitterSeries); + bool isSuccess = SplitterMutators::insertPlugin(splitter, plugin, 0, 0); + + REQUIRE(isSuccess); + REQUIRE(splitterSeries->chains[0].chain->latencyListener.calculatedTotalPluginLatency == 10); + REQUIRE(splitterSeries->chains.size() == 1); + REQUIRE(splitterSeries->chains[0].chain->chain.size() == 1); + REQUIRE(SplitterMutators::getPlugin(splitter, 0, 0) == plugin); + REQUIRE(latencyCalled); + REQUIRE(receivedLatency == 10); + + // Reset + latencyCalled = false; + receivedLatency = 0; + + WHEN("The slot is moved to the same position") { + const bool success {SplitterMutators::moveSlot(splitter, 0, 0, 0, 0)}; + + THEN("Nothing changes") { + CHECK(success); + CHECK(splitterSeries->chains.size() == 1); + CHECK(splitterSeries->chains[0].chain->chain.size() == 1); + CHECK(SplitterMutators::getPlugin(splitterSeries, 0, 0) == plugin); + CHECK(splitterSeries->chains[0].isSoloed == false); + CHECK(latencyCalled); + CHECK(receivedLatency == 10); + } + } + + WHEN("The slot is moved to a position > chain.size()") { + const bool success {SplitterMutators::moveSlot(splitter, 0, 0, 0, 10)}; + + THEN("Nothing changes") { + CHECK(success); + CHECK(splitterSeries->chains.size() == 1); + CHECK(splitterSeries->chains[0].chain->chain.size() == 1); + CHECK(SplitterMutators::getPlugin(splitterSeries, 0, 0) == plugin); + CHECK(splitterSeries->chains[0].isSoloed == false); + CHECK(latencyCalled); + CHECK(receivedLatency == 10); + } + } + } + + GIVEN("A parallel splitter with a two chains") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + return 0.0f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + auto splitterParallel = std::make_shared(config, modulationCallback, latencyCallback); + auto splitter = std::dynamic_pointer_cast(splitterParallel); + + // Add one more chain/band + SplitterMutators::addChain(splitterParallel); + + // Add three plugins and a gain stage to each chain + std::vector>> plugins; + for (int chainNumber {0}; chainNumber < splitterParallel->chains.size(); chainNumber++) { + plugins.push_back(std::vector>()); + int totalLatency {0}; + + for (int pluginNumber {0}; pluginNumber < 3; pluginNumber++) { + auto thisPlugin = std::make_shared(); + const int latency {10 * (pluginNumber + 1)}; + totalLatency += latency; + thisPlugin->setLatencySamples(latency); + plugins[chainNumber].push_back(thisPlugin); + + bool isSuccess = SplitterMutators::insertPlugin(splitter, thisPlugin, chainNumber, pluginNumber); + REQUIRE(splitterParallel->chains[chainNumber].chain->latencyListener.calculatedTotalPluginLatency == totalLatency); + REQUIRE(splitterParallel->chains[chainNumber].chain->chain.size() == pluginNumber + 1); + REQUIRE(SplitterMutators::getPlugin(splitter, chainNumber, pluginNumber) == thisPlugin); + REQUIRE(latencyCalled); + REQUIRE(receivedLatency == (chainNumber == 0 ? totalLatency : 60)); + } + + bool isSuccess = SplitterMutators::insertGainStage(splitter, chainNumber, 3); + REQUIRE(splitterParallel->chains[chainNumber].chain->chain.size() == 4); + REQUIRE(SplitterMutators::getPlugin(splitter, chainNumber, 3) == nullptr); + } + + REQUIRE(splitterParallel->chains.size() == 2); + + // Reset + latencyCalled = false; + receivedLatency = 0; + + WHEN("The middle slot is moved to the same position") { + PluginModulationConfig config; + config.isActive = true; + + auto paramConfig = std::make_shared(); + paramConfig->targetParameterName = "testParam"; + config.parameterConfigs.push_back(paramConfig); + + SplitterMutators::setSlotBypass(splitter, 0, 1, true); + SplitterMutators::setPluginModulationConfig(splitter, config, 0, 1); + const bool success {SplitterMutators::moveSlot(splitter, 0, 1, 0, 1)}; + + THEN("Nothing changes") { + CHECK(success); + REQUIRE(splitterParallel->chains.size() == 2); + + for (int chainNumber {0}; chainNumber < 2; chainNumber++) { + CHECK(splitterParallel->chains[chainNumber].chain->chain.size() == 4); + } + + CHECK(latencyCalled); + CHECK(receivedLatency == 60); + + // Check bypass + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 0) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 1) == true); + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 2) == false); + + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 0) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 1) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 2) == false); + + // Check the slots + CHECK(SplitterMutators::getPlugin(splitter, 0, 0) == plugins[0][0]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 1) == plugins[0][1]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 2) == plugins[0][2]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 3) == nullptr); + + CHECK(SplitterMutators::getPlugin(splitter, 1, 0) == plugins[1][0]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 1) == plugins[1][1]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 2) == plugins[1][2]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 3) == nullptr); + + // Check the modulation config + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 0).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 1).isActive == true); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 1).parameterConfigs[0]->targetParameterName == "testParam"); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 2).isActive == false); + + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 0).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 1).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 2).isActive == false); + } + } + + WHEN("A slot is moved from a lower to the next position up in the same chain") { + PluginModulationConfig config; + config.isActive = true; + + auto paramConfig = std::make_shared(); + paramConfig->targetParameterName = "testParam"; + config.parameterConfigs.push_back(paramConfig); + + SplitterMutators::setSlotBypass(splitter, 0, 1, true); + SplitterMutators::setPluginModulationConfig(splitter, config, 0, 1); + const bool success {SplitterMutators::moveSlot(splitter, 0, 1, 0, 2)}; + + THEN("Nothing changes") { // Not immediately obvious, but this is correct as the chain is already in the right place + CHECK(success); + REQUIRE(splitterParallel->chains.size() == 2); + + for (int chainNumber {0}; chainNumber < 2; chainNumber++) { + CHECK(splitterParallel->chains[chainNumber].chain->chain.size() == 4); + } + + CHECK(latencyCalled); + CHECK(receivedLatency == 60); + + // Check bypass + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 0) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 1) == true); + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 2) == false); + + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 0) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 1) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 2) == false); + + // Check the slots + CHECK(SplitterMutators::getPlugin(splitter, 0, 0) == plugins[0][0]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 1) == plugins[0][1]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 2) == plugins[0][2]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 3) == nullptr); + + CHECK(SplitterMutators::getPlugin(splitter, 1, 0) == plugins[1][0]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 1) == plugins[1][1]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 2) == plugins[1][2]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 3) == nullptr); + + // Check the modulation config + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 0).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 1).isActive == true); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 1).parameterConfigs[0]->targetParameterName == "testParam"); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 2).isActive == false); + + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 0).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 1).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 2).isActive == false); + } + } + + WHEN("A slot is moved from a lower to a higher position in the same chain") { + PluginModulationConfig config; + config.isActive = true; + + auto paramConfig = std::make_shared(); + paramConfig->targetParameterName = "testParam"; + config.parameterConfigs.push_back(paramConfig); + + SplitterMutators::setSlotBypass(splitter, 0, 0, true); + SplitterMutators::setPluginModulationConfig(splitter, config, 0, 0); + const bool success {SplitterMutators::moveSlot(splitter, 0, 0, 0, 2)}; + + THEN("The slot is moved to the new position") { + CHECK(success); + REQUIRE(splitterParallel->chains.size() == 2); + + for (int chainNumber {0}; chainNumber < 2; chainNumber++) { + CHECK(splitterParallel->chains[chainNumber].chain->chain.size() == 4); + } + + CHECK(latencyCalled); + CHECK(receivedLatency == 60); + + // Check bypass + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 0) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 1) == true); + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 2) == false); + + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 0) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 1) == false); + CHECK(SplitterMutators::getSlotBypass(splitter, 1, 2) == false); + + // Check the slots + CHECK(SplitterMutators::getPlugin(splitter, 0, 0) == plugins[0][1]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 1) == plugins[0][0]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 2) == plugins[0][2]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 3) == nullptr); + + CHECK(SplitterMutators::getPlugin(splitter, 1, 0) == plugins[1][0]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 1) == plugins[1][1]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 2) == plugins[1][2]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 3) == nullptr); + + // Check the modulation config + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 0).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 1).isActive == true); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 1).parameterConfigs[0]->targetParameterName == "testParam"); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 0, 2).isActive == false); + + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 0).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 1).isActive == false); + CHECK(SplitterMutators::getPluginModulationConfig(splitter, 1, 2).isActive == false); + } + } + + WHEN("A slot is moved from a higher to a lower position") { + SplitterMutators::setGainLinear(splitter, 0, 3, 0.1f); + SplitterMutators::setPan(splitter, 0, 3, 0.2f); + const bool success {SplitterMutators::moveSlot(splitter, 0, 3, 0, 0)}; + + THEN("The slot is moved to the new position") { + CHECK(success); + REQUIRE(splitterParallel->chains.size() == 2); + + for (int chainNumber {0}; chainNumber < 2; chainNumber++) { + CHECK(splitterParallel->chains[chainNumber].chain->chain.size() == 4); + } + + CHECK(latencyCalled); + CHECK(receivedLatency == 60); + + // Check gain and pan + CHECK(SplitterMutators::getGainLinear(splitter, 0, 0) == 0.1f); + CHECK(SplitterMutators::getPan(splitter, 0, 0) == 0.2f); + + // Check the slots + CHECK(SplitterMutators::getPlugin(splitter, 0, 0) == nullptr); + CHECK(SplitterMutators::getPlugin(splitter, 0, 1) == plugins[0][0]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 2) == plugins[0][1]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 3) == plugins[0][2]); + + CHECK(SplitterMutators::getPlugin(splitter, 1, 0) == plugins[1][0]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 1) == plugins[1][1]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 2) == plugins[1][2]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 3) == nullptr); + } + } + + WHEN("A slot is moved from one chain to another") { + SplitterMutators::setGainLinear(splitter, 0, 3, 0.1f); + SplitterMutators::setPan(splitter, 0, 3, 0.2f); + const bool success {SplitterMutators::moveSlot(splitter, 0, 3, 1, 0)}; + + THEN("The slot is moved to the new position") { + CHECK(success); + REQUIRE(splitterParallel->chains.size() == 2); + CHECK(splitterParallel->chains[0].chain->chain.size() == 3); + CHECK(splitterParallel->chains[1].chain->chain.size() == 5); + + CHECK(latencyCalled); + CHECK(receivedLatency == 60); + + // Check gain and pan + CHECK(SplitterMutators::getGainLinear(splitter, 1, 0) == 0.1f); + CHECK(SplitterMutators::getPan(splitter, 1, 0) == 0.2f); + + // Check the slots + CHECK(SplitterMutators::getPlugin(splitter, 0, 0) == plugins[0][0]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 1) == plugins[0][1]); + CHECK(SplitterMutators::getPlugin(splitter, 0, 2) == plugins[0][2]); + + CHECK(SplitterMutators::getPlugin(splitter, 1, 0) == nullptr); + CHECK(SplitterMutators::getPlugin(splitter, 1, 1) == plugins[1][0]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 2) == plugins[1][1]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 3) == plugins[1][2]); + CHECK(SplitterMutators::getPlugin(splitter, 1, 4) == nullptr); + } + } + } + + juce::MessageManager::deleteInstance(); +} + +SCENARIO("SplitterMutators: Chains can be moved") { + auto messageManager = juce::MessageManager::getInstance(); + + GIVEN("A splitter with a single plugin in a single chain") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + return 0.0f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + auto splitterSeries = std::make_shared(config, modulationCallback, latencyCallback); + + auto plugin = std::make_shared(); + plugin->setLatencySamples(10); + + auto splitter = std::dynamic_pointer_cast(splitterSeries); + bool isSuccess = SplitterMutators::insertPlugin(splitter, plugin, 0, 0); + + REQUIRE(isSuccess); + REQUIRE(splitterSeries->chains[0].chain->latencyListener.calculatedTotalPluginLatency == 10); + REQUIRE(splitterSeries->chains.size() == 1); + REQUIRE(splitterSeries->chains[0].chain->chain.size() == 1); + REQUIRE(SplitterMutators::getPlugin(splitter, 0, 0) == plugin); + REQUIRE(latencyCalled); + REQUIRE(receivedLatency == 10); + + // Reset + latencyCalled = false; + receivedLatency = 0; + + WHEN("The chain is moved to the same position") { + SplitterMutators::moveChain(splitter, 0, 0); + + THEN("Nothing changes") { + CHECK(splitterSeries->chains.size() == 1); + CHECK(splitterSeries->chains[0].chain->chain.size() == 1); + CHECK(SplitterMutators::getPlugin(splitterSeries, 0, 0) == plugin); + CHECK(splitterSeries->chains[0].isSoloed == false); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + } + } + + WHEN("The chain is moved to a position > chains.size()") { + SplitterMutators::moveChain(splitter, 0, 10); + + THEN("Nothing changes") { + CHECK(splitterSeries->chains.size() == 1); + CHECK(splitterSeries->chains[0].chain->chain.size() == 1); + CHECK(SplitterMutators::getPlugin(splitterSeries, 0, 0) == plugin); + CHECK(splitterSeries->chains[0].isSoloed == false); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + } + } + } + + GIVEN("A multiband splitter with a three chains") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + return 0.0f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + auto splitterMultiband = std::make_shared(config, modulationCallback, latencyCallback); + auto splitter = std::dynamic_pointer_cast(splitterMultiband); + + // Add one more chain/band + SplitterMutators::addBand(splitterMultiband); + + // Add a plugin to each chain + std::vector> plugins; + for (int pluginNumber {0}; pluginNumber < splitterMultiband->chains.size(); pluginNumber++) { + auto thisPlugin = std::make_shared(); + const int latency {10 * (pluginNumber + 1)}; + thisPlugin->setLatencySamples(latency); + plugins.push_back(thisPlugin); + + bool isSuccess = SplitterMutators::insertPlugin(splitter, thisPlugin, pluginNumber, 0); + REQUIRE(splitterMultiband->chains[pluginNumber].chain->latencyListener.calculatedTotalPluginLatency == latency); + REQUIRE(splitterMultiband->chains[pluginNumber].chain->chain.size() == 1); + REQUIRE(SplitterMutators::getPlugin(splitter, pluginNumber, 0) == thisPlugin); + REQUIRE(latencyCalled); + REQUIRE(receivedLatency == latency); + } + + REQUIRE(splitterMultiband->chains.size() == 3); + + // Reset + latencyCalled = false; + receivedLatency = 0; + + WHEN("The middle chain is moved to the same position") { + SplitterMutators::setChainSolo(splitter, 1, true); + SplitterMutators::moveChain(splitter, 1, 1); + + THEN("Nothing changes") { + CHECK(splitterMultiband->chains.size() == 3); + + for (int chainNumber {0}; chainNumber < 3; chainNumber++) { + CHECK(splitterMultiband->chains[chainNumber].chain->chain.size() == 1); + CHECK(splitterMultiband->crossover->bands[chainNumber].chain == splitterMultiband->chains[chainNumber].chain); + } + + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Check is soloed as it has to be copied with the wrapper + CHECK(splitterMultiband->chains[0].isSoloed == false); + CHECK(splitterMultiband->chains[1].isSoloed == true); + CHECK(splitterMultiband->chains[2].isSoloed == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 0) == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 1) == true); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 2) == false); + + // Check the plugins to see which chains have moved + CHECK(SplitterMutators::getPlugin(splitterMultiband, 0, 0) == plugins[0]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 1, 0) == plugins[1]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 2, 0) == plugins[2]); + } + } + + WHEN("A chain is moved from a lower to the next position up") { + SplitterMutators::setChainSolo(splitter, 0, true); + SplitterMutators::moveChain(splitter, 0, 1); + + THEN("Nothing changes") { // Not immediately obvious, but this is correct as the chain is already in the right place + CHECK(splitterMultiband->chains.size() == 3); + + for (int chainNumber {0}; chainNumber < 3; chainNumber++) { + CHECK(splitterMultiband->chains[chainNumber].chain->chain.size() == 1); + CHECK(splitterMultiband->crossover->bands[chainNumber].chain == splitterMultiband->chains[chainNumber].chain); + } + + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Check is soloed as it has to be copied with the wrapper + CHECK(splitterMultiband->chains[0].isSoloed == true); + CHECK(splitterMultiband->chains[1].isSoloed == false); + CHECK(splitterMultiband->chains[2].isSoloed == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 0) == true); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 1) == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 2) == false); + + // Check the plugins to see which chains have moved + CHECK(SplitterMutators::getPlugin(splitterMultiband, 0, 0) == plugins[0]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 1, 0) == plugins[1]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 2, 0) == plugins[2]); + } + } + + WHEN("The middle chain is moved to a position > chains.size()") { + SplitterMutators::setChainSolo(splitter, 1, true); + SplitterMutators::moveChain(splitter, 1, 10); + + THEN("The chain is moved to the end") { + CHECK(splitterMultiband->chains.size() == 3); + + for (int chainNumber {0}; chainNumber < 3; chainNumber++) { + CHECK(splitterMultiband->chains[chainNumber].chain->chain.size() == 1); + CHECK(splitterMultiband->crossover->bands[chainNumber].chain == splitterMultiband->chains[chainNumber].chain); + } + + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Check is soloed as it has to be copied with the wrapper + CHECK(splitterMultiband->chains[0].isSoloed == false); + CHECK(splitterMultiband->chains[1].isSoloed == false); + CHECK(splitterMultiband->chains[2].isSoloed == true); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 0) == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 1) == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 2) == true); + + // Check the plugins to see which chains have moved + CHECK(SplitterMutators::getPlugin(splitterMultiband, 0, 0) == plugins[0]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 1, 0) == plugins[2]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 2, 0) == plugins[1]); + } + } + + WHEN("A chain is moved from a lower to a higher position") { + SplitterMutators::setChainSolo(splitter, 0, true); + SplitterMutators::moveChain(splitter, 0, 2); + + THEN("The chain is moved to the new position") { + CHECK(splitterMultiband->chains.size() == 3); + + for (int chainNumber {0}; chainNumber < 3; chainNumber++) { + CHECK(splitterMultiband->chains[chainNumber].chain->chain.size() == 1); + CHECK(splitterMultiband->crossover->bands[chainNumber].chain == splitterMultiband->chains[chainNumber].chain); + } + + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Check is soloed as it has to be copied with the wrapper + CHECK(splitterMultiband->chains[0].isSoloed == false); + CHECK(splitterMultiband->chains[1].isSoloed == true); + CHECK(splitterMultiband->chains[2].isSoloed == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 0) == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 1) == true); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 2) == false); + + + // Check the plugins to see which chains have moved + CHECK(SplitterMutators::getPlugin(splitterMultiband, 0, 0) == plugins[1]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 1, 0) == plugins[0]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 2, 0) == plugins[2]); + } + } + + WHEN("A chain is moved from a higher to a lower position") { + SplitterMutators::setChainSolo(splitter, 2, true); + SplitterMutators::moveChain(splitter, 2, 0); + + THEN("The chain is moved to the new position") { + CHECK(splitterMultiband->chains.size() == 3); + + for (int chainNumber {0}; chainNumber < 3; chainNumber++) { + CHECK(splitterMultiband->chains[chainNumber].chain->chain.size() == 1); + CHECK(splitterMultiband->crossover->bands[chainNumber].chain == splitterMultiband->chains[chainNumber].chain); + } + + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + // Check is soloed as it has to be copied with the wrapper + CHECK(splitterMultiband->chains[0].isSoloed == true); + CHECK(splitterMultiband->chains[1].isSoloed == false); + CHECK(splitterMultiband->chains[2].isSoloed == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 0) == true); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 1) == false); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 2) == false); + + // Check the plugins to see which chains have moved + CHECK(SplitterMutators::getPlugin(splitterMultiband, 0, 0) == plugins[2]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 1, 0) == plugins[0]); + CHECK(SplitterMutators::getPlugin(splitterMultiband, 2, 0) == plugins[1]); + } + } + } + + juce::MessageManager::deleteInstance(); +} + +SCENARIO("SplitterMutators: Modulation config can be set and retrieved") { + GIVEN("A parallel splitter with two chains") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + // Return something unique we can test for later + return 1.234f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + auto splitterParallel = std::make_shared(config, modulationCallback, latencyCallback); + SplitterMutators::addChain(splitterParallel); + + auto splitter = std::dynamic_pointer_cast(splitterParallel); + SplitterMutators::insertGainStage(splitter, 0, 0); + SplitterMutators::insertPlugin(splitter, std::make_shared(), 0, 1); + SplitterMutators::insertPlugin(splitter, std::make_shared(), 1, 0); + + WHEN("The config is set for a plugin") { + PluginModulationConfig config; + config.isActive = true; + + auto paramConfig = std::make_shared(); + paramConfig->targetParameterName = "testParam"; + config.parameterConfigs.push_back(paramConfig); + + const bool success {SplitterMutators::setPluginModulationConfig(splitter, config, 1, 0)}; + + THEN("The new config can be retrieved") { + CHECK(success); + + const PluginModulationConfig retrievedConfig {SplitterMutators::getPluginModulationConfig(splitter, 1, 0)}; + 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(); + paramConfig->targetParameterName = "testParam"; + config.parameterConfigs.push_back(paramConfig); + + const bool success {SplitterMutators::setPluginModulationConfig(splitter, config, 0, 0)}; + + THEN("The configs haven't changed") { + CHECK(!success); + + for (int chainIndex {0}; chainIndex < splitter->chains.size(); chainIndex++) { + for (int slotIndex {0}; slotIndex < splitter->chains[chainIndex].chain->chain.size(); slotIndex++) { + const PluginModulationConfig retrievedConfig {SplitterMutators::getPluginModulationConfig(splitter, chainIndex, slotIndex)}; + CHECK(!retrievedConfig.isActive); + CHECK(retrievedConfig.parameterConfigs.size() == 0); + } + } + } + } + } +} + +SCENARIO("SplitterMutators: Slot parameters can be modified and retrieved") { + auto messageManager = juce::MessageManager::getInstance(); + + GIVEN("A splitter with two chains") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + // Return something unique we can test for later + return 1.234f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + const juce::String splitTypeString = GENERATE( + // We don't need series + 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) + ); + + std::shared_ptr splitter; + + if (splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR) { + auto splitterParallel = std::make_shared(config, modulationCallback, latencyCallback); + SplitterMutators::addChain(splitterParallel); + splitter = std::dynamic_pointer_cast(splitterParallel); + } else if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { + auto splitterMultiband = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterMultiband); + } else if (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR) { + auto splitterLeftRight = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterLeftRight); + } else if (splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR) { + auto splitterMidSide = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterMidSide); + } + + REQUIRE(splitter->chains.size() == 2); + REQUIRE(splitter->chains[0].chain->latencyListener.calculatedTotalPluginLatency == 0); + REQUIRE(splitter->chains[1].chain->latencyListener.calculatedTotalPluginLatency == 0); + + + SplitterMutators::insertGainStage(splitter, 0, 0); + + { + auto plugin = std::make_shared(); + plugin->setLatencySamples(20); + SplitterMutators::insertPlugin(splitter, plugin, 0, 1); + } + + { + auto plugin = std::make_shared(); + plugin->setLatencySamples(15); + SplitterMutators::insertPlugin(splitter, plugin, 1, 0); + } + + REQUIRE(receivedLatency == 20); + + // Reset + latencyCalled = false; + receivedLatency = 0; + + WHEN("A plugin is bypassed") { + const bool success {SplitterMutators::setSlotBypass(splitter, 0, 1, true)}; + + // Allow the latency message to be sent + messageManager->runDispatchLoopUntil(10); + + THEN("The plugin is bypassed correctly") { + CHECK(success); + CHECK(!SplitterMutators::getSlotBypass(splitter, 0, 0)); + CHECK(SplitterMutators::getSlotBypass(splitter, 0, 1)); + CHECK(!SplitterMutators::getSlotBypass(splitter, 1, 0)); + CHECK(latencyCalled); + CHECK(receivedLatency == 15); + } + } + + WHEN("A gain stage is bypassed") { + const bool success {SplitterMutators::setSlotBypass(splitter, 0, 0, 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(SplitterMutators::getSlotBypass(splitter, 0, 0)); + CHECK(!SplitterMutators::getSlotBypass(splitter, 0, 1)); + CHECK(!SplitterMutators::getSlotBypass(splitter, 1, 0)); + CHECK(latencyCalled); + CHECK(receivedLatency == 20); + } + } + + WHEN("An out of bounds slot is bypassed") { + const bool success {SplitterMutators::setSlotBypass(splitter, 10, 0, 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(!SplitterMutators::getSlotBypass(splitter, 0, 0)); + CHECK(!SplitterMutators::getSlotBypass(splitter, 0, 1)); + CHECK(!SplitterMutators::getSlotBypass(splitter, 1, 0)); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + } + } + + WHEN("A chain is soloed") { + const bool success {SplitterMutators::setChainSolo(splitter, 0, true)}; + + // Allow the latency message to be sent + messageManager->runDispatchLoopUntil(10); + + THEN("The chain is soloed correctly") { + CHECK(success); + CHECK(SplitterMutators::getChainSolo(splitter, 0)); + CHECK(!SplitterMutators::getChainSolo(splitter, 1)); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + + if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { + // Make sure the crossover state matches + auto splitterMultiband = std::dynamic_pointer_cast(splitter); + REQUIRE(splitterMultiband != nullptr); + CHECK(CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 0)); + CHECK(!CrossoverMutators::getIsSoloed(splitterMultiband->crossover, 1)); + } + } + } + + WHEN("An out of bounds chain is soloed") { + const bool success {SplitterMutators::setChainSolo(splitter, 10, true)}; + + // Allow the latency message to be sent + messageManager->runDispatchLoopUntil(10); + + THEN("Nothing is soloed") { + CHECK(!success); + CHECK(!SplitterMutators::getChainSolo(splitter, 0)); + CHECK(!SplitterMutators::getChainSolo(splitter, 1)); + CHECK(!latencyCalled); + CHECK(receivedLatency == 0); + } + } + + WHEN("Gain and pan is set for a plugin slot") { + const bool gainSuccess {SplitterMutators::setGainLinear(splitter, 0, 1, 0.5)}; + const bool panSuccess {SplitterMutators::setPan(splitter, 0, 1, -0.5)}; + + THEN("Nothing is changed") { + CHECK(!gainSuccess); + CHECK(!panSuccess); + CHECK(SplitterMutators::getGainLinear(splitter, 0, 1) == 0.0f); + CHECK(SplitterMutators::getPan(splitter, 0, 1) == 0.0f); + } + } + + WHEN("Gain and pan is set for a gain stage slot") { + const bool gainSuccess {SplitterMutators::setGainLinear(splitter, 0, 0, 0.5)}; + const bool panSuccess {SplitterMutators::setPan(splitter, 0, 0, -0.5)}; + + THEN("The gain stage is bypassed correctly") { + CHECK(gainSuccess); + CHECK(panSuccess); + CHECK(SplitterMutators::getGainLinear(splitter, 0, 0) == 0.5f); + CHECK(SplitterMutators::getPan(splitter, 0, 0) == -0.5f); + } + } + + WHEN("Gain and pan is set for an out of bounds chain") { + const bool gainSuccess {SplitterMutators::setGainLinear(splitter, 10, 0, 0.5)}; + const bool panSuccess {SplitterMutators::setPan(splitter, 10, 0, -0.5)}; + + THEN("Nothing is bypassed") { + CHECK(!gainSuccess); + CHECK(!panSuccess); + } + } + + if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { + // Make sure the crossover state matches + auto splitterMultiband = std::dynamic_pointer_cast(splitter); + REQUIRE(splitterMultiband != nullptr); + + WHEN("Crossover frequency is set") { + const bool success = SplitterMutators::setCrossoverFrequency(splitterMultiband, 0, 2500); + + THEN("the crossover frequency is set correctly") { + CHECK(success); + CHECK(SplitterMutators::getCrossoverFrequency(splitterMultiband, 0) == 2500); + } + } + + WHEN("Crossover frequency is set for an out of bounds crossover") { + const bool success = SplitterMutators::setCrossoverFrequency(splitterMultiband, 4, 4000); + + THEN("The default value is unchanged") { + CHECK(!success); + CHECK(SplitterMutators::getCrossoverFrequency(splitterMultiband, 0) == 1000); + } + } + } + } + + juce::MessageManager::deleteInstance(); +} + +SCENARIO("SplitterMutators: Chain custom names can be set and retrieved") { + GIVEN("A parallel splitter with two chains") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + // Return something unique we can test for later + return 1.234f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + auto splitterParallel = std::make_shared(config, modulationCallback, latencyCallback); + SplitterMutators::addChain(splitterParallel); + + auto splitter = std::dynamic_pointer_cast(splitterParallel); + + WHEN("A custom name is set for a chain") { + const bool success {SplitterMutators::setChainCustomName(splitter, 0, "Test Chain")}; + + THEN("The new name can be retrieved") { + CHECK(success); + CHECK(SplitterMutators::getChainCustomName(splitter, 0) == "Test Chain"); + CHECK(SplitterMutators::getChainCustomName(splitter, 1) == ""); + } + } + + WHEN("A custom name is set for an out of bounds chain") { + const bool success {SplitterMutators::setChainCustomName(splitter, 10, "Test Chain")}; + + THEN("The default name is unchanged") { + CHECK(!success); + CHECK(SplitterMutators::getChainCustomName(splitter, 0) == ""); + CHECK(SplitterMutators::getChainCustomName(splitter, 1) == ""); + } + } + } +} + + +SCENARIO("SplitterMutators: PluginEditorBounds can be retrieved") { + GIVEN("A parallel splitter with two chains") { + HostConfiguration config; + config.sampleRate = SAMPLE_RATE; + config.blockSize = NUM_SAMPLES; + + auto modulationCallback = [](int, MODULATION_TYPE) { + // Return something unique we can test for later + return 1.234f; + }; + + bool latencyCalled {false}; + int receivedLatency {0}; + auto latencyCallback = [&latencyCalled, &receivedLatency](int latency) { + latencyCalled = true; + receivedLatency = latency; + }; + + auto splitterParallel = std::make_shared(config, modulationCallback, latencyCallback); + SplitterMutators::addChain(splitterParallel); + REQUIRE(splitterParallel->chains[0].chain->latencyListener.calculatedTotalPluginLatency == 0); + REQUIRE(splitterParallel->chains[1].chain->latencyListener.calculatedTotalPluginLatency == 0); + + auto splitter = std::dynamic_pointer_cast(splitterParallel); + SplitterMutators::insertGainStage(splitter, 0, 0); + SplitterMutators::insertPlugin(splitter, std::make_shared(), 1, 0); + + auto& pluginBounds = std::dynamic_pointer_cast(splitter->chains[1].chain->chain[0])->editorBounds; + pluginBounds.reset(new PluginEditorBounds()); + *(pluginBounds.get()) = PluginEditorBoundsContainer( + juce::Rectangle(150, 200), + juce::Rectangle(2000, 1000)); + + WHEN("Editor bounds are retrieved for a plugin") { + auto bounds = SplitterMutators::getPluginEditorBounds(splitter, 1, 0); + + THEN("Bounds are retrieved correctly") { + CHECK(bounds->value().editorBounds == juce::Rectangle(150, 200)); + CHECK(bounds->value().displayArea == juce::Rectangle(2000, 1000)); + } + } + + WHEN("Editor bounds are retrieved for a gain stage") { + auto bounds = SplitterMutators::getPluginEditorBounds(splitter, 0, 0); + + THEN("Bounds aren't retrieved") { + CHECK(!bounds->has_value()); + } + } + + WHEN("Editor bounds are retrieved for an out of bounds slot") { + auto bounds = SplitterMutators::getPluginEditorBounds(splitter, 10, 0); + + THEN("Bounds aren't retrieved") { + CHECK(!bounds->has_value()); + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlConsts.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlConsts.hpp new file mode 100644 index 00000000..6ab5f5b3 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlConsts.hpp @@ -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; +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.cpp new file mode 100644 index 00000000..575acaa1 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.cpp @@ -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& types) { + juce::String retVal; + for (const juce::PluginDescription& type : types) { + retVal += type.pluginFormatName + " " + type.version + ", "; + } + + return retVal; + } +} + +namespace XmlReader { + std::shared_ptr restoreSplitterFromXml( + juce::XmlElement* element, + std::function getModulationValueCallback, + std::function latencyChangeCallback, + HostConfiguration configuration, + const PluginConfigurator& pluginConfigurator, + juce::Array availableTypes, + std::function 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 splitter; + if (splitType == SPLIT_TYPE::SERIES) { + splitter = std::make_shared(configuration, getModulationValueCallback, latencyChangeCallback); + } else if (splitType == SPLIT_TYPE::PARALLEL) { + splitter = std::make_shared(configuration, getModulationValueCallback, latencyChangeCallback); + } else if (splitType == SPLIT_TYPE::MULTIBAND) { + splitter = std::make_shared(configuration, getModulationValueCallback, latencyChangeCallback); + } else if (splitType == SPLIT_TYPE::LEFTRIGHT) { + splitter = std::make_shared(configuration, getModulationValueCallback, latencyChangeCallback); + } else if (splitType == SPLIT_TYPE::MIDSIDE) { + splitter = std::make_shared(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(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(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(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 restoreChainFromXml( + juce::XmlElement* element, + HostConfiguration configuration, + const PluginConfigurator& pluginConfigurator, + std::function getModulationValueCallback, + juce::Array availableTypes, + std::function onErrorCallback) { + + auto retVal = std::make_unique(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 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 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, 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 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(gain, pan, isSlotBypassed, busesLayout); + } + + std::unique_ptr restoreChainSlotPlugin( + juce::XmlElement* element, + std::function getModulationValueCallback, + HostConfiguration configuration, + const PluginConfigurator& pluginConfigurator, + LoadPluginFunction loadPlugin, + std::function 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 retVal; + + std::shared_ptr 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::fromString(boundsString), + juce::Rectangle::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 restorePluginModulationConfig(juce::XmlElement* element) { + auto retVal = std::make_unique(); + + 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 restorePluginParameterModulationConfig(juce::XmlElement* element) { + auto retVal = std::make_unique(); + + 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 thisSource = restorePluginParameterModulationSource(thisSourceElement); + retVal->sources.push_back(thisSource); + } + + return retVal; + } + + std::unique_ptr 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(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 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(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(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(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 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 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(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(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); + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.hpp new file mode 100644 index 00000000..89c2ebe8 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#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 restoreSplitterFromXml( + juce::XmlElement* element, + std::function getModulationValueCallback, + std::function latencyChangeCallback, + HostConfiguration configuration, + const PluginConfigurator& pluginConfigurator, + juce::Array availableTypes, + std::function onErrorCallback); + + std::unique_ptr restoreChainFromXml( + juce::XmlElement* element, + HostConfiguration configuration, + const PluginConfigurator& pluginConfigurator, + std::function getModulationValueCallback, + juce::Array availableTypes, + std::function onErrorCallback); + + bool XmlElementIsPlugin(juce::XmlElement* element); + bool XmlElementIsGainStage(juce::XmlElement* element); + + std::unique_ptr restoreChainSlotGainStage( + juce::XmlElement* element, const juce::AudioProcessor::BusesLayout& busesLayout); + + typedef std::function< + std::tuple, juce::String>( + const juce::PluginDescription&, const HostConfiguration&)> LoadPluginFunction; + + std::unique_ptr restoreChainSlotPlugin( + juce::XmlElement* element, + std::function getModulationValueCallback, + HostConfiguration configuration, + const PluginConfigurator& pluginConfigurator, + LoadPluginFunction loadPlugin, + std::function onErrorCallback); + std::unique_ptr restorePluginModulationConfig(juce::XmlElement* element); + std::unique_ptr restorePluginParameterModulationConfig(juce::XmlElement* element); + std::unique_ptr restorePluginParameterModulationSource(juce::XmlElement* element); + + void restoreModulationSourcesFromXml( + ModelInterface::ModulationSourcesState& state, + juce::XmlElement* element, + HostConfiguration configuration); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader_test.cpp new file mode 100644 index 00000000..bd4a5c6f --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlReader_test.cpp @@ -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(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 pluginTypes; + std::shared_ptr splitter = XmlReader::restoreSplitterFromXml( + &e, modulationCallback, latencyCallback, config, configurator, pluginTypes, errorCallback); + + THEN("A PluginSplitter with default values is created") { + CHECK(std::dynamic_pointer_cast(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(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(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(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 pluginTypes; + std::shared_ptr 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(splitter) != nullptr); + } else if (splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR) { + CHECK(std::dynamic_pointer_cast(splitter) != nullptr); + } else if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { + auto multibandSplitter = std::dynamic_pointer_cast(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(splitter) != nullptr); + } else if (splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR) { + CHECK(std::dynamic_pointer_cast(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 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(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, + HostConfiguration, + PluginConfigurator, + XmlReader::LoadPluginFunction, + std::function + >( + [](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, juce::String>( + std::make_unique(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, 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 bounds(150, 200); + e.setAttribute(XML_PLUGIN_EDITOR_BOUNDS_STR, bounds.toString()); + + const juce::Rectangle 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(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::MACRO, "macro"), + std::pair(MODULATION_TYPE::LFO, "lfo"), + std::pair(MODULATION_TYPE::ENVELOPE, "envelope"), + std::pair(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(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> 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> 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> 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> 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> depthModSources = ModulationMutators::getRandomDepthModulationSources(state, 0); + REQUIRE(depthModSources.size() == 1); + CHECK(depthModSources[0]->definition == ModulationSourceDefinition(6, MODULATION_TYPE::MACRO)); + CHECK(depthModSources[0]->modulationAmount == 0.7f); + } + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.cpp new file mode 100644 index 00000000..008c87ce --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.cpp @@ -0,0 +1,245 @@ +#include "XmlWriter.hpp" +#include "XmlConsts.hpp" + +#include "SplitterMutators.hpp" +#include "ModulationMutators.hpp" + +namespace XmlWriter { + void write(std::shared_ptr 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(splitter)) { + splitTypeString = XML_SPLIT_TYPE_SERIES_STR; + } else if (auto parallelSplitter = std::dynamic_pointer_cast(splitter)) { + splitTypeString = XML_SPLIT_TYPE_PARALLEL_STR; + } else if (auto multibandSplitter = std::dynamic_pointer_cast(splitter)) { + splitTypeString = XML_SPLIT_TYPE_MULTIBAND_STR; + } else if (auto leftRightSplitter = std::dynamic_pointer_cast(splitter)) { + splitTypeString = XML_SPLIT_TYPE_LEFTRIGHT_STR; + } else if (auto midSideSplitter = std::dynamic_pointer_cast(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(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 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(chain->chain[pluginNumber])) { + XmlWriter::write(gainStage, thisPluginElement); + } else if (auto pluginSlot = std::dynamic_pointer_cast(chain->chain[pluginNumber])) { + XmlWriter::write(pluginSlot, thisPluginElement); + } + } + } + + void write(std::shared_ptr 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 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 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 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 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 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 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> freqModSources = thisLfo->getFreqModulationSources(); + + for (int sourceIndex {0}; sourceIndex < freqModSources.size(); sourceIndex++) { + juce::XmlElement* thisSourceElement = freqModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); + + auto thisSource = std::dynamic_pointer_cast(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> depthModSources = thisLfo->getDepthModulationSources(); + + for (int sourceIndex {0}; sourceIndex < depthModSources.size(); sourceIndex++) { + juce::XmlElement* thisSourceElement = depthModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); + + auto thisSource = std::dynamic_pointer_cast(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> phaseModSources = thisLfo->getPhaseModulationSources(); + + for (int sourceIndex {0}; sourceIndex < phaseModSources.size(); sourceIndex++) { + juce::XmlElement* thisSourceElement = phaseModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); + + auto thisSource = std::dynamic_pointer_cast(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 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 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> freqModSources = thisRandom->getFreqModulationSources(); + + for (int sourceIndex {0}; sourceIndex < freqModSources.size(); sourceIndex++) { + juce::XmlElement* thisSourceElement = freqModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); + + auto thisSource = std::dynamic_pointer_cast(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> depthModSources = thisRandom->getDepthModulationSources(); + + for (int sourceIndex {0}; sourceIndex < depthModSources.size(); sourceIndex++) { + juce::XmlElement* thisSourceElement = depthModSourcesElement->createNewChildElement(getParameterModulationSourceXmlName(sourceIndex)); + + auto thisSource = std::dynamic_pointer_cast(depthModSources[sourceIndex].source); + if (thisSource != nullptr) { + thisSource->definition.writeToXml(thisSourceElement); + thisSourceElement->setAttribute(XML_MODULATION_SOURCE_AMOUNT, depthModSources[sourceIndex].amount); + } + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.hpp new file mode 100644 index 00000000..bc44356f --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#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 splitter, juce::XmlElement* element); + + void write(std::shared_ptr chain, juce::XmlElement* element); + + void write(std::shared_ptr gainStage, juce::XmlElement* element); + + void write(std::shared_ptr chainSlot, juce::XmlElement* element); + void write(std::shared_ptr config, juce::XmlElement* element); + void write(std::shared_ptr config, juce::XmlElement* element); + void write(std::shared_ptr source, juce::XmlElement* element); + + void write(ModelInterface::ModulationSourcesState& state, juce::XmlElement* element); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter_test.cpp new file mode 100644 index 00000000..40cd60f8 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Mutators/XmlWriter_test.cpp @@ -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(config, modulationCallback, latencyCallback); + + SplitterMutators::addChain(splitterParallel); + SplitterMutators::addChain(splitterParallel); + + auto splitter = std::dynamic_pointer_cast(splitterParallel); + + // Chain 0 + SplitterMutators::insertPlugin(splitter, std::make_shared(), 0, 0); + + // Chain 1 + SplitterMutators::insertGainStage(splitter, 1, 0); + SplitterMutators::insertPlugin(splitter, std::make_shared(), 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(splitter); + splitter = std::dynamic_pointer_cast(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(splitter); + splitter = std::dynamic_pointer_cast(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(splitter, std::optional>()); + splitter = std::dynamic_pointer_cast(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(splitter); + splitter = std::dynamic_pointer_cast(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::vector{"gain", "plugin"}, + std::vector{"plugin", "gain"} + ); + + auto chain = std::make_shared(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(); + 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(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 plugin = + std::make_shared(); + + auto config = std::make_shared(); + config->parameterConfigs.push_back(std::make_shared()); + config->parameterConfigs[0]->targetParameterName = "testConfig1"; + + auto modulationCallback = [](int, MODULATION_TYPE) { return 0.0f; }; + + auto slot = std::make_shared(plugin, isBypassed, modulationCallback, hostConfig); + slot->modulationConfig = config; + + slot->editorBounds.reset(new PluginEditorBounds()); + *(slot->editorBounds.get()) = PluginEditorBoundsContainer( + juce::Rectangle(150, 200), + juce::Rectangle(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::fromString(e.getStringAttribute(XML_PLUGIN_EDITOR_BOUNDS_STR)) == slot->editorBounds->value().editorBounds); + CHECK(juce::Rectangle::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(); + config->isActive = isActive; + config->parameterConfigs.push_back(std::make_shared()); + config->parameterConfigs[0]->targetParameterName = "testConfig1"; + config->parameterConfigs.push_back(std::make_shared()); + 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 testData; + auto [parameterName, restValue] = GENERATE( + testData("parameterName", 0), + testData(" ", 0.5), + testData("name with spaces", 1)); + + auto config = std::make_shared(); + config->targetParameterName = parameterName; + config->restValue = restValue; + config->sources.push_back(std::make_shared( + ModulationSourceDefinition(1, MODULATION_TYPE::LFO), -0.5)); + config->sources.push_back(std::make_shared( + 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( + 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 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(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)); + } + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.cpp new file mode 100644 index 00000000..d9a8c808 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.cpp @@ -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(config.blockSize), + static_cast(getTotalNumInputChannels(config.layout)) + }); + + for (std::shared_ptr slot : chain.chain) { + if (auto gainStage = std::dynamic_pointer_cast(slot)) { + ChainProcessors::prepareToPlay(*gainStage.get(), config); + } else if (auto pluginSlot = std::dynamic_pointer_cast(slot)) { + ChainProcessors::prepareToPlay(*pluginSlot.get(), config); + } + } + } + + void releaseResources(PluginChain& chain) { + for (std::shared_ptr slot : chain.chain) { + if (auto gainStage = std::dynamic_pointer_cast(slot)) { + ChainProcessors::releaseResources(*gainStage.get()); + } else if (auto pluginSlot = std::dynamic_pointer_cast(slot)) { + ChainProcessors::releaseResources(*pluginSlot.get()); + } + } + } + + void reset(PluginChain& chain) { + for (std::shared_ptr slot : chain.chain) { + if (auto gainStage = std::dynamic_pointer_cast(slot)) { + ChainProcessors::reset(*gainStage.get()); + } else if (auto pluginSlot = std::dynamic_pointer_cast(slot)) { + ChainProcessors::reset(*pluginSlot.get()); + } + } + } + + void processBlock(PluginChain& chain, + juce::AudioBuffer& buffer, + juce::MidiBuffer& midiMessages, + juce::AudioPlayHead* newPlayHead) { + // Add the latency compensation + juce::dsp::AudioBlock bufferBlock(buffer); + juce::dsp::ProcessContextReplacing 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 slot : chain.chain) { + if (auto gainStage = std::dynamic_pointer_cast(slot)) { + ChainProcessors::processBlock(*gainStage.get(), buffer); + } else if (auto pluginSlot = std::dynamic_pointer_cast(slot)) { + ChainProcessors::processBlock(*pluginSlot.get(), buffer, midiMessages, newPlayHead); + } + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.hpp new file mode 100644 index 00000000..831cf0f9 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#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& buffer, + juce::MidiBuffer& midiMessages, + juce::AudioPlayHead* newPlayHead); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors_test.cpp new file mode 100644 index 00000000..9711ec99 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainProcessors_test.cpp @@ -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> onPrepareToPlay; + std::optional> onReleaseResources; + std::optional> onReset; + std::optional&, 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& 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::vector{"gain", "plugin"}, + std::vector{"plugin", "gain"} + ); + + const bool isChainBypassed = GENERATE(false, true); + const bool isChainMuted = GENERATE(false, true); + + // Mono and stereo + auto [layout, buffer] = GENERATE( + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), + juce::AudioBuffer(1, NUM_SAMPLES) + ), + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), + juce::AudioBuffer(2, NUM_SAMPLES) + ) + ); + + buffer.clear(); + + auto chain = std::make_shared(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(); + plugin->onProcess = [&didCallPluginProcess, expectedNumChannels](juce::AudioBuffer& 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, float>({}, 1), + std::make_pair, float>({"gain", "plugin"}, 0.8), + std::make_pair, 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>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), + juce::AudioBuffer(1, NUM_SAMPLES) + ), + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), + juce::AudioBuffer(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(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(); + plugin->onProcess = [](juce::AudioBuffer& 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>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), + juce::AudioBuffer(1, NUM_SAMPLES) + ), + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), + juce::AudioBuffer(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(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(modulationCallback); + auto plugin1 = std::make_shared(); + auto plugin2 = std::make_shared(); + 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); + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.cpp new file mode 100644 index 00000000..f28694c2 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.cpp @@ -0,0 +1,134 @@ +#include "ChainSlotProcessors.hpp" + +#include +#include "PluginUtils.h" + +namespace { + void applyModulationForParamter(ChainSlotPlugin& slot, + juce::AudioProcessorParameter* targetParameter, + const std::shared_ptr 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& 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(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& 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& 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); + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.hpp new file mode 100644 index 00000000..9a8435e8 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#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& buffer); + + void prepareToPlay(ChainSlotPlugin& slot, HostConfiguration config); + void releaseResources(ChainSlotPlugin& slot); + void reset(ChainSlotPlugin& slot); + void processBlock(ChainSlotPlugin& slot, + juce::AudioBuffer& buffer, + juce::MidiBuffer& midiMessages, + juce::AudioPlayHead* newPlayHead); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors_test.cpp new file mode 100644 index 00000000..fe11a73e --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ChainSlotProcessors_test.cpp @@ -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&, juce::MidiBuffer&)> onProcess; + + void processBlock(juce::AudioBuffer& 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("param1")); + addHostedParameter(std::make_unique("param2")); + addHostedParameter(std::make_unique("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>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), + juce::AudioBuffer(1, NUM_SAMPLES) + ), + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), + juce::AudioBuffer(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>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), + juce::AudioBuffer(1, NUM_SAMPLES) + ), + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), + juce::AudioBuffer(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>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), + juce::AudioBuffer(1, NUM_SAMPLES) + ), + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), + juce::AudioBuffer(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 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 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 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 plugin(ProcessorTestPluginInstance::create()); + plugin->onProcess = [](juce::AudioBuffer& 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 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 plugin(ProcessorTestPluginInstance::create()); + plugin->onProcess = [&didCallProcess](juce::AudioBuffer& 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()); + slot.modulationConfig->parameterConfigs.push_back(std::make_shared()); + + // 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(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(ModulationSourceDefinition(2, MODULATION_TYPE::LFO), 0.22)); + slot.modulationConfig->parameterConfigs[1]->sources.push_back( + std::make_shared(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 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 plugin(ProcessorTestPluginInstance::createWithSidechain()); + plugin->onProcess = [&didCallProcess](juce::AudioBuffer& 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); + } + } + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.cpp new file mode 100644 index 00000000..dcb53e88 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.cpp @@ -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>& filter : state.lowpassFilters) { + filter->prepare({sampleRate, static_cast(samplesPerBlock), static_cast(numFilterChannels)}); + } + + for (std::shared_ptr>& filter : state.highpassFilters) { + filter->prepare({sampleRate, static_cast(samplesPerBlock), static_cast(numFilterChannels)}); + } + + for (std::shared_ptr>& filter : state.allpassFilters) { + filter->prepare({sampleRate, static_cast(samplesPerBlock), static_cast(numFilterChannels)}); + } + + for (juce::AudioBuffer& 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>& filter : state.lowpassFilters) { + filter->reset(); + } + + for (std::shared_ptr>& filter : state.highpassFilters) { + filter->reset(); + } + + for (std::shared_ptr>& filter : state.allpassFilters) { + filter->reset(); + } + + for (juce::AudioBuffer& buffer : state.buffers) { + buffer.clear(); + } + } + + void processBlock(CrossoverState& state, juce::AudioBuffer& 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& lowBuffer = crossoverNumber == 0 ? buffer : state.buffers[crossoverNumber - 1]; + juce::AudioBuffer& highBuffer = state.buffers[crossoverNumber]; + + highBuffer.makeCopyOf(lowBuffer); + + { + juce::dsp::AudioBlock block(juce::dsp::AudioBlock(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 lowBufferCropped(lowBuffer.getArrayOfWritePointers(), lowBuffer.getNumChannels(), buffer.getNumSamples()); + ChainProcessors::processBlock(*state.bands[crossoverNumber].chain.get(), lowBufferCropped, midiMessages, newPlayHead); + } + + { + juce::dsp::AudioBlock block(juce::dsp::AudioBlock(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 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 block(juce::dsp::AudioBlock(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()); + } + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.hpp new file mode 100644 index 00000000..97e7e963 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/CrossoverProcessors.hpp @@ -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& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.cpp new file mode 100644 index 00000000..d20ce141 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.cpp @@ -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& lfo : state.lfos) { + lfo->setSampleRate(sampleRate); + } + + for (std::shared_ptr& env : state.envelopes) { + env->envelope->setSampleRate(sampleRate); + } + + for (std::shared_ptr& random : state.randomSources) { + random->setSampleRate(sampleRate); + } + } + + void reset(Mi::ModulationSourcesState& state) { + for (std::shared_ptr& lfo : state.lfos) { + lfo->reset(); + } + + for (std::shared_ptr& env : state.envelopes) { + env->envelope->reset(); + } + + for (std::shared_ptr& random : state.randomSources) { + random->reset(); + } + } + + void processBlock(Mi::ModulationSourcesState& state, juce::AudioBuffer& buffer, juce::AudioPlayHead::CurrentPositionInfo tempoInfo) { + const int totalNumInputChannels = buffer.getNumChannels(); + + for (std::shared_ptr& 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& lfo : state.lfos) { + lfo->getNextOutput(0); + } + + // ENVs + for (std::shared_ptr 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& 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; + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.hpp new file mode 100644 index 00000000..1fd41408 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ModulationProcessors.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#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& 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); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.cpp new file mode 100644 index 00000000..90f860f6 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.cpp @@ -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& 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); + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.hpp new file mode 100644 index 00000000..1d2d3196 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/ProcessingInterface.hpp @@ -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& 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); +} \ No newline at end of file diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.cpp new file mode 100644 index 00000000..d4efe106 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.cpp @@ -0,0 +1,235 @@ +#include "SplitterProcessors.hpp" +#include "ChainProcessors.hpp" + +namespace { + void copyBuffer(juce::AudioBuffer& source, juce::AudioBuffer& 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& source, juce::AudioBuffer& 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& buffer, juce::MidiBuffer& midiMessages, juce::AudioPlayHead* newPlayHead) { + ChainProcessors::processBlock(*(splitter.chains[0].chain.get()), buffer, midiMessages, newPlayHead); + } + + void processBlockParallel(PluginSplitterParallel& splitter, juce::AudioBuffer& 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 inputBufferCropped(splitter.inputBuffer->getArrayOfWritePointers(), splitter.inputBuffer->getNumChannels(), buffer.getNumSamples()); + juce::AudioBuffer 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& 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& 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 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 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& 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 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 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(&splitter)) { + parallelSplitter->inputBuffer.reset(new juce::AudioBuffer(getTotalNumInputChannels(layout), samplesPerBlock)); + parallelSplitter->outputBuffer.reset(new juce::AudioBuffer(2, samplesPerBlock)); // stereo main + } else if (auto multibandSplitter = dynamic_cast(&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(&splitter)) { + leftRightSplitter->leftBuffer.reset(new juce::AudioBuffer(getTotalNumInputChannels(layout), samplesPerBlock)); + leftRightSplitter->rightBuffer.reset(new juce::AudioBuffer(getTotalNumInputChannels(layout), samplesPerBlock)); + } else if (auto midSideSplitter = dynamic_cast(&splitter)) { + midSideSplitter->midBuffer.reset(new juce::AudioBuffer(getTotalNumInputChannels(layout), samplesPerBlock)); + midSideSplitter->sideBuffer.reset(new juce::AudioBuffer(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& buffer, + juce::MidiBuffer& midiMessages, + juce::AudioPlayHead* newPlayHead) { + if (auto seriesSplitter = dynamic_cast(&splitter)) { + processBlockSeries(*seriesSplitter, buffer, midiMessages, newPlayHead); + } else if (auto parallelSplitter = dynamic_cast(&splitter)) { + processBlockParallel(*parallelSplitter, buffer, midiMessages, newPlayHead); + } else if (auto multibandSplitter = dynamic_cast(&splitter)) { + processBlockMultiband(*multibandSplitter, buffer, midiMessages, newPlayHead); + } else if (auto leftRightSplitter = dynamic_cast(&splitter)) { + processBlockLeftRight(*leftRightSplitter, buffer, midiMessages, newPlayHead); + } else if (auto midSideSplitter = dynamic_cast(&splitter)) { + processBlockMidSide(*midSideSplitter, buffer, midiMessages, newPlayHead); + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.hpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.hpp new file mode 100644 index 00000000..c6d055e7 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#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& buffer, + juce::MidiBuffer& midiMessages, + juce::AudioPlayHead* newPlayHead); +} diff --git a/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors_test.cpp b/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors_test.cpp new file mode 100644 index 00000000..d9eb9dcd --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/Processor/Processing/SplitterProcessors_test.cpp @@ -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> onPrepareToPlay; + std::optional> onReleaseResources; + std::optional> onReset; + std::optional&, 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& 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>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), + juce::AudioBuffer(1, NUM_SAMPLES) + ), + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), + juce::AudioBuffer(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 splitter; + + if (splitTypeString == XML_SPLIT_TYPE_SERIES_STR) { + auto splitterSeries = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterSeries); + } else if (splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR) { + auto splitterParallel = std::make_shared(config, modulationCallback, latencyCallback); + SplitterMutators::addChain(splitterParallel); + splitter = std::dynamic_pointer_cast(splitterParallel); + } else if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { + auto splitterMultiband = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterMultiband); + } else if (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR) { + auto splitterLeftRight = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterLeftRight); + } else if (splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR) { + auto splitterMidSide = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(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(); + plugin->onProcess = [](juce::AudioBuffer& 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>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::mono()), + juce::AudioBuffer(1, NUM_SAMPLES) + ), + std::make_pair>( + TestUtils::createLayoutWithInputChannels(juce::AudioChannelSet::stereo()), + juce::AudioBuffer(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 splitter; + + if (splitTypeString == XML_SPLIT_TYPE_SERIES_STR) { + auto splitterSeries = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterSeries); + } else if (splitTypeString == XML_SPLIT_TYPE_PARALLEL_STR) { + auto splitterParallel = std::make_shared(config, modulationCallback, latencyCallback); + SplitterMutators::addChain(splitterParallel); + splitter = std::dynamic_pointer_cast(splitterParallel); + } else if (splitTypeString == XML_SPLIT_TYPE_MULTIBAND_STR) { + auto splitterMultiband = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterMultiband); + } else if (splitTypeString == XML_SPLIT_TYPE_LEFTRIGHT_STR) { + auto splitterLeftRight = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(splitterLeftRight); + } else if (splitTypeString == XML_SPLIT_TYPE_MIDSIDE_STR) { + auto splitterMidSide = std::make_shared(config, modulationCallback, latencyCallback); + splitter = std::dynamic_pointer_cast(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(); + plugin->onProcess = [](juce::AudioBuffer& 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(config, modulationCallback, latencyCallback); + SplitterMutators::addChain(splitterParallel); + + auto plugin1 = std::make_shared(); + auto plugin2 = std::make_shared(); + + 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(1, 5)); + splitterParallel->outputBuffer.reset(new juce::AudioBuffer(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(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(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(splitterParallel); + SplitterProcessors::reset(*(splitterSeries.get())); + + THEN("Each chain's releaseResources is called") { + CHECK(calledReset1); + CHECK(calledReset2); + } + } + } + } +} diff --git a/ports-juce7/syndicate/PluginCommon/README.md b/ports-juce7/syndicate/PluginCommon/README.md new file mode 100644 index 00000000..863d7c73 --- /dev/null +++ b/ports-juce7/syndicate/PluginCommon/README.md @@ -0,0 +1 @@ +Contains code common to both Syndicate plugins \ No newline at end of file diff --git a/ports-juce7/syndicate/PluginScanServer/Main.cpp b/ports-juce7/syndicate/PluginScanServer/Main.cpp new file mode 100644 index 00000000..4092b397 --- /dev/null +++ b/ports-juce7/syndicate/PluginScanServer/Main.cpp @@ -0,0 +1,12 @@ +/* + ============================================================================== + + This file contains the basic startup code for a JUCE application. + + ============================================================================== +*/ + +#include +#include "ServerApplication.h" + +START_JUCE_APPLICATION(ServerApplication) diff --git a/ports-juce7/syndicate/PluginScanServer/ServerApplication.h b/ports-juce7/syndicate/PluginScanServer/ServerApplication.h new file mode 100644 index 00000000..774c54c0 --- /dev/null +++ b/ports-juce7/syndicate/PluginScanServer/ServerApplication.h @@ -0,0 +1,76 @@ +#pragma once + +#include + +#include "AllUtils.h" +#include "MainLogger.h" +#include "NullLogger.hpp" +#include "ServerProcess.h" + +// TODO use macros for name and version + +class ServerApplication : public juce::JUCEApplicationBase { +public: + ServerApplication() { + const Utils::Config config = Utils::LoadConfig(); + if (config.enableLogFile) { + _fileLogger = std::make_unique("Syndicate Plugin Scan Server", ProjectInfo::versionString, Utils::PluginScanServerLogDirectory); + juce::Logger::setCurrentLogger(_fileLogger.get()); + } else { + juce::Logger::setCurrentLogger(&_nullLogger); + } + } + + ~ServerApplication() { + // Logger must be removed before being deleted + // (this must be the last thing we do before exiting) + juce::Logger::setCurrentLogger(nullptr); + } + + const juce::String getApplicationName() override { return "Syndicate Plugin Scan Server"; } + + const juce::String getApplicationVersion() override { return "0.0.1"; } + + bool moreThanOneInstanceAllowed() override { return false; } + + void initialise(const juce::String& commandLineParameters) override { + juce::Logger::writeToLog("Initialising"); + _process.reset(new ServerProcess()); + + if (_process->initialiseFromCommandLine(commandLineParameters, Utils::PLUGIN_SCAN_SERVER_UID)) { + juce::Logger::writeToLog("Process started"); + } + } + + void shutdown() override { + juce::Logger::writeToLog("Shutting down"); + } + + void anotherInstanceStarted(const juce::String& commandLine) override { + // TODO + } + + void systemRequestedQuit() override { + juce::Logger::writeToLog("System Requested Quit"); + quit(); + } + + void suspended() override { + // TODO + } + + void resumed() override { + // TODO + } + + void unhandledException(const std::exception*, + const juce::String& sourceFilename, + int lineNumber) override { + juce::Logger::writeToLog("Unhandled exception"); + } + +private: + std::unique_ptr _fileLogger; + NullLogger _nullLogger; + std::unique_ptr _process; +}; diff --git a/ports-juce7/syndicate/PluginScanServer/ServerProcess.h b/ports-juce7/syndicate/PluginScanServer/ServerProcess.h new file mode 100644 index 00000000..55822095 --- /dev/null +++ b/ports-juce7/syndicate/PluginScanServer/ServerProcess.h @@ -0,0 +1,106 @@ +#pragma once + +#include + +class ServerProcess : private juce::ChildProcessWorker, + private juce::AsyncUpdater { +public: + ServerProcess() { + formatManager.addDefaultFormats(); + } + + using ChildProcessWorker::initialiseFromCommandLine; + +private: + void handleMessageFromCoordinator(const juce::MemoryBlock& mb) override { + if (mb.isEmpty()) { + return; + } + + if (!doScan(mb)) { + { + const std::lock_guard lock(mutex); + pendingBlocks.emplace(mb); + } + + triggerAsyncUpdate(); + } + } + + void handleConnectionLost() override { + juce::JUCEApplicationBase::quit(); + } + + void handleAsyncUpdate() override { + for (;;) { + const auto block = [&]() -> juce::MemoryBlock { + const std::lock_guard lock(mutex); + + if (pendingBlocks.empty()) + return {}; + + auto out = std::move(pendingBlocks.front()); + pendingBlocks.pop(); + return out; + }(); + + if (block.isEmpty()) { + return; + } + + doScan(block); + } + } + + bool doScan(const juce::MemoryBlock& block) + { + juce::MemoryInputStream stream { block, false }; + const auto formatName = stream.readString(); + const auto identifier = stream.readString(); + + juce::PluginDescription pd; + pd.fileOrIdentifier = identifier; + pd.uniqueId = pd.deprecatedUid = 0; + + const auto matchingFormat = [&]() -> juce::AudioPluginFormat* { + for (auto* format : formatManager.getFormats()) { + if (format->getName() == formatName) { + juce::Logger::writeToLog("Using format: " + format->getName()); + return format; + } + } + + return nullptr; + }(); + + if (matchingFormat == nullptr + || (!juce::MessageManager::getInstance()->isThisTheMessageThread() + && !matchingFormat->requiresUnblockedMessageThreadDuringCreation(pd))) { + return false; + } + + juce::OwnedArray results; + matchingFormat->findAllTypesForFile(results, identifier); + sendPluginDescriptions(results); + return true; + } + + void sendPluginDescriptions(const juce::OwnedArray& results) { + juce::XmlElement xml("LIST"); + + for (const auto& desc : results) { + xml.addChildElement(desc->createXml().release()); + } + + const auto str = xml.toString(); + juce::Logger::writeToLog("Results: " + juce::String(results.size()) + " Sending description: " + str); + sendMessageToCoordinator({str.toRawUTF8(), str.getNumBytesAsUTF8()}); + } + + std::mutex mutex; + std::queue pendingBlocks; + + // After construction, this will only be accessed by doScan so there's no need + // to worry about synchronisation. + juce::AudioPluginFormatManager formatManager; +}; diff --git a/ports-juce7/syndicate/README.md b/ports-juce7/syndicate/README.md new file mode 100644 index 00000000..b7b1c4b4 --- /dev/null +++ b/ports-juce7/syndicate/README.md @@ -0,0 +1,7 @@ +# syndicate-mirror + +Known issues (as of 1.3.0): +- MIDI isn't handled consistently in different split types +- [linux] Plugin editor windows aren't forced to the top correctly +- Crashed plugins dialogue can't be dismissed without closing the plugin selector window +- Plugin open error dialogue can't be dismissed without closing the plugin selector window diff --git a/ports-juce7/syndicate/Syndicate/ParameterData.h b/ports-juce7/syndicate/Syndicate/ParameterData.h new file mode 100644 index 00000000..635512a0 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/ParameterData.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include "General/ParameterDefinition.h" +#include "General/CoreMath.h" + +constexpr int NUM_MACROS {4}; + +const ParameterDefinition::RangedParameter MACRO(0, 1, 0), + ENVELOPE_AMOUNT(-5, 5, 0), + OUTPUTGAIN(-60, 20, 0), + OUTPUTPAN(-1, 1, 0); + +const juce::Array MACRO_STRS {"Macro1", "Macro2", "Macro3", "Macro4"}; + +const juce::String SPLIT_TYPE_STR("SplitType"), + GRAPH_STATE_STR("GraphStateParam"), + OUTPUTGAIN_STR("OutputGain"), + OUTPUTPAN_STR("OutputPan"); diff --git a/ports-juce7/syndicate/Syndicate/PluginProcessor.cpp b/ports-juce7/syndicate/Syndicate/PluginProcessor.cpp new file mode 100644 index 00000000..3f252fa7 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/PluginProcessor.cpp @@ -0,0 +1,1171 @@ +/* + ============================================================================== + + This file contains the basic framework code for a JUCE plugin processor. + + ============================================================================== +*/ + +#include "PluginProcessor.h" + +#include "PluginEditor.h" +#include "ParameterData.h" +#include "AllUtils.h" +#include "PluginUtils.h" +#include "XmlReader.hpp" +#include "XmlWriter.hpp" + +namespace { + // Modulation sources + const char* XML_MODULATION_SOURCES_STR {"ModulationSources"}; + + const char* XML_MACRO_NAMES_STR {"MacroNames"}; + + const char* XML_METADATA_STR {"Metadata"}; + const char* XML_METADATA_NAME_STR {"MetadataName"}; + const char* XML_METADATA_FULLPATH_STR {"MetadataFullPath"}; + const char* XML_METADATA_AUTHOR_STR {"MetadataAuthor"}; + const char* XML_METADATA_DESCRIPTION_STR {"MetadataDescription"}; + + const char* XML_MAIN_WINDOW_STATE_STR {"MainWindowState"}; + const char* XML_MAIN_WINDOW_BOUNDS_STR {"MainEditorBounds"}; + const char* XML_GRAPH_VIEW_POSITION_STR {"GraphViewScrollPosition"}; + const char* XML_CHAIN_VIEW_POSITIONS_STR {"ChainViewScrollPositions"}; + const char* XML_LFO_BUTTONS_POSITION_STR {"LfoButtonsScrollPosition"}; + const char* XML_ENV_BUTTONS_POSITION_STR {"EnvButtonsScrollPosition"}; + const char* XML_SELECTED_SOURCE_STR {"SelectedSource"}; + + juce::String getMacroNameXMLName(int macroNumber) { + juce::String retVal("MacroName_"); + retVal += juce::String(macroNumber); + return retVal; + } + + juce::String getChainPositionXMLName(int chainNumber) { + juce::String retVal("Chain_"); + retVal += juce::String(chainNumber); + return retVal; + } + + // Window states + const char* XML_PLUGIN_SELECTOR_STATE_STR {"pluginSelectorState"}; + const char* XML_PLUGIN_PARAMETER_SELECTOR_STATE_STR {"pluginParameterSelectorState"}; +} + +//============================================================================== +SyndicateAudioProcessor::SyndicateAudioProcessor() : + 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); }), + _editor(nullptr), + _outputGainLinear(1) +{ + + const Utils::Config config = Utils::LoadConfig(); + if (config.enableLogFile) { + _fileLogger = std::make_unique(JucePlugin_Name, JucePlugin_VersionString, Utils::PluginLogDirectory); + juce::Logger::setCurrentLogger(_fileLogger.get()); + } else { + juce::Logger::setCurrentLogger(&_nullLogger); + } + + constexpr float PRECISION {0.01f}; + registerPrivateParameter(_splitterParameters, "SplitterParameters"); + + // Register the macro parameters + for (int index {0}; index < macros.size(); index++) { + registerParameter(macros[index], MACRO_STRS[index], &MACRO, MACRO.defaultValue, PRECISION); + } + + registerParameter(outputGainLog, OUTPUTGAIN_STR, &OUTPUTGAIN, OUTPUTGAIN.defaultValue, PRECISION); + registerParameter(outputPan, OUTPUTPAN_STR, &OUTPUTPAN, OUTPUTPAN.defaultValue, PRECISION); + + // Add a default LFO and envelope + ModelInterface::createDefaultSources(manager); + + // Make sure everything is initialised + _splitterParameters->setProcessor(this); + _onParameterUpdate(); + pluginScanClient.restore(); + + for (int index {0}; index < macroNames.size(); index++) { + macroNames[index] = "Macro " + juce::String(index + 1); + } + + for (auto& env : meterEnvelopes) { + env.setAttackTimeMs(1); + env.setReleaseTimeMs(50); + env.setFilterEnabled(false); + } + + formatManager.addDefaultFormats(); +} + +SyndicateAudioProcessor::~SyndicateAudioProcessor() +{ + pluginScanClient.stopScan(); + + // Logger must be removed before being deleted + // (this must be the last thing we do before exiting) + juce::Logger::setCurrentLogger(nullptr); +} + +//============================================================================== +const juce::String SyndicateAudioProcessor::getName() const +{ + return JucePlugin_Name; +} + +bool SyndicateAudioProcessor::acceptsMidi() const +{ + #if JucePlugin_WantsMidiInput + return true; + #else + return false; + #endif +} + +bool SyndicateAudioProcessor::producesMidi() const +{ + #if JucePlugin_ProducesMidiOutput + return true; + #else + return false; + #endif +} + +bool SyndicateAudioProcessor::isMidiEffect() const +{ + #if JucePlugin_IsMidiEffect + return true; + #else + return false; + #endif +} + +double SyndicateAudioProcessor::getTailLengthSeconds() const +{ + return 0.0; +} + +int SyndicateAudioProcessor::getNumPrograms() +{ + return 1; // NB: some hosts don't cope very well if you tell them there are 0 programs, + // so this should be at least 1, even if you're not really implementing programs. +} + +int SyndicateAudioProcessor::getCurrentProgram() +{ + return 0; +} + +void SyndicateAudioProcessor::setCurrentProgram (int index) +{ +} + +const juce::String SyndicateAudioProcessor::getProgramName (int index) +{ + return {}; +} + +void SyndicateAudioProcessor::changeProgramName (int index, const juce::String& newName) +{ +} + +//============================================================================== +void SyndicateAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock) +{ + // Use this method as the place to do any pre-playback + // initialisation that you need.. + for (auto& env : meterEnvelopes) { + env.setSampleRate(sampleRate); + } + + juce::Logger::writeToLog("Setting bus layout:\n" + Utils::busesLayoutToString(getBusesLayout())); + ModelInterface::prepareToPlay(manager, sampleRate, samplesPerBlock, getBusesLayout()); +} + +void SyndicateAudioProcessor::releaseResources() +{ + // When playback stops, you can use this as an opportunity to free up any + // spare memory, etc. + ModelInterface::releaseResources(manager); +} + +void SyndicateAudioProcessor::reset() { + for (auto& env : meterEnvelopes) { + env.reset(); + } + + ModelInterface::reset(manager); +} + +bool SyndicateAudioProcessor::isBusesLayoutSupported(const BusesLayout& layout) const { + const bool inputEqualsOutput { + layout.getMainInputChannelSet() == layout.getMainOutputChannelSet() + }; + + const bool isMonoOrStereo { + layout.getMainInputChannelSet().size() == 1 || layout.getMainInputChannelSet().size() == 2 + }; + + return inputEqualsOutput && isMonoOrStereo && !layout.getMainInputChannelSet().isDisabled(); +} + +void SyndicateAudioProcessor::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) +{ + juce::ScopedNoDenormals noDenormals; + auto totalNumInputChannels = getTotalNumInputChannels(); + auto totalNumOutputChannels = getTotalNumOutputChannels(); + + // In case we have more outputs than inputs, this code clears any output + // channels that didn't contain input data, (because these aren't + // guaranteed to be empty - they may contain garbage). + // This is here to avoid people getting screaming feedback + // when they first compile a plugin, but obviously you don't need to keep + // this code if your algorithm always overwrites all the output channels. + for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i) + buffer.clear (i, 0, buffer.getNumSamples()); + + // Send tempo and playhead information to the LFOs + 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) + ModelInterface::processBlock(manager, buffer, midiMessages, getPlayHead(), mTempoInfo); + + // Apply the output gain + for (int channel {0}; channel < getMainBusNumInputChannels(); channel++) + { + juce::FloatVectorOperations::multiply(buffer.getWritePointer(channel), + _outputGainLinear, + buffer.getNumSamples()); + } + + // Apply the output pan + if (canDoStereoSplitTypes(getBusesLayout())) { + // Stereo input - apply balance + Utils::processBalance(outputPan->get(), buffer); + } + + // After processing everything, update the meter envelopes + for (int sampleIndex {0}; sampleIndex < buffer.getNumSamples(); sampleIndex++) { + for (int channel {0}; channel < std::min(getMainBusNumInputChannels(), static_cast(meterEnvelopes.size())); channel++) { + meterEnvelopes[channel].getNextOutput(buffer.getReadPointer(channel)[sampleIndex]); + } + } +} + +//============================================================================== +bool SyndicateAudioProcessor::hasEditor() const +{ + return true; // (change this to false if you choose to not supply an editor) +} + +juce::AudioProcessorEditor* SyndicateAudioProcessor::createEditor() +{ + return new SyndicateAudioProcessorEditor (*this); +} + +void SyndicateAudioProcessor::addLfo() { + ModelInterface::addLfo(manager); + + if (_editor != nullptr) { + _editor->needsModulationBarRebuild(); + } +} + +void SyndicateAudioProcessor::setLfoTempoSyncSwitch(int lfoIndex, bool val) { + ModelInterface::setLfoTempoSyncSwitch(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setLfoInvertSwitch(int lfoIndex, bool val) { + ModelInterface::setLfoInvertSwitch(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setLfoOutputMode(int lfoIndex, int val) { + ModelInterface::setLfoOutputMode(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setLfoWave(int lfoIndex, int val) { + ModelInterface::setLfoWave(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setLfoTempoNumer(int lfoIndex, int val) { + ModelInterface::setLfoTempoNumer(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setLfoTempoDenom(int lfoIndex, int val) { + ModelInterface::setLfoTempoDenom(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setLfoFreq(int lfoIndex, double val) { + ModelInterface::setLfoFreq(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setLfoDepth(int lfoIndex, double val) { + ModelInterface::setLfoDepth(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setLfoManualPhase(int lfoIndex, double val) { + ModelInterface::setLfoManualPhase(manager, lfoIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::addEnvelope() { + ModelInterface::addEnvelope(manager); + + if (_editor != nullptr) { + _editor->needsModulationBarRebuild(); + } +} + +void SyndicateAudioProcessor::setEnvAttackTimeMs(int envIndex, double val) { + ModelInterface::setEnvAttackTimeMs(manager, envIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setEnvReleaseTimeMs(int envIndex, double val) { + ModelInterface::setEnvReleaseTimeMs(manager, envIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setEnvFilterEnabled(int envIndex, bool val) { + ModelInterface::setEnvFilterEnabled(manager, envIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setEnvFilterHz(int envIndex, double lowCut, double highCut) { + ModelInterface::setEnvFilterHz(manager, envIndex, lowCut, highCut); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setEnvAmount(int envIndex, float val) { + ModelInterface::setEnvAmount(manager, envIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setEnvUseSidechainInput(int envIndex, bool val) { + ModelInterface::setEnvUseSidechainInput(manager, envIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::addRandomSource() { + ModelInterface::addRandom(manager); + + if (_editor != nullptr) { + _editor->needsModulationBarRebuild(); + } +} + +void SyndicateAudioProcessor::setRandomOutputMode(int randomIndex, int val) { + ModelInterface::setRandomOutputMode(manager, randomIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setRandomFreq(int randomIndex, double val) { + ModelInterface::setRandomFreq(manager, randomIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setRandomDepth(int randomIndex, double val) { + ModelInterface::setRandomDepth(manager, randomIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +float SyndicateAudioProcessor::getModulationValueForSource(int id, MODULATION_TYPE type) { + // TODO This method may be called multiple times for each buffer, could be optimised + if (type == MODULATION_TYPE::MACRO) { + const int index {id - 1}; + if (index < macros.size()) { + return macros[index]->get(); + } + } else if (type == MODULATION_TYPE::LFO) { + return ModelInterface::getLfoModulationValue(manager, id); + } else if (type == MODULATION_TYPE::ENVELOPE) { + return ModelInterface::getEnvelopeModulationValue(manager, id); + } else if (type == MODULATION_TYPE::RANDOM) { + return ModelInterface::getRandomModulationValue(manager, id); + } + + return 0.0f; +} + +void SyndicateAudioProcessor::removeModulationSource(ModulationSourceDefinition definition) { + ModelInterface::removeModulationSource(manager, definition); + + // Make sure any changes to assigned sources are reflected in the UI + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::setSplitType(SPLIT_TYPE splitType) { + if (ModelInterface::setSplitType(manager, splitType, {getBusesLayout(), getSampleRate(), getBlockSize()})) { + // For graph state changes we need to make sure the processor has updated its state first, + // then the UI can rebuild based on the processor state + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + } +} + +void SyndicateAudioProcessor::setChainBypass(int chainNumber, bool val) { + ModelInterface::setChainBypass(manager, chainNumber, val); + + if (_editor != nullptr) { + _editor->needsChainButtonsRefresh(); + } +} + +void SyndicateAudioProcessor::setChainMute(int chainNumber, bool val) { + ModelInterface::setChainMute(manager, chainNumber, val); + + if (_editor != nullptr) { + _editor->needsChainButtonsRefresh(); + } +} + +void SyndicateAudioProcessor::setChainSolo(int chainNumber, bool val) { + ModelInterface::setChainSolo(manager, chainNumber, val); + + if (_editor != nullptr) { + _editor->needsChainButtonsRefresh(); + } +} + +void SyndicateAudioProcessor::setChainCustomName(int chainNumber, const juce::String& name) { + ModelInterface::setChainCustomName(manager, chainNumber, name); + + if (_editor != nullptr) { + _editor->needsChainButtonsRefresh(); + } + +} + +void SyndicateAudioProcessor::setSlotBypass(int chainNumber, int positionInChain, bool bypass) { + ModelInterface::setSlotBypass(manager, chainNumber, positionInChain, bypass); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setSlotGainLinear(int chainNumber, int positionInChain, float gain) { + ModelInterface::setGainLinear(manager, chainNumber, positionInChain, gain); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setSlotPan(int chainNumber, int positionInChain, float pan) { + ModelInterface::setPan(manager, chainNumber, positionInChain, pan); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setPluginModulationIsActive(int chainNumber, int pluginNumber, bool val) { + ModelInterface::setPluginModulationIsActive(manager, chainNumber, pluginNumber, val); + + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::setModulationTarget(int chainNumber, int pluginNumber, int targetNumber, juce::String targetName) { + ModelInterface::setModulationTarget(manager, chainNumber, pluginNumber, targetNumber, targetName); + + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::removeModulationTarget(int chainNumber, int pluginNumber, int targetNumber) { + ModelInterface::removeModulationTarget(manager, chainNumber, pluginNumber, targetNumber); + + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::addModulationSourceToTarget(int chainNumber, int pluginNumber, int targetNumber, ModulationSourceDefinition source) { + ModelInterface::addModulationSourceToTarget(manager, chainNumber, pluginNumber, targetNumber, source); + + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::removeModulationSourceFromTarget(int chainNumber, int pluginNumber, int targetNumber, ModulationSourceDefinition source) { + ModelInterface::removeModulationSourceFromTarget(manager, chainNumber, pluginNumber, targetNumber, source); + + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::setModulationTargetValue(int chainNumber, int pluginNumber, int targetNumber, float val) { + ModelInterface::setModulationTargetValue(manager, chainNumber, pluginNumber, targetNumber, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::setModulationSourceValue(int chainNumber, int pluginNumber, int targetNumber, int sourceNumber, float val) { + ModelInterface::setModulationSourceValue(manager, chainNumber, pluginNumber, targetNumber, sourceNumber, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::addSourceToLFOFreq(int lfoIndex, ModulationSourceDefinition source) { + ModelInterface::addSourceToLFOFreq(manager, lfoIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::removeSourceFromLFOFreq(int lfoIndex, ModulationSourceDefinition source) { + ModelInterface::removeSourceFromLFOFreq(manager, lfoIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::setLFOFreqModulationAmount(int lfoIndex, int sourceIndex, double val) { + ModelInterface::setLFOFreqModulationAmount(manager, lfoIndex, sourceIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::addSourceToLFODepth(int lfoIndex, ModulationSourceDefinition source) { + ModelInterface::addSourceToLFODepth(manager, lfoIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::removeSourceFromLFODepth(int lfoIndex, ModulationSourceDefinition source) { + ModelInterface::removeSourceFromLFODepth(manager, lfoIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::setLFODepthModulationAmount(int lfoIndex, int sourceIndex, double val) { + ModelInterface::setLFODepthModulationAmount(manager, lfoIndex, sourceIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::addSourceToLFOPhase(int lfoIndex, ModulationSourceDefinition source) { + ModelInterface::addSourceToLFOPhase(manager, lfoIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::removeSourceFromLFOPhase(int lfoIndex, ModulationSourceDefinition source) { + ModelInterface::removeSourceFromLFOPhase(manager, lfoIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::setLFOPhaseModulationAmount(int lfoIndex, int sourceIndex, double val) { + ModelInterface::setLFOPhaseModulationAmount(manager, lfoIndex, sourceIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::addSourceToRandomFreq(int randomIndex, ModulationSourceDefinition source) { + ModelInterface::addSourceToRandomFreq(manager, randomIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::removeSourceFromRandomFreq(int randomIndex, ModulationSourceDefinition source) { + ModelInterface::removeSourceFromRandomFreq(manager, randomIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::setRandomFreqModulationAmount(int randomIndex, int sourceIndex, double val) { + ModelInterface::setRandomFreqModulationAmount(manager, randomIndex, sourceIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::addSourceToRandomDepth(int randomIndex, ModulationSourceDefinition source) { + ModelInterface::addSourceToRandomDepth(manager, randomIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::removeSourceFromRandomDepth(int randomIndex, ModulationSourceDefinition source) { + ModelInterface::removeSourceFromRandomDepth(manager, randomIndex, source); + + if (_editor != nullptr) { + _editor->needsSelectedModulationSourceRebuild(); + } +} + +void SyndicateAudioProcessor::setRandomDepthModulationAmount(int randomIndex, int sourceIndex, double val) { + ModelInterface::setRandomDepthModulationAmount(manager, randomIndex, sourceIndex, val); + + if (_editor != nullptr) { + _editor->needsUndoRedoRefresh(); + } +} + +void SyndicateAudioProcessor::addParallelChain() { + if (ModelInterface::addParallelChain(manager)) { + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + } +} + +void SyndicateAudioProcessor::removeParallelChain(int chainNumber) { + if (ModelInterface::removeParallelChain(manager, chainNumber)) { + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + } +} + +void SyndicateAudioProcessor::addCrossoverBand() { + ModelInterface::addCrossoverBand(manager); + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::removeCrossoverBand(int bandNumber) { + if (ModelInterface::removeCrossoverBand(manager, bandNumber)) { + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + } +} + +void SyndicateAudioProcessor::setCrossoverFrequency(size_t index, float val) { + // Changing the frequency of one crossover may affect others if they also need to be + // moved - so we set the splitter first, it will update all the frequencies internally, + // then update the parameter + if (ModelInterface::setCrossoverFrequency(manager, index, val)) { + _splitterParameters->triggerUpdate(); + } +} + +bool SyndicateAudioProcessor::onPluginSelectedByUser(std::shared_ptr plugin, + int chainNumber, + int pluginNumber) { + + juce::Logger::writeToLog("SyndicateAudioProcessor::onPluginSelectedByUser: Loading plugin"); + + if (pluginConfigurator.configure(plugin, + {getBusesLayout(), getSampleRate(), getBlockSize()})) { + juce::Logger::writeToLog("SyndicateAudioProcessor::onPluginSelectedByUser: Plugin configured"); + + // Hand the plugin over to the splitter + if (ModelInterface::replacePlugin(manager, std::move(plugin), chainNumber, pluginNumber)) { + // Ideally we'd like to handle plugin selection like any other parameter - just update the + // parameter and just action the update in the callback + // We can't do that though as the splitters are stateful, but we still update the parameter + // so the UI also gets the update - need to do this last though as the UI pulls its state + // from the splitter + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + + return true; + } else { + juce::Logger::writeToLog("SyndicateAudioProcessor::onPluginSelectedByUser: Failed to insert new plugin"); + } + } else { + juce::Logger::writeToLog("SyndicateAudioProcessor::onPluginSelectedByUser: Failed to configure plugin"); + } + + return false; +} + +void SyndicateAudioProcessor::removePlugin(int chainNumber, int pluginNumber) { + juce::Logger::writeToLog("Removing slot from graph: " + juce::String(chainNumber) + " " + juce::String(pluginNumber)); + + if (ModelInterface::removeSlot(manager, chainNumber, pluginNumber)) { + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + } +} + +void SyndicateAudioProcessor::insertGainStage(int chainNumber, int pluginNumber) { + juce::Logger::writeToLog("Inserting gain stage: " + juce::String(chainNumber) + " " + juce::String(pluginNumber)); + + if (ModelInterface::insertGainStage(manager, chainNumber, pluginNumber)) { + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + } +} + +void SyndicateAudioProcessor::copySlot(int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber) { + auto onSuccess = [&]() { + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + }; + + ModelInterface::copySlot(manager, onSuccess, formatManager, fromChainNumber, fromSlotNumber, toChainNumber, toSlotNumber); +} + +void SyndicateAudioProcessor::moveSlot(int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber) { + ModelInterface::moveSlot(manager, fromChainNumber, fromSlotNumber, toChainNumber, toSlotNumber); + + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::moveChain(int fromChainNumber, int toChainNumber) { + ModelInterface::moveChain(manager, fromChainNumber, toChainNumber); + + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } +} + +void SyndicateAudioProcessor::copyChain(int fromChainNumber, int toChainNumber) { + auto onSuccess = [&]() { + if (_editor != nullptr) { + _editor->needsGraphRebuild(); + } + }; + + ModelInterface::copyChain(manager, onSuccess, formatManager, fromChainNumber, toChainNumber); +} + +void SyndicateAudioProcessor::resetAllState() { + ModelInterface::resetAllState(manager, + {getBusesLayout(), getSampleRate(), getBlockSize()}, + [&](int id, MODULATION_TYPE type) { return getModulationValueForSource(id, type); }, + [&](int newLatencySamples) { onLatencyChange(newLatencySamples); }); + + ModelInterface::createDefaultSources(manager); + ModelInterface::prepareToPlay(manager, getSampleRate(), getBlockSize(), getBusesLayout()); + + if (_editor != nullptr) { + _editor->needsToRefreshAll(); + } +} + +void SyndicateAudioProcessor::undo() { + ModelInterface::undo(manager, getSampleRate(), getBlockSize(), getBusesLayout()); + + if (_editor != nullptr) { + _editor->needsToRefreshAll(); + } +} + +void SyndicateAudioProcessor::redo() { + ModelInterface::redo(manager, getSampleRate(), getBlockSize(), getBusesLayout()); + + if (_editor != nullptr) { + _editor->needsToRefreshAll(); + } +} + +void SyndicateAudioProcessor::setPresetMetadata(const PresetMetadata& newMetadata) { + presetMetadata = newMetadata; + + if (_editor != nullptr) { + _editor->needsImportExportRefresh(); + } +} + +void SyndicateAudioProcessor::onLatencyChange(int newLatencySamples) { + setLatencySamples(newLatencySamples); +} + +std::vector SyndicateAudioProcessor::_provideParamNamesForMigration() { + // No parameters to migrate + return std::vector(); +} + +void SyndicateAudioProcessor::_migrateParamValues(std::vector& /*paramValues*/) { + // Do nothing - no parameters to migrate +} + +void SyndicateAudioProcessor::_onParameterUpdate() { + _outputGainLinear = WECore::CoreMath::dBToLinear(outputGainLog->get()); +} + +void SyndicateAudioProcessor::getStateInformation(juce::MemoryBlock& destData) { +#ifdef DEMO_BUILD + juce::Logger::writeToLog("Not saving state - demo build"); +#else + WECore::JUCEPlugin::CoreAudioProcessor::getStateInformation(destData); +#endif +} + +void SyndicateAudioProcessor::setStateInformation(const void* data, int sizeInBytes) { +#ifdef DEMO_BUILD + juce::Logger::writeToLog("Not restoring state - demo build"); +#else + WECore::JUCEPlugin::CoreAudioProcessor::setStateInformation(data, sizeInBytes); + + // Some DAWs can open the UI before loading the state, so we need to make sure the UI is updated + if (_editor != nullptr) { + _editor->needsToRefreshAll(); + } +#endif +} + +void SyndicateAudioProcessor::SplitterParameters::restoreFromXml(juce::XmlElement* element) { + juce::Logger::writeToLog("Restoring plugin state from XML"); + + if (_processor != nullptr) { + juce::XmlElement* splitterElement = element->getChildByName(XML_SPLITTER_STR); + if (splitterElement != nullptr) { + // Restore the splitter first as we need to know how many chains there are + _restoreSplitterFromXml(splitterElement); + } else { + juce::Logger::writeToLog("Missing element " + juce::String(XML_SPLITTER_STR)); + } + + juce::XmlElement* modulationElement = element->getChildByName(XML_MODULATION_SOURCES_STR); + if (modulationElement != nullptr) { + // Restore the modulation source parameters + _restoreModulationSourcesFromXml(modulationElement); + } else { + juce::Logger::writeToLog("Missing element " + juce::String(XML_MODULATION_SOURCES_STR)); + } + + juce::XmlElement* macroNamesElement = element->getChildByName(XML_MACRO_NAMES_STR); + if (macroNamesElement != nullptr) { + // Restore the macro names + _restoreMacroNamesFromXml(macroNamesElement); + } else { + juce::Logger::writeToLog("Missing element " + juce::String(XML_MACRO_NAMES_STR)); + } + + juce::XmlElement* metadataElement = element->getChildByName(XML_METADATA_STR); + if (metadataElement != nullptr) { + // Restore the metadata + _restoreMetadataFromXml(metadataElement); + } else { + juce::Logger::writeToLog("Missing element " + juce::String(XML_METADATA_STR)); + } + + juce::XmlElement* pluginSelectorElement = element->getChildByName(XML_PLUGIN_SELECTOR_STATE_STR); + if (pluginSelectorElement != nullptr) { + // Restore the plugin selector window state + _processor->pluginSelectorState.restoreFromXml(pluginSelectorElement); + } else { + juce::Logger::writeToLog("Missing element " + juce::String(XML_PLUGIN_SELECTOR_STATE_STR)); + } + + juce::XmlElement* pluginParameterSelectorElement = element->getChildByName(XML_PLUGIN_PARAMETER_SELECTOR_STATE_STR); + if (pluginParameterSelectorElement != nullptr) { + // Restore the plugin parameter selector window state + _processor->pluginParameterSelectorState.restoreFromXml(pluginParameterSelectorElement); + } else { + juce::Logger::writeToLog("Missing element " + juce::String(XML_PLUGIN_PARAMETER_SELECTOR_STATE_STR)); + } + + juce::XmlElement* mainWindowElement = element->getChildByName(XML_MAIN_WINDOW_STATE_STR); + if (mainWindowElement != nullptr) { + // Restore the main window state + _restoreMainWindowStateFromXml(mainWindowElement); + } else { + juce::Logger::writeToLog("Missing element " + juce::String(XML_MAIN_WINDOW_STATE_STR)); + } + } else { + juce::Logger::writeToLog("Restore failed - no processor"); + } +} + +void SyndicateAudioProcessor::SplitterParameters::writeToXml(juce::XmlElement* element) { + juce::Logger::writeToLog("Writing plugin state to XML"); + + if (_processor != nullptr) { + // Store the splitter + juce::XmlElement* splitterElement = element->createNewChildElement(XML_SPLITTER_STR); + _writeSplitterToXml(splitterElement); + + // Store the LFOs/envelopes + juce::XmlElement* modulationElement = element->createNewChildElement(XML_MODULATION_SOURCES_STR); + _writeModulationSourcesToXml(modulationElement); + + // Store the macro names + juce::XmlElement* macroNamesElement = element->createNewChildElement(XML_MACRO_NAMES_STR); + _writeMacroNamesToXml(macroNamesElement); + + // Store the metadata + juce::XmlElement* metadataElement = element->createNewChildElement(XML_METADATA_STR); + _writeMetadataToXml(metadataElement); + + // Store window states + juce::XmlElement* mainWindowElement = element->createNewChildElement(XML_MAIN_WINDOW_STATE_STR); + _writeMainWindowStateToXml(mainWindowElement); + + juce::XmlElement* pluginSelectorElement = element->createNewChildElement(XML_PLUGIN_SELECTOR_STATE_STR); + _processor->pluginSelectorState.writeToXml(pluginSelectorElement); + + juce::XmlElement* pluginParameterSelectorElement = element->createNewChildElement(XML_PLUGIN_PARAMETER_SELECTOR_STATE_STR); + _processor->pluginParameterSelectorState.writeToXml(pluginParameterSelectorElement); + } else { + juce::Logger::writeToLog("Writing failed - no processor"); + } +} + +void SyndicateAudioProcessor::SplitterParameters::_restoreSplitterFromXml(juce::XmlElement* element) { + SyndicateAudioProcessor* tmpProcessor = _processor; + + ModelInterface::restoreSplitterFromXml( + _processor->manager, + element, + [tmpProcessor](int id, MODULATION_TYPE type) { return tmpProcessor->getModulationValueForSource(id, type); }, + [tmpProcessor](int newLatencySamples) { tmpProcessor->onLatencyChange(newLatencySamples); }, + {_processor->getBusesLayout(), _processor->getSampleRate(), _processor->getBlockSize()}, + _processor->pluginConfigurator, + _processor->pluginScanClient.getPluginTypes(), + [&](juce::String errorText) { _processor->restoreErrors.push_back(errorText); } + ); +} + +void SyndicateAudioProcessor::SplitterParameters::_restoreModulationSourcesFromXml(juce::XmlElement* element) { + ModelInterface::restoreSourcesFromXml( + _processor->manager, + element, + {_processor->getBusesLayout(), _processor->getSampleRate(), _processor->getBlockSize()} + ); +} + +void SyndicateAudioProcessor::SplitterParameters::_restoreMacroNamesFromXml(juce::XmlElement* element) { + for (int index {0}; index < _processor->macroNames.size(); index++) { + if (element->hasAttribute(getMacroNameXMLName(index))) { + _processor->macroNames[index] = element->getStringAttribute(getMacroNameXMLName(index)); + } else { + juce::Logger::writeToLog("Missing macro name attribute: " + getMacroNameXMLName(index)); + } + } +} + +void SyndicateAudioProcessor::SplitterParameters::_restoreMetadataFromXml(juce::XmlElement* element) { + if (element->hasAttribute(XML_METADATA_NAME_STR)) { + _processor->presetMetadata.name = element->getStringAttribute(XML_METADATA_NAME_STR); + } else { + juce::Logger::writeToLog("Missing attribute " + juce::String(XML_METADATA_NAME_STR)); + } + + if (element->hasAttribute(XML_METADATA_FULLPATH_STR)) { + _processor->presetMetadata.fullPath = element->getStringAttribute(XML_METADATA_FULLPATH_STR); + } else { + juce::Logger::writeToLog("Missing attribute " + juce::String(XML_METADATA_FULLPATH_STR)); + } + + if (element->hasAttribute(XML_METADATA_AUTHOR_STR)) { + _processor->presetMetadata.author = element->getStringAttribute(XML_METADATA_AUTHOR_STR); + } else { + juce::Logger::writeToLog("Missing attribute " + juce::String(XML_METADATA_AUTHOR_STR)); + } + + if (element->hasAttribute(XML_METADATA_DESCRIPTION_STR)) { + _processor->presetMetadata.description = element->getStringAttribute(XML_METADATA_DESCRIPTION_STR); + } else { + juce::Logger::writeToLog("Missing attribute " + juce::String(XML_METADATA_DESCRIPTION_STR)); + } +} + +void SyndicateAudioProcessor::SplitterParameters::_restoreMainWindowStateFromXml(juce::XmlElement* element) { + if (element->hasAttribute(XML_MAIN_WINDOW_BOUNDS_STR)) { + const juce::String boundsString = element->getStringAttribute(XML_MAIN_WINDOW_BOUNDS_STR); + _processor->mainWindowState.bounds = juce::Rectangle::fromString(boundsString); + } else { + juce::Logger::writeToLog("Missing attribute " + juce::String(XML_MAIN_WINDOW_BOUNDS_STR)); + } + + if (element->hasAttribute(XML_GRAPH_VIEW_POSITION_STR)) { + _processor->mainWindowState.graphViewScrollPosition = element->getIntAttribute(XML_GRAPH_VIEW_POSITION_STR); + } else { + juce::Logger::writeToLog("Missing attribute " + juce::String(XML_GRAPH_VIEW_POSITION_STR)); + } + + juce::XmlElement* chainScrollPositionsElement = element->getChildByName(XML_CHAIN_VIEW_POSITIONS_STR); + if (chainScrollPositionsElement != nullptr) { + std::vector chainViewScrollPositions; + + const int numChains {chainScrollPositionsElement->getNumAttributes()}; + for (int index {0}; index < numChains; index++) { + if (chainScrollPositionsElement->hasAttribute(getChainPositionXMLName(index))) { + chainViewScrollPositions.push_back(chainScrollPositionsElement->getIntAttribute(getChainPositionXMLName(index))); + } + } + + _processor->mainWindowState.chainViewScrollPositions = chainViewScrollPositions; + } + + if (element->hasAttribute(XML_LFO_BUTTONS_POSITION_STR)) { + _processor->mainWindowState.lfoButtonsScrollPosition = element->getIntAttribute(XML_LFO_BUTTONS_POSITION_STR); + } else { + juce::Logger::writeToLog("Missing attribute " + juce::String(XML_LFO_BUTTONS_POSITION_STR)); + } + + if (element->hasAttribute(XML_ENV_BUTTONS_POSITION_STR)) { + _processor->mainWindowState.envButtonsScrollPosition = element->getIntAttribute(XML_ENV_BUTTONS_POSITION_STR); + } else { + juce::Logger::writeToLog("Missing attribute " + juce::String(XML_ENV_BUTTONS_POSITION_STR)); + } + + juce::XmlElement* selectedSourceElement = element->getChildByName(XML_SELECTED_SOURCE_STR); + if (selectedSourceElement != nullptr) { + _processor->mainWindowState.selectedModulationSource = ModulationSourceDefinition(1, MODULATION_TYPE::LFO); + _processor->mainWindowState.selectedModulationSource.value().restoreFromXml(selectedSourceElement); + } else { + juce::Logger::writeToLog("Missing element " + juce::String(XML_SELECTED_SOURCE_STR)); + } +} + +void SyndicateAudioProcessor::SplitterParameters::_writeSplitterToXml(juce::XmlElement* element) { + ModelInterface::writeSplitterToXml(_processor->manager, element); +} + +void SyndicateAudioProcessor::SplitterParameters::_writeModulationSourcesToXml(juce::XmlElement* element) { + ModelInterface::writeSourcesToXml(_processor->manager, element); +} + +void SyndicateAudioProcessor::SplitterParameters::_writeMacroNamesToXml(juce::XmlElement* element) { + for (int index {0}; index < _processor->macroNames.size(); index++) { + element->setAttribute(getMacroNameXMLName(index), _processor->macroNames[index]); + } +} + +void SyndicateAudioProcessor::SplitterParameters::_writeMetadataToXml(juce::XmlElement* element) { + element->setAttribute(XML_METADATA_NAME_STR, _processor->presetMetadata.name); + element->setAttribute(XML_METADATA_FULLPATH_STR, _processor->presetMetadata.fullPath); + element->setAttribute(XML_METADATA_AUTHOR_STR, _processor->presetMetadata.author); + element->setAttribute(XML_METADATA_DESCRIPTION_STR, _processor->presetMetadata.description); +} + +void SyndicateAudioProcessor::SplitterParameters::_writeMainWindowStateToXml(juce::XmlElement* element) { + // Store the main window bounds + element->setAttribute(XML_MAIN_WINDOW_BOUNDS_STR, _processor->mainWindowState.bounds.toString()); + + // Store scroll positions + element->setAttribute(XML_GRAPH_VIEW_POSITION_STR, _processor->mainWindowState.graphViewScrollPosition); + + juce::XmlElement* chainScrollPositionsElement = element->createNewChildElement(XML_CHAIN_VIEW_POSITIONS_STR); + for (int index {0}; index < _processor->mainWindowState.chainViewScrollPositions.size(); index++) { + chainScrollPositionsElement->setAttribute( + getChainPositionXMLName(index), _processor->mainWindowState.chainViewScrollPositions[index] + ); + } + + element->setAttribute(XML_LFO_BUTTONS_POSITION_STR, _processor->mainWindowState.lfoButtonsScrollPosition); + element->setAttribute(XML_ENV_BUTTONS_POSITION_STR, _processor->mainWindowState.envButtonsScrollPosition); + + if (_processor->mainWindowState.selectedModulationSource.has_value()) { + juce::XmlElement* selectedSourceElement = element->createNewChildElement(XML_SELECTED_SOURCE_STR); + _processor->mainWindowState.selectedModulationSource.value().writeToXml(selectedSourceElement); + } +} + +//============================================================================== +// This creates new instances of the plugin.. +juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter() +{ + return new SyndicateAudioProcessor(); +} diff --git a/ports-juce7/syndicate/Syndicate/PluginProcessor.h b/ports-juce7/syndicate/Syndicate/PluginProcessor.h new file mode 100644 index 00000000..7c6eec39 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/PluginProcessor.h @@ -0,0 +1,264 @@ +/* + ============================================================================== + + This file contains the basic framework code for a JUCE plugin processor. + + ============================================================================== +*/ + +#pragma once + +#include + +#include "CoreJUCEPlugin/CoreAudioProcessor.h" +#include "CoreJUCEPlugin/CustomParameter.h" +#include "MainLogger.h" +#include "NullLogger.hpp" +#include "PluginScanClient.h" +#include "PluginSelectorState.h" +#include "PluginParameterSelectorState.h" +#include "ParameterData.h" +#include "PluginConfigurator.hpp" +#include "ModelInterface.hpp" +#include "PresetMetadata.hpp" + +class SyndicateAudioProcessorEditor; + +struct MainWindowState { + juce::Rectangle bounds; + int graphViewScrollPosition; + std::vector chainViewScrollPositions; + int lfoButtonsScrollPosition; + int envButtonsScrollPosition; + int rndButtonsScrollPosition; + std::optional selectedModulationSource; + + MainWindowState() : bounds(0, 0, 860, 620), + graphViewScrollPosition(0), + lfoButtonsScrollPosition(0), + envButtonsScrollPosition(0) {} +}; + +//============================================================================== +/** +*/ +class SyndicateAudioProcessor : public WECore::JUCEPlugin::CoreAudioProcessor +{ +public: + PluginScanClient pluginScanClient; + PluginSelectorState pluginSelectorState; // TODO convert this to a custom parameter + PluginParameterSelectorState pluginParameterSelectorState; + ModelInterface::StateManager manager; + PluginConfigurator pluginConfigurator; + std::array macroNames; + std::array meterEnvelopes; + std::vector restoreErrors; // Populated during restore, displayed and cleared when the UI is opened + juce::AudioPluginFormatManager formatManager; + MainWindowState mainWindowState; + PresetMetadata presetMetadata; + + //============================================================================== + SyndicateAudioProcessor(); + ~SyndicateAudioProcessor() override; + + //============================================================================== + void prepareToPlay (double sampleRate, int samplesPerBlock) override; + void releaseResources() override; + void reset() override; + + bool isBusesLayoutSupported (const BusesLayout& layouts) const override; + + void processBlock (juce::AudioBuffer&, juce::MidiBuffer&) override; + + //============================================================================== + juce::AudioProcessorEditor* createEditor() override; + bool hasEditor() const override; + + //============================================================================== + const juce::String getName() const override; + + bool acceptsMidi() const override; + bool producesMidi() const override; + bool isMidiEffect() const override; + double getTailLengthSeconds() const override; + + //============================================================================== + int getNumPrograms() override; + int getCurrentProgram() override; + void setCurrentProgram (int index) override; + const juce::String getProgramName (int index) override; + void changeProgramName (int index, const juce::String& newName) override; + + // Public parameters + juce::AudioParameterFloat* outputGainLog; + juce::AudioParameterFloat* outputPan; + std::array macros; + + void setEditor(SyndicateAudioProcessorEditor* editor) { _editor = editor; } + void removeEditor() { _editor = nullptr; } + + // Sources + void addLfo(); + void setLfoTempoSyncSwitch(int lfoIndex, bool val); + void setLfoInvertSwitch(int lfoIndex, bool val); + void setLfoOutputMode(int lfoIndex, int val); + void setLfoWave(int lfoIndex, int val); + void setLfoTempoNumer(int lfoIndex, int val); + void setLfoTempoDenom (int lfoIndex, int val); + void setLfoFreq(int lfoIndex, double val); + void setLfoDepth(int lfoIndex, double val); + void setLfoManualPhase(int lfoIndex, double val); + + void addEnvelope(); + void setEnvAttackTimeMs(int envIndex, double val); + void setEnvReleaseTimeMs(int envIndex, double val); + void setEnvFilterEnabled(int envIndex, bool val); + void setEnvFilterHz(int envIndex, double lowCut, double highCut); + void setEnvAmount(int envIndex, float val); + void setEnvUseSidechainInput(int envIndex, bool val); + + void addRandomSource(); + void setRandomOutputMode(int randomIndex, int val); + void setRandomFreq(int randomIndex, double val); + void setRandomDepth(int randomIndex, double val); + + float getModulationValueForSource(int id, MODULATION_TYPE type); + void removeModulationSource(ModulationSourceDefinition definition); + + void setSplitType(SPLIT_TYPE splitType); + SPLIT_TYPE getSplitType() { return ModelInterface::getSplitType(manager); } + + void setChainBypass(int chainNumber, bool val); + void setChainMute(int chainNumber, bool val); + void setChainSolo(int chainNumber, bool val); + void setChainCustomName(int chainNumber, const juce::String& name); + + void setSlotBypass(int chainNumber, int positionInChain, bool bypass); + void setSlotGainLinear(int chainNumber, int positionInChain, float gain); + void setSlotPan(int chainNumber, int positionInChain, float pan); + + // Modulation tray + void setPluginModulationIsActive(int chainNumber, int pluginNumber, bool val); + void setModulationTarget(int chainNumber, int pluginNumber, int targetNumber, juce::String targetName); + void removeModulationTarget(int chainNumber, int pluginNumber, int targetNumber); + void addModulationSourceToTarget(int chainNumber, int pluginNumber, int targetNumber, ModulationSourceDefinition source); + void removeModulationSourceFromTarget(int chainNumber, int pluginNumber, int targetNumber, ModulationSourceDefinition source); + void setModulationTargetValue(int chainNumber, int pluginNumber, int targetNumber, float val); + void setModulationSourceValue(int chainNumber, int pluginNumber, int targetNumber, int sourceNumber, float val); + + // Parameter modulation + void addSourceToLFOFreq(int lfoIndex, ModulationSourceDefinition source); + void removeSourceFromLFOFreq(int lfoIndex, ModulationSourceDefinition source); + void setLFOFreqModulationAmount(int lfoIndex, int sourceIndex, double val); + + void addSourceToLFODepth(int lfoIndex, ModulationSourceDefinition source); + void removeSourceFromLFODepth(int lfoIndex, ModulationSourceDefinition source); + void setLFODepthModulationAmount(int lfoIndex, int sourceIndex, double val); + + void addSourceToLFOPhase(int lfoIndex, ModulationSourceDefinition source); + void removeSourceFromLFOPhase(int lfoIndex, ModulationSourceDefinition source); + void setLFOPhaseModulationAmount(int lfoIndex, int sourceIndex, double val); + + void addSourceToRandomFreq(int randomIndex, ModulationSourceDefinition source); + void removeSourceFromRandomFreq(int randomIndex, ModulationSourceDefinition source); + void setRandomFreqModulationAmount(int randomIndex, int sourceIndex, double val); + + void addSourceToRandomDepth(int randomIndex, ModulationSourceDefinition source); + void removeSourceFromRandomDepth(int randomIndex, ModulationSourceDefinition source); + void setRandomDepthModulationAmount(int randomIndex, int sourceIndex, double val); + + // Parallel Split + void addParallelChain(); + void removeParallelChain(int chainNumber); + + // Multiband Split + void addCrossoverBand(); + void removeCrossoverBand(int bandNumber); + void setCrossoverFrequency(size_t index, float val); + + // Plugin events + bool onPluginSelectedByUser(std::shared_ptr plugin, + int chainNumber, + int pluginNumber); + + void removePlugin(int chainNumber, int pluginNumber); + + void insertGainStage(int chainNumber, int pluginNumber); + + void copySlot(int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber); + + void moveSlot(int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber); + + void moveChain(int fromChainNumber, int toChainNumber); + + void copyChain(int fromChainNumber, int toChainNumber); + + void resetAllState(); + + void undo(); + void redo(); + + void setPresetMetadata(const PresetMetadata& newMetadata); + + /** + * Called by a splitter when its latency has changed, so this processor can update the latency + * it reports back to the host. + */ + void onLatencyChange(int newLatencySamples); + + /** + * Override so we can disable conventional save/restore in the demo but still allow it manually using the + * import/export buttons. + */ + virtual void getStateInformation(juce::MemoryBlock& destData) override; + virtual void setStateInformation(const void* data, int sizeInBytes) override; + +private: + /** + * Provides a way for the processor to trigger UI updates, and also manages saving and restoring + * parameter state for the splitter. + */ + class SplitterParameters : public WECore::JUCEPlugin::CustomParameter { + public: + SplitterParameters() : _processor(nullptr) { } + ~SplitterParameters() = default; + + void triggerUpdate() { _updateListener(); } + + void setProcessor(SyndicateAudioProcessor* processor) { _processor = processor; } + + void restoreFromXml(juce::XmlElement* element) override; + void writeToXml(juce::XmlElement* element) override; + + private: + SyndicateAudioProcessor* _processor; + + void _restoreSplitterFromXml(juce::XmlElement* element); + void _restoreModulationSourcesFromXml(juce::XmlElement* element); + void _restoreMacroNamesFromXml(juce::XmlElement* element); + void _restoreMetadataFromXml(juce::XmlElement* element); + void _restoreMainWindowStateFromXml(juce::XmlElement* element); + + void _writeSplitterToXml(juce::XmlElement* element); + void _writeModulationSourcesToXml(juce::XmlElement* element); + void _writeMacroNamesToXml(juce::XmlElement* element); + void _writeMetadataToXml(juce::XmlElement* element); + void _writeMainWindowStateToXml(juce::XmlElement* element); + }; + + std::unique_ptr _fileLogger; + NullLogger _nullLogger; + SyndicateAudioProcessorEditor* _editor; + double _outputGainLinear; + + SplitterParameters* _splitterParameters; + + + std::vector _provideParamNamesForMigration() override; + void _migrateParamValues(std::vector& paramValues) override; + + void _onParameterUpdate() override; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SyndicateAudioProcessor) +}; diff --git a/ports-juce7/syndicate/Syndicate/PresetMetadata.hpp b/ports-juce7/syndicate/Syndicate/PresetMetadata.hpp new file mode 100644 index 00000000..abd31a6f --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/PresetMetadata.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +struct PresetMetadata { + juce::String name; + juce::String fullPath; + juce::String author; + juce::String description; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButton.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButton.cpp new file mode 100644 index 00000000..725181cc --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButton.cpp @@ -0,0 +1,57 @@ +#include "ChainButton.h" +#include "UIUtils.h" + +void ChainButtonLookAndFeel::drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& /*backgroundColour*/, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + const juce::Rectangle area = button.getLocalBounds().reduced(1, 1).toFloat(); + + if (button.getToggleState()) { + g.setColour(button.findColour(highlightColour)); + } else { + g.setColour(button.findColour(backgroundColour)); + } + g.fillEllipse(area); +} + +void ChainButtonLookAndFeel::drawButtonText(juce::Graphics& g, + juce::TextButton& button, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + + if (!button.isEnabled()) { + g.setColour(button.findColour(disabledColour)); + } else if (button.getToggleState()) { + g.setColour(button.findColour(backgroundColour)); + } else { + g.setColour(button.findColour(highlightColour)); + } + + g.drawText(button.getButtonText(), button.getLocalBounds().reduced(2), juce::Justification::centred, false); +} + +ChainButton::ChainButton(CHAIN_BUTTON_TYPE type) { + setLookAndFeel(&_lookAndFeel); + + setColour(ChainButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + setColour(ChainButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + setColour(ChainButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + + switch (type) { + case CHAIN_BUTTON_TYPE::BYPASS: + setButtonText("B"); + break; + case CHAIN_BUTTON_TYPE::MUTE: + setButtonText("M"); + break; + case CHAIN_BUTTON_TYPE::SOLO: + setButtonText("S"); + break; + } +} + +ChainButton::~ChainButton() { + setLookAndFeel(nullptr); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButton.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButton.h new file mode 100644 index 00000000..7a5ce1e5 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButton.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include "CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h" + +enum class CHAIN_BUTTON_TYPE { + BYPASS, + MUTE, + SOLO +}; + +class ChainButtonLookAndFeel : public WECore::LookAndFeelMixins::WEV2LookAndFeel { +public: + enum ColourIds { + backgroundColour, + highlightColour, + disabledColour + }; + + ChainButtonLookAndFeel() = default; + virtual ~ChainButtonLookAndFeel() = default; + + virtual void drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + + virtual void drawButtonText(juce::Graphics& g, + juce::TextButton& button, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; +}; + +class ChainButton : public juce::TextButton { +public: + ChainButton(CHAIN_BUTTON_TYPE type); + virtual ~ChainButton(); + +private: + ChainButtonLookAndFeel _lookAndFeel; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButtonsComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButtonsComponent.cpp new file mode 100644 index 00000000..62d8ed3b --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButtonsComponent.cpp @@ -0,0 +1,157 @@ +#include "ChainButtonsComponent.h" +#include "ModelInterface.hpp" + +ChainButtonsComponent::ChainButtonsComponent(SyndicateAudioProcessor& processor, + int chainNumber, + const juce::String& defaultName) : + _processor(processor), _chainNumber(chainNumber), _defaultName(defaultName) { + chainLabel.reset(new juce::Label("Chain Label", TRANS(""))); + addAndMakeVisible(chainLabel.get()); + chainLabel->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle("Regular")); + chainLabel->setJustificationType(juce::Justification::centred); + chainLabel->setEditable(true, true, false); + chainLabel->setColour(juce::Label::backgroundColourId, juce::Colours::transparentBlack); + chainLabel->setColour(juce::Label::textColourId, UIUtils::highlightColour); + chainLabel->setColour(juce::Label::outlineColourId, juce::Colours::transparentBlack); + chainLabel->setColour(juce::Label::backgroundWhenEditingColourId, UIUtils::backgroundColour); + chainLabel->setColour(juce::Label::textWhenEditingColourId, UIUtils::highlightColour); + chainLabel->setColour(juce::Label::outlineWhenEditingColourId, juce::Colours::transparentBlack); + chainLabel->setColour(juce::TextEditor::highlightColourId, UIUtils::highlightColour); + chainLabel->setColour(juce::TextEditor::highlightedTextColourId, UIUtils::neutralColour); + chainLabel->setColour(juce::CaretComponent::caretColourId, UIUtils::highlightColour); + chainLabel->onTextChange = [this] () { + _processor.setChainCustomName(_chainNumber, chainLabel->getText()); + }; + + secondaryLabel.reset(new juce::Label("Chain Label", TRANS(""))); + secondaryLabel->setFont(juce::Font(12.00f, juce::Font::plain).withTypefaceStyle("Regular")); + secondaryLabel->setJustificationType(juce::Justification::centred); + secondaryLabel->setColour(juce::Label::backgroundColourId, juce::Colours::transparentBlack); + secondaryLabel->setColour(juce::Label::textColourId, UIUtils::highlightColour.withBrightness(0.7)); + secondaryLabel->setColour(juce::Label::outlineColourId, juce::Colours::transparentBlack); + addAndMakeVisible(secondaryLabel.get()); + secondaryLabel->toBehind(chainLabel.get()); + + _setLabelsText(); + + dragHandle.reset(new UIUtils::DragHandle()); + addAndMakeVisible(dragHandle.get()); + dragHandle->setColour(UIUtils::DragHandle::handleColourId, UIUtils::highlightColour); + dragHandle->setTooltip(TRANS("Drag to move this chain to another position - hold " + UIUtils::getCopyKeyName() + " key to copy")); + dragHandle->addMouseListener(this, false); + + bypassBtn.reset(new ChainButton(CHAIN_BUTTON_TYPE::BYPASS)); + addAndMakeVisible(bypassBtn.get()); + bypassBtn->setTooltip("Bypass this chain"); + bypassBtn->addMouseListener(this, false); + bypassBtn->onClick = [&processor, chainNumber] () { + processor.setChainBypass(chainNumber, !ModelInterface::getChainBypass(processor.manager, chainNumber)); + }; + + muteBtn.reset(new ChainButton(CHAIN_BUTTON_TYPE::MUTE)); + addAndMakeVisible(muteBtn.get()); + muteBtn->setTooltip("Mute this chain"); + muteBtn->addMouseListener(this, false); + muteBtn->onClick = [&processor, chainNumber] () { + processor.setChainMute(chainNumber, !ModelInterface::getChainMute(processor.manager, chainNumber)); + }; + + soloBtn.reset(new ChainButton(CHAIN_BUTTON_TYPE::SOLO)); + addAndMakeVisible(soloBtn.get()); + soloBtn->setTooltip("Solo this chain"); + soloBtn->addMouseListener(this, false); + soloBtn->onClick = [&processor, chainNumber] () { + processor.setChainSolo(chainNumber, !ModelInterface::getChainSolo(processor.manager, chainNumber)); + }; + + removeButton.reset(new UIUtils::CrossButton("Remove Button")); + addAndMakeVisible(removeButton.get()); + removeButton->setTooltip("Remove this chain"); + removeButton->setColour(UIUtils::CrossButton::enabledColour, UIUtils::highlightColour); + removeButton->setColour(UIUtils::CrossButton::disabledColour, UIUtils::deactivatedColour); + removeButton->addMouseListener(this, false); + removeButton->setEnabled(false); + removeButton->onClick = [this] () { + _removeChainCallback(); + }; +} + +ChainButtonsComponent::ChainButtonsComponent(SyndicateAudioProcessor& processor, + int chainNumber, + const juce::String& defaultName, + std::function removeChainCallback) : + ChainButtonsComponent(processor, chainNumber, defaultName) { + _removeChainCallback = removeChainCallback; + removeButton->setEnabled(true); +} + +ChainButtonsComponent::~ChainButtonsComponent() { + chainLabel = nullptr; + secondaryLabel = nullptr; + dragHandle = nullptr; + bypassBtn = nullptr; + muteBtn = nullptr; + soloBtn = nullptr; + removeButton = nullptr; +} + +void ChainButtonsComponent::resized() { + juce::Rectangle availableArea = getLocalBounds(); + + const juce::Rectangle topArea = availableArea.removeFromTop(availableArea.getHeight() / 2); + chainLabel->setBounds(topArea); + secondaryLabel->setBounds(topArea.withHeight(12)); + + constexpr int BUTTON_ROW_MARGIN {5}; + availableArea = availableArea.reduced(BUTTON_ROW_MARGIN, 0); + + constexpr int DRAG_HANDLE_WIDTH {22}; + constexpr int BAND_BUTTON_WIDTH {21}; + + juce::FlexBox flexBox; + flexBox.flexWrap = juce::FlexBox::Wrap::wrap; + flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + flexBox.alignContent = juce::FlexBox::AlignContent::center; + flexBox.items.add(juce::FlexItem(*dragHandle.get()).withMinWidth(DRAG_HANDLE_WIDTH).withMinHeight(DRAG_HANDLE_WIDTH)); + flexBox.items.add(juce::FlexItem(*bypassBtn.get()).withMinWidth(BAND_BUTTON_WIDTH).withMinHeight(BAND_BUTTON_WIDTH)); + flexBox.items.add(juce::FlexItem(*muteBtn.get()).withMinWidth(BAND_BUTTON_WIDTH).withMinHeight(BAND_BUTTON_WIDTH)); + flexBox.items.add(juce::FlexItem(*soloBtn.get()).withMinWidth(BAND_BUTTON_WIDTH).withMinHeight(BAND_BUTTON_WIDTH)); + flexBox.items.add(juce::FlexItem(*removeButton.get()).withMinWidth(BAND_BUTTON_WIDTH).withMinHeight(BAND_BUTTON_WIDTH)); + flexBox.performLayout(availableArea.toFloat()); +} + +void ChainButtonsComponent::refresh() { + _setLabelsText(); + bypassBtn->setToggleState(ModelInterface::getChainBypass(_processor.manager, _chainNumber), juce::dontSendNotification); + muteBtn->setToggleState(ModelInterface::getChainMute(_processor.manager, _chainNumber), juce::dontSendNotification); + soloBtn->setToggleState(ModelInterface::getChainSolo(_processor.manager, _chainNumber), juce::dontSendNotification); +} + +void ChainButtonsComponent::mouseDrag(const juce::MouseEvent& e) { + if (dragHandle != nullptr && e.originalComponent == dragHandle.get()) { + juce::DragAndDropContainer* container = juce::DragAndDropContainer::findParentDragContainerFor(this); + + if (container != nullptr) { + juce::var details; + details.append(_chainNumber); + + // This is a copy if alt is down, otherwise move + details.append(juce::ModifierKeys::currentModifiers.isAltDown()); + + container->startDragging(details, this); + } + } +} + +void ChainButtonsComponent::_setLabelsText() { + juce::String customName = ModelInterface::getChainCustomName(_processor.manager, _chainNumber); + + if (customName.isEmpty()) { + chainLabel->setText(_defaultName, juce::dontSendNotification); + secondaryLabel->setVisible(false); + } else { + chainLabel->setText(customName, juce::dontSendNotification); + secondaryLabel->setText(_defaultName, juce::dontSendNotification); + secondaryLabel->setVisible(true); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButtonsComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButtonsComponent.h new file mode 100644 index 00000000..7d0dc8a7 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ChainButtonsComponent.h @@ -0,0 +1,35 @@ +#pragma once + +#include "ChainButton.h" +#include "UIUtils.h" +#include "PluginProcessor.h" + +class ChainButtonsComponent : public juce::Component { +public: + ChainButtonsComponent(SyndicateAudioProcessor& processor, int chainNumber, const juce::String& defaultName); + ChainButtonsComponent(SyndicateAudioProcessor& processor, int chainNumber, const juce::String& defaultName, std::function removeChainCallback); + virtual ~ChainButtonsComponent(); + + void resized() override; + void mouseDrag(const juce::MouseEvent& e) override; + + void refresh(); + + std::unique_ptr chainLabel; + std::unique_ptr secondaryLabel; + std::unique_ptr dragHandle; + std::unique_ptr bypassBtn; + std::unique_ptr muteBtn; + std::unique_ptr soloBtn; + std::unique_ptr removeButton; + +private: + SyndicateAudioProcessor& _processor; + int _chainNumber; + const juce::String _defaultName; + std::function _removeChainCallback; + + void _setLabelsText(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ChainButtonsComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.cpp new file mode 100644 index 00000000..84008aae --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.cpp @@ -0,0 +1,49 @@ +#include "CrossoverImagerComponent.h" +#include "UIUtils.h" +#include "UIUtilsCrossover.h" +#include "ModelInterface.hpp" + +namespace { + int dBToYPos(float dBValue, int crossoverHeight) { + constexpr float MIN_DB {-60}; + constexpr float MAX_DB {20}; + constexpr float range = MAX_DB - MIN_DB; + + return crossoverHeight - std::max(((std::min(dBValue, MAX_DB) - MIN_DB) / range) * crossoverHeight, 0.0f); + } +} + +CrossoverImagerComponent::CrossoverImagerComponent(SyndicateAudioProcessor& processor) + : _processor(processor) { + start(); +} + +void CrossoverImagerComponent::paint(juce::Graphics& g) { + _stopEvent.reset(); + + g.fillAll(UIUtils::backgroundColour); + + auto [fftBuffer, binWidth] = ModelInterface::getFFTOutputs(_processor.manager); + + // Draw a line to each point in the FFT + juce::Path p; + + for (int index {0}; index < fftBuffer.size(); index++) { + const float binCentreFreq {binWidth * index + binWidth / 2}; + const int XPos {static_cast(UIUtils::Crossover::sliderValueToXPos(binCentreFreq, getWidth()))}; + + const float leveldB {static_cast(WECore::CoreMath::linearTodB(fftBuffer[index]))}; + const int YPos {dBToYPos(leveldB, getHeight())}; + + if (index == 0) { + p.startNewSubPath(XPos, YPos); + } else { + p.lineTo(XPos, YPos); + } + } + + g.setColour(UIUtils::highlightColour); + g.strokePath(p, juce::PathStrokeType(0.5f)); + + _stopEvent.signal(); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.h new file mode 100644 index 00000000..fbda14f7 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include "PluginProcessor.h" +#include "UIUtils.h" + +/** + * Draws the calculated width for each band behind the crossover controls. + */ +class CrossoverImagerComponent : public UIUtils::SafeAnimatedComponent { +public: + CrossoverImagerComponent(SyndicateAudioProcessor& processor); + + void paint(juce::Graphics& g) override; + +private: + SyndicateAudioProcessor& _processor; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.cpp new file mode 100644 index 00000000..f04673b6 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.cpp @@ -0,0 +1,84 @@ +#include "CrossoverMouseListener.h" + +#include + +#include "UIUtilsCrossover.h" +#include "WEFilters/StereoWidthProcessorParameters.h" + +namespace { + std::function createDragCallback(SyndicateAudioProcessor& processor, int crossoverNumber) { + return [&processor, crossoverNumber](const juce::MouseEvent& event) { + const int currentXPos {event.getPosition().getX()}; + const int componentWidth {event.eventComponent->getWidth()}; + + processor.setCrossoverFrequency(crossoverNumber, UIUtils::Crossover::XPosToSliderValue(currentXPos, componentWidth)); + + // Check if this crossover handle is getting too close to another and move it if + // needed + const size_t numCrossovers {ModelInterface::getNumChains(processor.manager) - 1}; + for (int otherCrossoverNumber {0}; otherCrossoverNumber < numCrossovers; otherCrossoverNumber++) { + const double otherXPos { + UIUtils::Crossover::sliderValueToXPos(ModelInterface::getCrossoverFrequency(processor.manager, otherCrossoverNumber), componentWidth) + }; + + constexpr double MIN_SPACING {4 * UIUtils::Crossover::SLIDER_THUMB_RADIUS}; + const double requiredGap {MIN_SPACING * std::abs(crossoverNumber - otherCrossoverNumber)}; + const double actualGap {std::abs(currentXPos - otherXPos)}; + + if (otherCrossoverNumber < crossoverNumber && actualGap < requiredGap) { + processor.setCrossoverFrequency(otherCrossoverNumber, UIUtils::Crossover::XPosToSliderValue(currentXPos - requiredGap, componentWidth)); + } else if (otherCrossoverNumber > crossoverNumber && actualGap < requiredGap) { + processor.setCrossoverFrequency(otherCrossoverNumber, UIUtils::Crossover::XPosToSliderValue(currentXPos + requiredGap, componentWidth)); + } + } + }; + } +} + +CrossoverMouseListener::CrossoverMouseListener(SyndicateAudioProcessor& processor) + : _processor(processor) { +} + +CrossoverMouseListener::~CrossoverMouseListener() { +} + +void CrossoverMouseListener::mouseDown(const juce::MouseEvent& event) { + _resolveParameterInteraction(event); +} + +void CrossoverMouseListener::mouseDrag(const juce::MouseEvent& event) { + if (_dragCallback.has_value()) { + _dragCallback.value()(event); + } +} + +void CrossoverMouseListener::mouseUp(const juce::MouseEvent& /*event*/) { + if (_dragCallback.has_value()) { + _dragCallback.reset(); + } +} + +void CrossoverMouseListener::_resolveParameterInteraction(const juce::MouseEvent& event) { + const int mouseDownX {event.getMouseDownX()}; + + // For each available band, check if the cursor landed on a crossover frequency handle or on + // the gaps in between + const size_t numBands {ModelInterface::getNumChains(_processor.manager)}; + + for (size_t bandIndex {0}; bandIndex < numBands; bandIndex++) { + const double crossoverXPos {bandIndex < numBands - 1 ? + UIUtils::Crossover::sliderValueToXPos(ModelInterface::getCrossoverFrequency(_processor.manager, bandIndex), event.eventComponent->getWidth()) : + event.eventComponent->getWidth() + }; + + if (mouseDownX < crossoverXPos - UIUtils::Crossover::SLIDER_THUMB_TARGET_WIDTH) { + // Click landed below a crossover handle + break; + + } else if (mouseDownX < crossoverXPos + UIUtils::Crossover::SLIDER_THUMB_TARGET_WIDTH) { + // Click landed on a crossover handle + _dragCallback = createDragCallback(_processor, bandIndex); + break; + } + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.h new file mode 100644 index 00000000..1d767ae3 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include "JuceHeader.h" +#include "PluginProcessor.h" +#include "ParameterData.h" +#include "MONSTRFilters/MONSTRParameters.h" + +class CrossoverMouseListener : public juce::MouseListener { +public: + + CrossoverMouseListener(SyndicateAudioProcessor& processor); + virtual ~CrossoverMouseListener(); + + void mouseDown(const juce::MouseEvent& event) override; + + void mouseDrag(const juce::MouseEvent& event) override; + + void mouseUp(const juce::MouseEvent& event) override; + +private: + SyndicateAudioProcessor& _processor; + std::optional> _dragCallback; + + /** + * If the mouse event occured inside a button the function will handle it and return null, + * if the event occured inside a slider it will return the corresponding + * FloatParameterInteraction for it to be handled by the appropriate event handlers. + */ + void _resolveParameterInteraction(const juce::MouseEvent& event); +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.cpp new file mode 100644 index 00000000..d6be3431 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.cpp @@ -0,0 +1,152 @@ +#include "CrossoverParameterComponent.h" + +#include +#include "WEFilters/StereoWidthProcessorParameters.h" +#include "UIUtilsCrossover.h" +#include "General/CoreMath.h" + +namespace { + constexpr int FREQUENCY_TEXT_HEIGHT {16}; + constexpr int FREQUENCY_TEXT_MARGIN {2}; +} + +CrossoverParameterComponent::CrossoverParameterComponent(SyndicateAudioProcessor& processor) + : _processor(processor) { + _rebuildChainLabels(); + _setLabelsText(); +} + +void CrossoverParameterComponent::paint(juce::Graphics &g) { + _drawSliderThumbs(g); + _drawFrequencyText(g); +} + +void CrossoverParameterComponent::resized() { + _resizeChainLabels(); +} + +void CrossoverParameterComponent::onNumChainsChanged() { + _rebuildChainLabels(); + _setLabelsText(); + resized(); +} + +void CrossoverParameterComponent::_drawSliderThumbs(juce::Graphics& g) { + ModelInterface::forEachCrossover(_processor.manager, [&](float crossoverFrequency) { + const double crossoverXPos { + UIUtils::Crossover::sliderValueToXPos(crossoverFrequency, getWidth()) + }; + + const int lineLength {getHeight() / 2 - FREQUENCY_TEXT_HEIGHT / 2 - FREQUENCY_TEXT_MARGIN}; + + juce::Path p; + p.startNewSubPath(crossoverXPos, 0); + p.lineTo(crossoverXPos, lineLength); + + p.startNewSubPath(crossoverXPos, getHeight() - lineLength); + p.lineTo(crossoverXPos, getHeight()); + + g.setColour(UIUtils::highlightColour); + g.strokePath(p, juce::PathStrokeType(0.5f)); + }); +} + +void CrossoverParameterComponent::_drawFrequencyText(juce::Graphics &g) { + constexpr int WIDTH {100}; + const int yPos {getHeight() / 2 - FREQUENCY_TEXT_HEIGHT / 2}; + + g.setColour(UIUtils::highlightColour); + + ModelInterface::forEachCrossover(_processor.manager, [&](float crossoverFrequency) { + + const double crossoverXPos { + UIUtils::Crossover::sliderValueToXPos(crossoverFrequency, getWidth()) + }; + + g.drawText( + juce::String(static_cast(crossoverFrequency)) + " Hz", + crossoverXPos - WIDTH / 2, + yPos, + WIDTH, + FREQUENCY_TEXT_HEIGHT, + juce::Justification::centred, + false); + }); +} + +void CrossoverParameterComponent::_rebuildChainLabels() { + _chainLabels.clear(); + _secondaryLabels.clear(); + + ModelInterface::forEachChain(_processor.manager, [&](int bandNumber, std::shared_ptr) { + _chainLabels.emplace_back(std::make_unique("Chain Label", TRANS(""))); + addAndMakeVisible(_chainLabels.back().get()); + _chainLabels.back()->setText(juce::String(bandNumber + 1), juce::dontSendNotification); + _chainLabels.back()->setJustificationType(juce::Justification::centred); + _chainLabels.back()->setEditable(true, true, false); + _chainLabels.back()->setColour(juce::Label::backgroundColourId, juce::Colours::transparentBlack); + _chainLabels.back()->setColour(juce::Label::textColourId, UIUtils::highlightColour); + _chainLabels.back()->setColour(juce::Label::outlineColourId, juce::Colours::transparentBlack); + _chainLabels.back()->setColour(juce::Label::backgroundWhenEditingColourId, UIUtils::backgroundColour); + _chainLabels.back()->setColour(juce::Label::textWhenEditingColourId, UIUtils::highlightColour); + _chainLabels.back()->setColour(juce::Label::outlineWhenEditingColourId, juce::Colours::transparentBlack); + _chainLabels.back()->setColour(juce::TextEditor::highlightColourId, UIUtils::highlightColour); + _chainLabels.back()->setColour(juce::TextEditor::highlightedTextColourId, UIUtils::neutralColour); + _chainLabels.back()->setColour(juce::CaretComponent::caretColourId, UIUtils::highlightColour); + + auto chainLabel = _chainLabels.back().get(); + _chainLabels.back()->onTextChange = [this, bandNumber, chainLabel] () { + _processor.setChainCustomName(bandNumber, chainLabel->getText()); + }; + + _secondaryLabels.emplace_back(std::make_unique("Secondary Label", TRANS(""))); + addAndMakeVisible(_secondaryLabels.back().get()); + _secondaryLabels.back()->setFont(juce::Font(12.00f, juce::Font::plain).withTypefaceStyle("Regular")); + _secondaryLabels.back()->setJustificationType(juce::Justification::centred); + _secondaryLabels.back()->setColour(juce::Label::backgroundColourId, juce::Colours::transparentBlack); + _secondaryLabels.back()->setColour(juce::Label::textColourId, UIUtils::highlightColour.withBrightness(0.7)); + _secondaryLabels.back()->setColour(juce::Label::outlineColourId, juce::Colours::transparentBlack); + _secondaryLabels.back()->toBehind(_chainLabels.back().get()); + }); +} + +void CrossoverParameterComponent::_resizeChainLabels() { + double xPosLeft {0}; + + for (size_t index {0}; index < _chainLabels.size(); index++) { + const double xPosRight { + index < _chainLabels.size() - 1 ? + UIUtils::Crossover::sliderValueToXPos(ModelInterface::getCrossoverFrequency(_processor.manager, index), getWidth()) : + getWidth() + }; + + _chainLabels[index]->setBounds( + xPosLeft + UIUtils::Crossover::SLIDER_THUMB_TARGET_WIDTH, + 0, + xPosRight - xPosLeft - UIUtils::Crossover::SLIDER_THUMB_TARGET_WIDTH * 2, // * 2 to subtract the space added on the left and then make space on the right + getHeight()); + + _secondaryLabels[index]->setBounds( + xPosLeft + UIUtils::Crossover::SLIDER_THUMB_TARGET_WIDTH, + 0, + xPosRight - xPosLeft - UIUtils::Crossover::SLIDER_THUMB_TARGET_WIDTH * 2, // * 2 to subtract the space added on the left and then make space on the right + 12); + + xPosLeft = xPosRight; + } +} + +void CrossoverParameterComponent::_setLabelsText() { + for (size_t index {0}; index < _chainLabels.size(); index++) { + juce::String customName = ModelInterface::getChainCustomName(_processor.manager, index); + + if (customName.isEmpty()) { + _chainLabels[index]->setText(juce::String(index + 1), juce::dontSendNotification); + _secondaryLabels[index]->setVisible(false); + } else { + _chainLabels[index]->setText(customName, juce::dontSendNotification); + _secondaryLabels[index]->setText(juce::String(index + 1), juce::dontSendNotification); + _secondaryLabels[index]->setVisible(true); + } + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.h new file mode 100644 index 00000000..2a59f432 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +#include "JuceHeader.h" +#include "PluginProcessor.h" + +/** + * Draws the clickable parts of the crossover component. + */ +class CrossoverParameterComponent : public juce::Component, + public juce::SettableTooltipClient { +public: + + CrossoverParameterComponent(SyndicateAudioProcessor& processor); + virtual ~CrossoverParameterComponent() = default; + + void paint(juce::Graphics& g) override; + void resized() override; + + void onParameterUpdate() { + _resizeChainLabels(); + _setLabelsText(); + repaint(); + } + + void onNumChainsChanged(); + +private: + SyndicateAudioProcessor& _processor; + std::vector> _chainLabels; + std::vector> _secondaryLabels; + + void _drawSliderThumbs(juce::Graphics& g); + void _drawFrequencyText(juce::Graphics& g); + + void _rebuildChainLabels(); + void _resizeChainLabels(); + void _setLabelsText(); +}; \ No newline at end of file diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.cpp new file mode 100644 index 00000000..bf7e4629 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.cpp @@ -0,0 +1,28 @@ +#include "CrossoverWrapperComponent.h" + +CrossoverWrapperComponent::CrossoverWrapperComponent(SyndicateAudioProcessor& processor) { + + _parameterComponent.reset(new CrossoverParameterComponent(processor)); + addAndMakeVisible(_parameterComponent.get()); + _parameterComponent->setTooltip("Drag the handles to change each band's crossover point"); + + _imagerComponent.reset(new CrossoverImagerComponent(processor)); + addAndMakeVisible(_imagerComponent.get()); + _imagerComponent->toBack(); + + _mouseListener = std::make_unique(processor); + _parameterComponent->addMouseListener(_mouseListener.get(), false); +} + +void CrossoverWrapperComponent::resized() { + _parameterComponent->setBounds(getLocalBounds()); + _imagerComponent->setBounds(getLocalBounds()); +} + +void CrossoverWrapperComponent::onParameterUpdate() { + _parameterComponent->onParameterUpdate(); +} + +void CrossoverWrapperComponent::onNumChainsChanged() { + _parameterComponent->onNumChainsChanged(); +} \ No newline at end of file diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.h new file mode 100644 index 00000000..6bf2ef11 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.h @@ -0,0 +1,27 @@ +#pragma once + +#include "JuceHeader.h" + +#include + +#include "CrossoverImagerComponent.h" +#include "CrossoverMouseListener.h" +#include "CrossoverParameterComponent.h" + +class CrossoverWrapperComponent : public juce::Component { +public: + + CrossoverWrapperComponent(SyndicateAudioProcessor& processor); + virtual ~CrossoverWrapperComponent() = default; + + void resized() override; + + void onParameterUpdate(); + + void onNumChainsChanged(); + +private: + std::unique_ptr _imagerComponent; + std::unique_ptr _parameterComponent; + std::unique_ptr _mouseListener; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/UIUtilsCrossover.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/UIUtilsCrossover.h new file mode 100644 index 00000000..8b00a71e --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/Crossover/UIUtilsCrossover.h @@ -0,0 +1,52 @@ +#pragma once + +#include "MONSTRFilters/MONSTRParameters.h" +#include "UIUtils.h" + +namespace UIUtils::Crossover { + constexpr int SLIDER_THUMB_RADIUS {6}; + constexpr int SLIDER_THUMB_TARGET_WIDTH {SLIDER_THUMB_RADIUS * 4}; // * 4 as it must be minimum 2 * radius, then add a little extra + + constexpr int BAND_BUTTON_PADDING {4}; + + // These are tuned experimentally to get the desired log curve and crossings close to 0,0 and 1,1. + constexpr double LOG_SCALE {3.00043}; + constexpr double LOG_OFFSET_1 {0.001}; + constexpr double LOG_OFFSET_2 {3}; + + /** + * Maps a linear scaled value in the range 0:1 to a log scaled value in the same range. + */ + inline double sliderValueToInternalLog(double sliderValue) { + return std::pow(10, LOG_SCALE * sliderValue - LOG_OFFSET_2) - LOG_OFFSET_1; + } + + /** + * Maps a log scaled value in the range 0:1 to a linear scaled value in the same range. + */ + inline double internalLogToSliderValue(double internalValue) { + return (std::log10(internalValue + LOG_OFFSET_1) + LOG_OFFSET_2) / LOG_SCALE; + } + + /** + * Converts a crossover frequency value between in the internal parameter range to an x coordinate. + */ + inline double sliderValueToXPos(double sliderValue, int componentWidth) { + const double MARGIN_PX {componentWidth * 0.05}; + const double realRange {componentWidth - 2 * MARGIN_PX}; + + const double normalisedSliderValue {WECore::MONSTR::Parameters::CROSSOVER_FREQUENCY.InternalToNormalised(sliderValue)}; + + return (internalLogToSliderValue(normalisedSliderValue) * realRange) + MARGIN_PX; + } + + /** + * Converts an x coordinate to a crossover frequency parameter value in the internal parameter range. + */ + inline double XPosToSliderValue(int XPos, int componentWidth) { + const double MARGIN_PX {componentWidth * 0.05}; + const double realRange {componentWidth - 2 * MARGIN_PX}; + + return WECore::MONSTR::Parameters::CROSSOVER_FREQUENCY.NormalisedToInternal(sliderValueToInternalLog(std::max(XPos - MARGIN_PX, 0.0) / realRange)); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.cpp new file mode 100644 index 00000000..f1b0d38f --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.cpp @@ -0,0 +1,15 @@ +#include "UIUtils.h" + +#include "LeftrightSplitterSubComponent.h" + +LeftrightSplitterSubComponent::LeftrightSplitterSubComponent(SyndicateAudioProcessor& processor, + UIUtils::LinkedScrollView* graphView) + : SplitterHeaderComponent(processor, graphView) { + auto leftChainbuttons = std::make_unique(_processor, 0, "Left"); + _viewPort->getViewedComponent()->addAndMakeVisible(leftChainbuttons.get()); + _chainButtons.push_back(std::move(leftChainbuttons)); + + auto rightChainbuttons = std::make_unique(_processor, 1, "Right"); + _viewPort->getViewedComponent()->addAndMakeVisible(rightChainbuttons.get()); + _chainButtons.push_back(std::move(rightChainbuttons)); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.h new file mode 100644 index 00000000..03be37ef --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "SplitterHeaderComponent.h" +#include "ChainButtonsComponent.h" + +class LeftrightSplitterSubComponent : public SplitterHeaderComponent { +public: + LeftrightSplitterSubComponent(SyndicateAudioProcessor& processor, + UIUtils::LinkedScrollView* graphView); + ~LeftrightSplitterSubComponent() = default; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LeftrightSplitterSubComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.cpp new file mode 100644 index 00000000..f7aacb7a --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.cpp @@ -0,0 +1,15 @@ +#include "UIUtils.h" + +#include "MidsideSplitterSubComponent.h" + +MidsideSplitterSubComponent::MidsideSplitterSubComponent(SyndicateAudioProcessor& processor, + UIUtils::LinkedScrollView* graphView) + : SplitterHeaderComponent(processor, graphView) { + auto midChainbuttons = std::make_unique(_processor, 0, "Mid"); + _viewPort->getViewedComponent()->addAndMakeVisible(midChainbuttons.get()); + _chainButtons.push_back(std::move(midChainbuttons)); + + auto sideChainbuttons = std::make_unique(_processor, 1, "Side"); + _viewPort->getViewedComponent()->addAndMakeVisible(sideChainbuttons.get()); + _chainButtons.push_back(std::move(sideChainbuttons)); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.h new file mode 100644 index 00000000..8871d328 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "SplitterHeaderComponent.h" +#include "ChainButtonsComponent.h" + +class MidsideSplitterSubComponent : public SplitterHeaderComponent { +public: + MidsideSplitterSubComponent(SyndicateAudioProcessor& processor, + UIUtils::LinkedScrollView* graphView); + ~MidsideSplitterSubComponent() = default; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MidsideSplitterSubComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.cpp new file mode 100644 index 00000000..49ebcc3f --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.cpp @@ -0,0 +1,90 @@ +#include "MultibandSplitterSubComponent.h" + +MultibandSplitterSubComponent::MultibandSplitterSubComponent(SyndicateAudioProcessor& processor, + juce::Component* extensionComponent, + UIUtils::LinkedScrollView* graphView) + : SplitterHeaderComponent(processor, graphView) { + addBandBtn.reset(new juce::TextButton("Add Band Button")); + extensionComponent->addAndMakeVisible(addBandBtn.get()); + addBandBtn->setButtonText(TRANS("Chain")); + addBandBtn->setTooltip("Add another parallel chain"); + addBandBtn->setLookAndFeel(&_buttonLookAndFeel); + addBandBtn->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + addBandBtn->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + addBandBtn->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + addBandBtn->addListener(this); + + crossoverComponent.reset(new CrossoverWrapperComponent(processor)); + addAndMakeVisible(crossoverComponent.get()); + crossoverComponent->setName("Crossover Component"); +} + +MultibandSplitterSubComponent::~MultibandSplitterSubComponent() { + addBandBtn->setLookAndFeel(nullptr); + addBandBtn = nullptr; + + crossoverComponent = nullptr; +} + +void MultibandSplitterSubComponent::resized() { + juce::Rectangle headerArea = getLocalBounds(); + + juce::Rectangle extensionArea = addBandBtn->getParentComponent()->getLocalBounds(); + constexpr int MARGIN {4}; + extensionArea.reduce(MARGIN, MARGIN); + extensionArea = extensionArea.withSizeKeepingCentre(extensionArea.getWidth(), extensionArea.getWidth()); + addBandBtn->setBounds(extensionArea); + + // Resize within the header component + crossoverComponent->setBounds(headerArea.removeFromTop(getHeight() / 2)); + _viewPort->setBounds(getLocalBounds()); + SplitterHeaderComponent::resized(); +} + +void MultibandSplitterSubComponent::buttonClicked(juce::Button* buttonThatWasClicked){ + if (buttonThatWasClicked == addBandBtn.get()) { + _processor.addCrossoverBand(); + } +} + +void MultibandSplitterSubComponent::refreshChainButtons() { + crossoverComponent->onParameterUpdate(); + SplitterHeaderComponent::refreshChainButtons(); +} + +void MultibandSplitterSubComponent::onParameterUpdate() { + crossoverComponent->onParameterUpdate(); + + if (_chainButtons.size() != ModelInterface::getNumChains(_processor.manager)) { + _rebuildHeader(); + } + + SplitterHeaderComponent::onParameterUpdate(); +} + +void MultibandSplitterSubComponent::_rebuildHeader() { + crossoverComponent->onNumChainsChanged(); + + const size_t numChains {ModelInterface::getNumChains(_processor.manager)}; + + _chainButtons.clear(); + + for (size_t index {0}; index < numChains; index++) { + if (numChains > 2) { + _chainButtons.emplace_back(std::make_unique( + _processor, + index, + juce::String(index + 1), + [&, index]() { _processor.removeCrossoverBand(index); } + )); + } else { + // Don't provide a callback if there's only two chains - they can't be removed + _chainButtons.emplace_back(std::make_unique(_processor, index, juce::String(index + 1))); + } + _chainButtons[index]->chainLabel->setText("", juce::dontSendNotification); + _viewPort->getViewedComponent()->addAndMakeVisible(_chainButtons[index].get()); + } + + // Layout the buttons + SplitterHeaderComponent::resized(); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.h new file mode 100644 index 00000000..06f7179a --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include "SplitterHeaderComponent.h" +#include "CrossoverWrapperComponent.h" +#include "ChainButtonsComponent.h" + +class MultibandSplitterSubComponent : public SplitterHeaderComponent, + public juce::Button::Listener { +public: + MultibandSplitterSubComponent(SyndicateAudioProcessor& processor, + juce::Component* extensionComponent, + UIUtils::LinkedScrollView* graphView); + ~MultibandSplitterSubComponent() override; + + void onParameterUpdate() override; + + void resized() override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + + void refreshChainButtons() override; + +private: + UIUtils::AddButtonLookAndFeel _buttonLookAndFeel; + + std::unique_ptr addBandBtn; + std::unique_ptr crossoverComponent; + + void _rebuildHeader(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MultibandSplitterSubComponent) +}; + +//[EndFile] You can add extra defines here... +//[/EndFile] + diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.cpp new file mode 100644 index 00000000..bec7d8c7 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.cpp @@ -0,0 +1,80 @@ +#include "UIUtils.h" + +#include "ParallelSplitterSubComponent.h" + +ParallelSplitterSubComponent::ParallelSplitterSubComponent(SyndicateAudioProcessor& processor, + juce::Component* extensionComponent, + UIUtils::LinkedScrollView* graphView) + : SplitterHeaderComponent(processor, graphView) { + addChainBtn.reset(new juce::TextButton("Add Chain Button")); + extensionComponent->addAndMakeVisible(addChainBtn.get()); + addChainBtn->setButtonText(TRANS("Chain")); + addChainBtn->setTooltip("Add another parallel chain"); + addChainBtn->setLookAndFeel(&_buttonLookAndFeel); + addChainBtn->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + addChainBtn->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + addChainBtn->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + addChainBtn->addListener(this); +} + +ParallelSplitterSubComponent::~ParallelSplitterSubComponent() { + addChainBtn->setLookAndFeel(nullptr); + addChainBtn = nullptr; +} + +void ParallelSplitterSubComponent::resized() { + // Resize within the extension + juce::Rectangle extensionArea = addChainBtn->getParentComponent()->getLocalBounds(); + constexpr int MARGIN {4}; + extensionArea.reduce(MARGIN, MARGIN); + extensionArea = extensionArea.withSizeKeepingCentre(extensionArea.getWidth(), extensionArea.getWidth()); + + addChainBtn->setBounds(extensionArea); + + // Resize within the header component + _viewPort->setBounds(getLocalBounds()); + SplitterHeaderComponent::resized(); +} + +void ParallelSplitterSubComponent::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == addChainBtn.get()) { + _processor.addParallelChain(); + } +} + +void ParallelSplitterSubComponent::onParameterUpdate() { + if (_chainButtons.size() != ModelInterface::getNumChains(_processor.manager)) { + _rebuildHeader(); + } + + SplitterHeaderComponent::onParameterUpdate(); +} + +void ParallelSplitterSubComponent::_rebuildHeader() { + const size_t numChains {ModelInterface::getNumChains(_processor.manager)}; + + _chainButtons.clear(); + + for (size_t index {0}; index < numChains; index++) { + if (numChains == 1) { + // Don't provide a callback if there's only one chain - it can't be removed + _chainButtons.emplace_back(std::make_unique( + _processor, + index, + juce::String(index + 1) + )); + } else { + _chainButtons.emplace_back(std::make_unique( + _processor, + index, + juce::String(index + 1), + [&, index]() { _processor.removeParallelChain(index); } + )); + } + + _viewPort->getViewedComponent()->addAndMakeVisible(_chainButtons[index].get()); + } + + // Layout the buttons + SplitterHeaderComponent::resized(); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.h new file mode 100644 index 00000000..fb26a189 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include "SplitterHeaderComponent.h" +#include "UIUtils.h" + +class ParallelSplitterSubComponent : public SplitterHeaderComponent, + public juce::Button::Listener { +public: + ParallelSplitterSubComponent(SyndicateAudioProcessor& processor, + juce::Component* extensionComponent, + UIUtils::LinkedScrollView* graphView); + ~ParallelSplitterSubComponent() override; + + void onParameterUpdate() override; + + void resized() override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + +private: + UIUtils::AddButtonLookAndFeel _buttonLookAndFeel; + std::unique_ptr addChainBtn; + + void _rebuildHeader(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ParallelSplitterSubComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.cpp new file mode 100644 index 00000000..0ce37e54 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.cpp @@ -0,0 +1,11 @@ +#include "SeriesSplitterSubComponent.h" + +#include "UIUtils.h" + +SeriesSplitterSubComponent::SeriesSplitterSubComponent(SyndicateAudioProcessor& processor, + UIUtils::LinkedScrollView* graphView) + : SplitterHeaderComponent(processor, graphView) { + auto chainButtons = std::make_unique(_processor, 0, ""); + _viewPort->getViewedComponent()->addAndMakeVisible(chainButtons.get()); + _chainButtons.push_back(std::move(chainButtons)); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.h new file mode 100644 index 00000000..be725ec9 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include "ChainButtonsComponent.h" +#include "SplitterHeaderComponent.h" + +class SeriesSplitterSubComponent : public SplitterHeaderComponent { +public: + SeriesSplitterSubComponent(SyndicateAudioProcessor& processor, + UIUtils::LinkedScrollView* graphView); + ~SeriesSplitterSubComponent() = default; + +private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SeriesSplitterSubComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SplitterHeaderComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SplitterHeaderComponent.cpp new file mode 100644 index 00000000..25b30622 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SplitterHeaderComponent.cpp @@ -0,0 +1,150 @@ +#include "SplitterHeaderComponent.h" +#include "UIUtils.h" + +namespace { + std::tuple chainDetailsFromVariant(juce::var variant) { + bool isValid {false}; + int chainNumber {0}; + bool isCopy {false}; + + if (variant.isArray() && variant.size() == 2 && variant[0].isInt() && variant[1].isBool()) { + isValid = true; + chainNumber = variant[0]; + isCopy = variant[1]; + } + + return std::make_tuple(isValid, chainNumber, isCopy); + } +} + +SplitterHeaderComponent::SplitterHeaderComponent(SyndicateAudioProcessor& processor, + UIUtils::LinkedScrollView* graphView) + : _processor(processor), _graphView(graphView) { + _viewPort.reset(new UIUtils::LinkedScrollView()); + _viewPort->setViewedComponent(new juce::Component()); + _viewPort->setScrollBarsShown(false, false, false, true); + _viewPort->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + _viewPort->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, UIUtils::neutralColour.withAlpha(0.5f)); + _viewPort->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + addAndMakeVisible(_viewPort.get()); + _viewPort->setBounds(getLocalBounds()); + _viewPort->setOtherView(_graphView); + _graphView->setOtherView(_viewPort.get()); +} + +SplitterHeaderComponent::~SplitterHeaderComponent() { + _graphView->removeOtherView(_viewPort.get()); +} + +void SplitterHeaderComponent::refreshChainButtons() { + for (auto& buttons : _chainButtons) { + buttons->refresh(); + } +} + +void SplitterHeaderComponent::resized() { + _viewPort->setBounds(getLocalBounds()); + + const size_t numChains {_chainButtons.size()}; + const int scrollPosition {_viewPort->getViewPositionX()}; + const int scrollableWidth {std::max(getWidth(), static_cast(UIUtils::CHAIN_WIDTH * numChains))}; + const int scrollableHeight {getHeight()}; + _viewPort->getViewedComponent()->setBounds(juce::Rectangle(scrollableWidth, scrollableHeight)); + + // Preserve scroll position during resize + _viewPort->setViewPosition(scrollPosition, 0); + + juce::Rectangle scrollableArea = _viewPort->getViewedComponent()->getLocalBounds(); + if (scrollableWidth == getWidth()) { + // If the scrollable area is the same as the width we need to remove from the left to + // make the chains centred properly (otherwise we just left align them since the scrolling + // will make it appear correct) + scrollableArea.removeFromLeft(UIUtils::getChainXPos(0, numChains, getWidth())); + } + + // Actually set the bounds of each set of buttons + for (std::unique_ptr& chainButton : _chainButtons) { + chainButton->setBounds(scrollableArea.removeFromLeft(UIUtils::CHAIN_WIDTH)); + } +} + +void SplitterHeaderComponent::paint(juce::Graphics& g) { + if (_shouldDrawDragHint) { + g.setColour(UIUtils::neutralColour); + const int hintXPos { + _dragHintChainNumber < _chainButtons.size() ? + _chainButtons[_dragHintChainNumber]->getX() - _viewPort->getViewPositionX() : + _chainButtons[_chainButtons.size() - 1]->getX() - _viewPort->getViewPositionX() + UIUtils::CHAIN_WIDTH + }; + g.drawLine(hintXPos, 0, hintXPos, getHeight(), 2.0f); + } +} + +bool SplitterHeaderComponent::isInterestedInDragSource(const SourceDetails& dragSourceDetails) { + auto [isValid, fromChainNumber, isCopy] = chainDetailsFromVariant(dragSourceDetails.description); + + // TODO only interested if chain has actually moved or isCopy + + return isValid; +} + +void SplitterHeaderComponent::itemDragEnter(const SourceDetails& dragSourceDetails) { + auto [isValid, fromChainNumber, isCopy] = chainDetailsFromVariant(dragSourceDetails.description); + + if (isValid) { + _shouldDrawDragHint = true; + _dragHintChainNumber = _dragCursorPositionToChainNumber(dragSourceDetails.localPosition); + repaint(); + } +} + +void SplitterHeaderComponent::itemDragMove(const SourceDetails& dragSourceDetails) { +auto [isValid, fromChainNumber, isCopy] = chainDetailsFromVariant(dragSourceDetails.description); + + if (isValid) { + _dragHintChainNumber = _dragCursorPositionToChainNumber(dragSourceDetails.localPosition); + repaint(); + } +} + +void SplitterHeaderComponent::itemDragExit(const SourceDetails& dragSourceDetails) { + _shouldDrawDragHint = false; + repaint(); +} + +void SplitterHeaderComponent::itemDropped(const SourceDetails& dragSourceDetails) { + _shouldDrawDragHint = false; + repaint(); + + // Actually move the slot + auto [isValid, fromChainNumber, isCopy] = chainDetailsFromVariant(dragSourceDetails.description); + + if (isValid) { + if (isCopy) { + _processor.copyChain(fromChainNumber, _dragHintChainNumber); + } else { + _processor.moveChain(fromChainNumber, _dragHintChainNumber); + } + } +} + +void SplitterHeaderComponent::onParameterUpdate() { + for (auto& buttons : _chainButtons) { + buttons->refresh(); + } +} + +int SplitterHeaderComponent::_dragCursorPositionToChainNumber(juce::Point cursorPosition) { + const int numChains {static_cast(_chainButtons.size())}; + + // Check which component the drag is over + const int scrollPosition {_viewPort->getViewPositionX()}; + for (size_t chainNumber {0}; chainNumber < numChains; chainNumber++) { + if (cursorPosition.getX() < _chainButtons[chainNumber]->getRight() - scrollPosition) { + return chainNumber; + } + } + + return numChains; +} + diff --git a/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SplitterHeaderComponent.h b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SplitterHeaderComponent.h new file mode 100644 index 00000000..ee4d609e --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/HeaderComponents/SplitterHeaderComponent.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "ChainButtonsComponent.h" + +class SplitterHeaderComponent : public juce::Component, + public juce::DragAndDropTarget { +public: + explicit SplitterHeaderComponent(SyndicateAudioProcessor& processor, UIUtils::LinkedScrollView* graphView); + virtual ~SplitterHeaderComponent(); + + virtual void refreshChainButtons(); + + virtual void resized() override; + + void paint(juce::Graphics& g) override; + + bool isInterestedInDragSource(const SourceDetails& dragSourceDetails) override; + void itemDragEnter(const SourceDetails& dragSourceDetails) override; + void itemDragMove(const SourceDetails& dragSourceDetails) override; + void itemDragExit(const SourceDetails& dragSourceDetails) override; + void itemDropped(const SourceDetails& dragSourceDetails) override; + + virtual void onParameterUpdate(); + +protected: + std::vector> _chainButtons; + std::unique_ptr _viewPort; + SyndicateAudioProcessor& _processor; + UIUtils::LinkedScrollView* _graphView; + +private: + bool _shouldDrawDragHint {false}; + int _dragHintChainNumber {0}; + + int _dragCursorPositionToChainNumber(juce::Point cursorPosition); +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/ImportExportComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/ImportExportComponent.cpp new file mode 100644 index 00000000..2ae5bf65 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ImportExportComponent.cpp @@ -0,0 +1,158 @@ +#include "ImportExportComponent.h" + +#include "PluginEditor.h" + +namespace { + const juce::String undoTooltipPrefix = "Undo the previous change - "; + const juce::String redoTooltipPrefix = "Redo the last change - "; + const juce::String defaultUndoTooltip = undoTooltipPrefix + "no changes to undo"; + const juce::String defaultRedoTooltip = redoTooltipPrefix + "no changes to redo"; + + void redactXml(juce::XmlElement* element) { + if (element == nullptr) { + return; + } + + // Remove the file attribute that plugins have + if (element->hasAttribute("file")) { + element->removeAttribute("file"); + } + + // Remove the metadata file path + // TODO could optimise this - we don't need to check every element + if (element->hasAttribute("MetadataFullPath")) { + element->removeAttribute("MetadataFullPath"); + } + + redactXml(element->getFirstChildElement()); + redactXml(element->getNextElement()); + } +} + +ImportExportComponent::ImportExportComponent(SyndicateAudioProcessor& processor, SyndicateAudioProcessorEditor& editor) + : _processor(processor), _editor(editor) { + + auto styleButton = [this](juce::TextButton* button) { + button->setLookAndFeel(&_buttonLookAndFeel); + button->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + button->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + button->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + }; + + _exportButton.reset(new juce::TextButton("Save Button")); + addAndMakeVisible(_exportButton.get()); + _exportButton->setButtonText(TRANS("Save")); + _exportButton->setTooltip("Save the current settings to a file"); + styleButton(_exportButton.get()); + _exportButton->onClick = [&]() { + const int flags {juce::FileBrowserComponent::canSelectFiles | juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::warnAboutOverwriting}; + _fileChooser.reset(new juce::FileChooser("Export Syndicate Preset", juce::File(), "*.syn")); + _fileChooser->launchAsync(flags, [&](const juce::FileChooser& chooser) { + _onExportToFile(chooser.getResult()); + }); + }; + + _importButton.reset(new juce::TextButton("Load Button")); + addAndMakeVisible(_importButton.get()); + _importButton->setButtonText(TRANS("Load")); + _importButton->setTooltip("Load settings from a previously saved file"); + styleButton(_importButton.get()); + _importButton->onClick = [&]() { + const int flags {juce::FileBrowserComponent::canSelectFiles | juce::FileBrowserComponent::openMode}; + _fileChooser.reset(new juce::FileChooser("Import Syndicate Preset", juce::File(), "*.syn")); + _fileChooser->launchAsync(flags, [&](const juce::FileChooser& chooser) { + _onImportFromFile(chooser.getResult()); + }); + }; + + _metaButton.reset(new juce::TextButton("Meta Button")); + addAndMakeVisible(_metaButton.get()); + _metaButton->setButtonText(TRANS("Info")); + _metaButton->setTooltip("View and edit preset metadata"); + styleButton(_metaButton.get()); + _metaButton->onClick = [&]() { + _metadataEditPopover.reset(new MetadataEditComponent( + _processor.presetMetadata, + [&](const PresetMetadata& newMetadata) { _processor.setPresetMetadata(newMetadata); }, + [&]() { _metadataEditPopover.reset(); } + )); + + getParentComponent()->addAndMakeVisible(_metadataEditPopover.get()); + _metadataEditPopover->setBounds(getParentComponent()->getLocalBounds()); + }; + + _nameLabel.reset(new juce::Label("Name Label", UIUtils::presetNameOrPlaceholder(_processor.presetMetadata.name))); + addAndMakeVisible(_nameLabel.get()); + _nameLabel->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle("Regular")); + _nameLabel->setJustificationType(juce::Justification::centred); + _nameLabel->setEditable(false, false, false); + _nameLabel->setColour(juce::Label::textColourId, UIUtils::tooltipColour); + _nameLabel->toBack(); + + _initButton.reset(new juce::TextButton("Init Button")); + addAndMakeVisible(_initButton.get()); + _initButton->setButtonText(TRANS("Init")); + _initButton->setTooltip("Reset all state and parameters to their default values"); + styleButton(_initButton.get()); + _initButton->onClick = [&]() { + _processor.resetAllState(); + }; +} + +ImportExportComponent::~ImportExportComponent() { +} + +void ImportExportComponent::resized() { + juce::Rectangle availableArea = getLocalBounds(); + availableArea.reduce(4, 2); + + constexpr int BUTTON_HEIGHT {24}; + constexpr int BUTTON_WIDTH {56}; // Match sidebar buttons + constexpr int SPACER_WIDTH {8}; + + _nameLabel->setBounds(availableArea); + + _exportButton->setBounds(availableArea.removeFromLeft(BUTTON_WIDTH)); + availableArea.removeFromLeft(SPACER_WIDTH); + _importButton->setBounds(availableArea.removeFromLeft(BUTTON_WIDTH)); + availableArea.removeFromLeft(SPACER_WIDTH); + _metaButton->setBounds(availableArea.removeFromLeft(BUTTON_WIDTH)); + availableArea.removeFromLeft(SPACER_WIDTH); + _initButton->setBounds(availableArea.removeFromLeft(BUTTON_WIDTH)); +} + +void ImportExportComponent::paint(juce::Graphics& g) { + g.fillAll(UIUtils::modulationTrayBackgroundColour); +} + +void ImportExportComponent::refresh() { + _nameLabel->setText(UIUtils::presetNameOrPlaceholder(_processor.presetMetadata.name), juce::dontSendNotification); +} + +void ImportExportComponent::_onExportToFile(juce::File file) { + PresetMetadata newMetadata = _processor.presetMetadata; + newMetadata.name = file.getFileNameWithoutExtension(); + newMetadata.fullPath = file.getFullPathName(); + _processor.setPresetMetadata(newMetadata); + + std::unique_ptr element = _processor.writeToXml(); + redactXml(element.get()); + element->writeTo(file); +} + +void ImportExportComponent::_onImportFromFile(juce::File file) { + if (file.existsAsFile()) { + std::unique_ptr element = juce::XmlDocument::parse(file); + if (element != nullptr) { + _processor.restoreFromXml(std::move(element)); + + PresetMetadata newMetadata = _processor.presetMetadata; + newMetadata.name = file.getFileNameWithoutExtension(); + newMetadata.fullPath = file.getFullPathName(); + _processor.setPresetMetadata(newMetadata); + + _editor.closeGuestPluginWindows(); + _editor.needsToRefreshAll(); + } + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ImportExportComponent.h b/ports-juce7/syndicate/Syndicate/UI/ImportExportComponent.h new file mode 100644 index 00000000..c9fd7344 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ImportExportComponent.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "UIUtils.h" +#include "PluginProcessor.h" +#include "MetadataEditComponent.hpp" + +class SyndicateAudioProcessorEditor; + +class ImportExportComponent : public juce::Component { +public: + ImportExportComponent(SyndicateAudioProcessor& processor, SyndicateAudioProcessorEditor& editor); + ~ImportExportComponent() override; + + void paint(juce::Graphics& g) override; + void resized() override; + + void refresh(); + +private: + UIUtils::StaticButtonLookAndFeel _buttonLookAndFeel; + + std::unique_ptr _exportButton; + std::unique_ptr _importButton; + std::unique_ptr _fileChooser; + + std::unique_ptr _metaButton; + std::unique_ptr _metadataEditPopover; + + std::unique_ptr _nameLabel; + + std::unique_ptr _initButton; + + SyndicateAudioProcessor& _processor; + SyndicateAudioProcessorEditor& _editor; + + void _onExportToFile(juce::File file); + void _onImportFromFile(juce::File file); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ImportExportComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/MacroComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/MacroComponent.cpp new file mode 100644 index 00000000..9836d2cb --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/MacroComponent.cpp @@ -0,0 +1,106 @@ +#include "ParameterData.h" + +#include "MacroComponent.h" + +MacroComponent::MacroComponent(int macroNumber, + juce::DragAndDropContainer* dragContainer, + juce::AudioParameterFloat* macroParam, + juce::String& macroName) + : _dragContainer(dragContainer), + _modulationSourceDefinition(macroNumber, MODULATION_TYPE::MACRO), + _macroParam(macroParam), + _macroName(macroName) { + + const juce::String tooltipString("Macro " + juce::String(macroNumber) + " - Drag handle to a plugin modulation tray to assign"); + setTooltip(tooltipString); + + macroSld.reset(new juce::Slider("Macro Slider")); + addAndMakeVisible(macroSld.get()); + macroSld->setRange(0, 1, 0); + macroSld->setSliderStyle(juce::Slider::RotaryVerticalDrag); + macroSld->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + macroSld->addListener(this); + macroSld->setTooltip(tooltipString); + + nameLbl.reset(new juce::Label("Name Label", TRANS("Macro"))); + addAndMakeVisible(nameLbl.get()); + nameLbl->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle ("Regular")); + nameLbl->setJustificationType(juce::Justification::centred); + nameLbl->setEditable(true, true, false); + nameLbl->setColour(juce::Label::textColourId, UIUtils::neutralColour); + nameLbl->setColour(juce::Label::outlineWhenEditingColourId, juce::Colours::transparentBlack); + nameLbl->setColour(juce::TextEditor::textColourId, juce::Colours::black); + nameLbl->setColour(juce::TextEditor::backgroundColourId, juce::Colour(0x00000000)); + nameLbl->addListener(this); + nameLbl->setTooltip(tooltipString); + + dragHandle.reset(new UIUtils::DragHandle()); + addAndMakeVisible(dragHandle.get()); + dragHandle->setColour(UIUtils::DragHandle::handleColourId, UIUtils::neutralColour); + dragHandle->setTooltip(tooltipString); + + const juce::Colour& baseColour = UIUtils::getColourForModulationType(MODULATION_TYPE::MACRO); + + macroSld->setDoubleClickReturnValue(true, MACRO.defaultValue); + macroSld->setLookAndFeel(&_sliderLookAndFeel); + macroSld->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + + nameLbl->setText(_macroName, juce::dontSendNotification); + dragHandle->addMouseListener(this, false); +} + +MacroComponent::~MacroComponent() { + macroSld->setLookAndFeel(nullptr); + + macroSld = nullptr; + nameLbl = nullptr; + dragHandle = nullptr; +} + +void MacroComponent::resized() { + constexpr int Y_SPACING {4}; + + juce::Rectangle availableArea = getLocalBounds(); + macroSld->setBounds(availableArea.removeFromTop(getWidth())); + availableArea.removeFromTop(Y_SPACING); + nameLbl->setBounds(availableArea.removeFromTop(24)); + availableArea.removeFromTop(Y_SPACING); + dragHandle->setBounds(availableArea.removeFromTop(24)); +} + +void MacroComponent::sliderValueChanged(juce::Slider* sliderThatWasMoved) { + if (sliderThatWasMoved == macroSld.get()) { + // TODO call ourProcessor->setParameterValueInternal here instead + _macroParam->setValueNotifyingHost(macroSld->getValue()); + } +} + +void MacroComponent::sliderDragStarted(juce::Slider* slider) { + if (slider == macroSld.get()) { + _macroParam->beginChangeGesture(); + } +} + +void MacroComponent::sliderDragEnded(juce::Slider* slider) { + if (slider == macroSld.get()) { + _macroParam->endChangeGesture(); + } +} + +void MacroComponent::labelTextChanged(juce::Label* labelThatHasChanged) { + if (labelThatHasChanged == nameLbl.get()) { + _macroName = nameLbl->getText(); + } +} + +void MacroComponent::mouseDrag(const juce::MouseEvent& e) { + _dragContainer->startDragging(_modulationSourceDefinition.toVariant(), dragHandle.get()); +} + +void MacroComponent::onParameterUpdate() { + macroSld->setValue(_macroParam->get(), juce::dontSendNotification); +} + +void MacroComponent::updateName(juce::String name) { + nameLbl->setText(name, juce::dontSendNotification); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/MacroComponent.h b/ports-juce7/syndicate/Syndicate/UI/MacroComponent.h new file mode 100644 index 00000000..f03a526d --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/MacroComponent.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "ModulationSourceDefinition.hpp" +#include "UIUtils.h" + +class MacroComponent : public juce::Component, + public juce::Slider::Listener, + public juce::Label::Listener, + public juce::SettableTooltipClient { +public: + MacroComponent(int macroNumber, + juce::DragAndDropContainer* dragContainer, + juce::AudioParameterFloat* macroParam, + juce::String& macroName); + ~MacroComponent() override; + + void onParameterUpdate(); + void updateName(juce::String name); + + void resized() override; + void sliderValueChanged(juce::Slider* sliderThatWasMoved) override; + void sliderDragStarted(juce::Slider* slider) override; + void sliderDragEnded(juce::Slider* slider) override; + void labelTextChanged(juce::Label* labelThatHasChanged) override; + void mouseDrag(const juce::MouseEvent& e) override; + +private: + juce::DragAndDropContainer* _dragContainer; + ModulationSourceDefinition _modulationSourceDefinition; + juce::AudioParameterFloat* _macroParam; + UIUtils::StandardSliderLookAndFeel _sliderLookAndFeel; + juce::String& _macroName; + + std::unique_ptr macroSld; + std::unique_ptr nameLbl; + std::unique_ptr dragHandle; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MacroComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/MacrosComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/MacrosComponent.cpp new file mode 100644 index 00000000..0206065b --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/MacrosComponent.cpp @@ -0,0 +1,49 @@ +#include "MacrosComponent.h" +#include "UIUtils.h" + +MacrosComponent::MacrosComponent(juce::DragAndDropContainer* dragContainer, + std::array& macroParams, + std::array& macroNames) { + for (int index {0}; index < UIUtils::NUM_MACROS; index++) { + _macroComponents.push_back(std::make_unique(index + 1, dragContainer, macroParams[index], macroNames[index])); + addAndMakeVisible(_macroComponents[index].get()); + } +} + +MacrosComponent::~MacrosComponent() { + for (std::unique_ptr& macro : _macroComponents) { + macro = nullptr; + } +} + +void MacrosComponent::resized() { + juce::Rectangle availableArea = getLocalBounds(); + availableArea.removeFromTop(12); + availableArea.removeFromLeft(8); + availableArea.removeFromRight(8); + + juce::FlexBox flexBox; + flexBox.flexDirection = juce::FlexBox::Direction::column; + flexBox.flexWrap = juce::FlexBox::Wrap::wrap; + flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + flexBox.alignContent = juce::FlexBox::AlignContent::center; + + for (std::unique_ptr& macro : _macroComponents) { + flexBox.items.add(juce::FlexItem(*macro.get()).withMinWidth(availableArea.getWidth()).withMinHeight(UIUtils::MACRO_HEIGHT)); + } + + flexBox.performLayout(availableArea.toFloat()); +} + +void MacrosComponent::onParameterUpdate() { + for (std::unique_ptr& component : _macroComponents) { + component->onParameterUpdate(); + } +} + +void MacrosComponent::updateNames(std::array& macroNames) { + for (int index {0}; index < NUM_MACROS; index++) { + _macroComponents[index]->updateName(macroNames[index]); + } +} + diff --git a/ports-juce7/syndicate/Syndicate/UI/MacrosComponent.h b/ports-juce7/syndicate/Syndicate/UI/MacrosComponent.h new file mode 100644 index 00000000..10ce699f --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/MacrosComponent.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "MacroComponent.h" +#include "ParameterData.h" +class MacrosComponent : public juce::Component +{ +public: + MacrosComponent(juce::DragAndDropContainer* dragContainer, + std::array& macroParams, + std::array& macroNames); + ~MacrosComponent() override; + + void resized() override; + + void onParameterUpdate(); + + void updateNames(std::array& macroNames); + +private: + std::vector> _macroComponents; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/MetadataEditComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/MetadataEditComponent.cpp new file mode 100644 index 00000000..1ae4f454 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/MetadataEditComponent.cpp @@ -0,0 +1,129 @@ +#include "MetadataEditComponent.hpp" + +MetadataEditComponent::MetadataEditComponent( + const PresetMetadata& metadata, + std::function onUpdateCallback, + std::function onCloseCallback) + : _onUpdateCallback(onUpdateCallback), + _onCloseCallback(onCloseCallback) { + auto setUpLabel = [](juce::Label* label) { + label->setFont(juce::Font(20.00f, juce::Font::plain).withTypefaceStyle("Bold")); + label->setJustificationType(juce::Justification::left); + label->setEditable(false, false, false); + label->setColour(juce::Label::textColourId, UIUtils::highlightColour); + }; + + auto setUpValueLabel = [](juce::Label* label) { + label->setFont(juce::Font(20.00f, juce::Font::plain).withTypefaceStyle("Regular")); + label->setJustificationType(juce::Justification::left); + label->setEditable(false, false, false); + label->setColour(juce::Label::textColourId, UIUtils::highlightColour); + }; + + auto setUpEditor = [](juce::TextEditor* editor, UIUtils::SearchBarLookAndFeel* lookAndFeel) { + editor->setMultiLine(false); + editor->setReturnKeyStartsNewLine(false); + editor->setReadOnly(false); + editor->setScrollbarsShown(true); + editor->setCaretVisible(true); + editor->setPopupMenuEnabled(true); + editor->setText(juce::String()); + editor->setEscapeAndReturnKeysConsumed(false); + editor->setSelectAllWhenFocused(true); + editor->setLookAndFeel(lookAndFeel); + editor->setColour(juce::TextEditor::outlineColourId, UIUtils::highlightColour); + editor->setColour(juce::TextEditor::backgroundColourId, UIUtils::backgroundColour); + editor->setColour(juce::TextEditor::textColourId, UIUtils::highlightColour); + editor->setColour(juce::TextEditor::highlightColourId, UIUtils::highlightColour); + editor->setColour(juce::TextEditor::highlightedTextColourId, UIUtils::neutralColour); + editor->setColour(juce::CaretComponent::caretColourId, UIUtils::highlightColour); + }; + + _nameLabel.reset(new juce::Label("Name Label", "Name")); + addAndMakeVisible(_nameLabel.get()); + setUpLabel(_nameLabel.get()); + + _nameValueLabel.reset(new juce::Label("Name Value", UIUtils::presetNameOrPlaceholder(metadata.name))); + addAndMakeVisible(_nameValueLabel.get()); + setUpValueLabel(_nameValueLabel.get()); + + _fullPathLabel.reset(new juce::Label("Full Path Label", "File Path")); + addAndMakeVisible(_fullPathLabel.get()); + setUpLabel(_fullPathLabel.get()); + + _fullPathValueLabel.reset(new juce::Label("Full Path Value", UIUtils::presetNameOrPlaceholder(metadata.fullPath))); + addAndMakeVisible(_fullPathValueLabel.get()); + setUpValueLabel(_fullPathValueLabel.get()); + + _authorLabel.reset(new juce::Label("Author Label", "Author")); + addAndMakeVisible(_authorLabel.get()); + setUpLabel(_authorLabel.get()); + + _authorEditor.reset(new juce::TextEditor("Author Editor")); + addAndMakeVisible(_authorEditor.get()); + setUpEditor(_authorEditor.get(), &_searchBarLookAndFeel); + _authorEditor->setText(metadata.author); + + _descriptionLabel.reset(new juce::Label("Description Label", "Description")); + addAndMakeVisible(_descriptionLabel.get()); + setUpLabel(_descriptionLabel.get()); + + _descriptionEditor.reset(new juce::TextEditor("Description Editor")); + addAndMakeVisible(_descriptionEditor.get()); + setUpEditor(_descriptionEditor.get(), &_searchBarLookAndFeel); + _descriptionEditor->setMultiLine(true); + _descriptionEditor->setReturnKeyStartsNewLine(true); + _descriptionEditor->setText(metadata.description); + + _closeButton.reset(new juce::TextButton("OK button")); + addAndMakeVisible(_closeButton.get()); + _closeButton->setButtonText(TRANS("OK")); + _closeButton->setLookAndFeel(&_buttonLookAndFeel); + _closeButton->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + _closeButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + _closeButton->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + _closeButton->onClick = [&, metadata] { + _onUpdateCallback(PresetMetadata{ + metadata.name, + metadata.fullPath, + _authorEditor->getText(), + _descriptionEditor->getText() + }); + _onCloseCallback(); + }; +} + +void MetadataEditComponent::resized() { + constexpr int ROW_HEIGHT {24}; + + juce::Rectangle availableArea = getLocalBounds().reduced(20); + + // Name row + _nameLabel->setBounds(availableArea.removeFromTop(ROW_HEIGHT)); + _nameValueLabel->setBounds(availableArea.removeFromTop(ROW_HEIGHT)); + + availableArea.removeFromTop(ROW_HEIGHT); + + // Full path row + _fullPathLabel->setBounds(availableArea.removeFromTop(ROW_HEIGHT)); + _fullPathValueLabel->setBounds(availableArea.removeFromTop(ROW_HEIGHT)); + + availableArea.removeFromTop(ROW_HEIGHT); + + // Author row + _authorLabel->setBounds(availableArea.removeFromTop(ROW_HEIGHT)); + _authorEditor->setBounds(availableArea.removeFromTop(ROW_HEIGHT)); + + availableArea.removeFromTop(ROW_HEIGHT); + + // Description row + _descriptionLabel->setBounds(availableArea.removeFromTop(ROW_HEIGHT)); + _descriptionEditor->setBounds(availableArea.removeFromTop(4 * ROW_HEIGHT)); + + juce::Rectangle closeButtonArea = availableArea.removeFromBottom(availableArea.getHeight() / 4); + _closeButton->setBounds(closeButtonArea.withSizeKeepingCentre(60, 40)); +} + +void MetadataEditComponent::paint(juce::Graphics& g) { + g.fillAll(juce::Colours::black.withAlpha(0.8f)); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/MetadataEditComponent.hpp b/ports-juce7/syndicate/Syndicate/UI/MetadataEditComponent.hpp new file mode 100644 index 00000000..eb6b06f3 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/MetadataEditComponent.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "PresetMetadata.hpp" +#include "UIUtils.h" + +class MetadataEditComponent : public juce::Component { +public: + MetadataEditComponent( + const PresetMetadata& metadata, + std::function onUpdateCallback, + std::function onCloseCallback); + + void resized() override; + void paint(juce::Graphics& g) override; + +private: + std::function _onUpdateCallback; + std::function _onCloseCallback; + + UIUtils::StaticButtonLookAndFeel _buttonLookAndFeel; + UIUtils::SearchBarLookAndFeel _searchBarLookAndFeel; + + std::unique_ptr _nameLabel; + std::unique_ptr _nameValueLabel; + std::unique_ptr _fullPathLabel; + std::unique_ptr _fullPathValueLabel; + std::unique_ptr _authorLabel; + std::unique_ptr _authorEditor; + std::unique_ptr _descriptionLabel; + std::unique_ptr _descriptionEditor; + std::unique_ptr _closeButton; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulatableParameter.cpp b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulatableParameter.cpp new file mode 100644 index 00000000..4e5890e6 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulatableParameter.cpp @@ -0,0 +1,100 @@ +#include "ModulatableParameter.hpp" +#include "UIUtils.h" + +ModulatableParameter::ModulatableParameter( + std::vector> sources, + std::function onSourceSlotAdded, + std::function onSourceSlotRemoved, + std::function onSliderMovedCallback, + std::function getModulatedValue, + juce::String sliderName, + juce::String labelName, + juce::String labelText) : _sources(sources), _onSourceSlotAdded(onSourceSlotAdded) { + slider.reset(new ModulationTargetSlider(sliderName, getModulatedValue)); + addAndMakeVisible(slider.get()); + slider->setSliderStyle(juce::Slider::RotaryVerticalDrag); + slider->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + + label.reset(new juce::Label(labelName, TRANS(labelText))); + addAndMakeVisible(label.get()); + UIUtils::setDefaultLabelStyle(label); + slider->start(label.get(), label->getText()); + + sourceSliders.reset(new ModulationTargetSourceSliders( + sources, + onSliderMovedCallback, + onSourceSlotRemoved + )); + addAndMakeVisible(sourceSliders.get()); +} + +ModulatableParameter::~ModulatableParameter() { + slider->stop(); + slider = nullptr; + label = nullptr; +} + +void ModulatableParameter::resized() { + constexpr int SLIDER_HEIGHT {39}; + constexpr int LABEL_HEIGHT {25}; + constexpr int SOURCE_SLIDERS_HEIGHT {17}; + + juce::FlexBox flexBox; + flexBox.flexDirection = juce::FlexBox::Direction::column; + flexBox.flexWrap = juce::FlexBox::Wrap::noWrap; + flexBox.justifyContent = juce::FlexBox::JustifyContent::center; + flexBox.alignContent = juce::FlexBox::AlignContent::center; + + flexBox.items.add(juce::FlexItem(*slider.get()).withMinHeight(SLIDER_HEIGHT).withMinWidth(getWidth())); + flexBox.items.add(juce::FlexItem(*label.get()).withMinHeight(LABEL_HEIGHT).withMinWidth(getWidth())); + flexBox.items.add(juce::FlexItem(*sourceSliders.get()).withMinHeight(SOURCE_SLIDERS_HEIGHT).withMinWidth(getWidth())); + + juce::Rectangle availableArea = getLocalBounds(); + flexBox.performLayout(availableArea.toFloat()); +} + +bool ModulatableParameter::isInterestedInDragSource(const SourceDetails& dragSourceDetails) { + const std::optional draggedDefinition = + ModulationSourceDefinition::fromVariant(dragSourceDetails.description); + + // Check dragged item contains a valid definition + if (!draggedDefinition.has_value()) { + return false; + } + + // Check if the source is already in the list + for (const auto source : _sources) { + if (source->definition == draggedDefinition.value()) { + return false; + } + } + + return true; + +} + +void ModulatableParameter::itemDragEnter(const SourceDetails& dragSourceDetails) { + // Make the modulation slot visible + const std::optional draggedDefinition = + ModulationSourceDefinition::fromVariant(dragSourceDetails.description); + + if (draggedDefinition.has_value()) { + // Add a slot to the UI (but not on the backend) + sourceSliders->addSource(draggedDefinition.value()); + } +} + +void ModulatableParameter::itemDragExit(const SourceDetails& dragSourceDetails) { + // Remove the modulation slot from the UI + sourceSliders->removeLastSource(); +} + +void ModulatableParameter::itemDropped(const SourceDetails& dragSourceDetails) { + const std::optional draggedDefinition = + ModulationSourceDefinition::fromVariant(dragSourceDetails.description); + + // // Add another source to the target + if (draggedDefinition.has_value()) { + _onSourceSlotAdded(draggedDefinition.value()); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulatableParameter.hpp b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulatableParameter.hpp new file mode 100644 index 00000000..77304f90 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulatableParameter.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include "ModulationTargetSlider.hpp" +#include "ModulationTargetSourceSlider.hpp" +#include "PluginProcessor.h" + +class ModulatableParameter : public juce::Component, + public juce::DragAndDropTarget { +public: + std::unique_ptr slider; + std::unique_ptr label; + std::unique_ptr sourceSliders; + + ModulatableParameter( + std::vector> sources, + std::function onSourceSlotAdded, + std::function onSourceSlotRemoved, + std::function onSliderMovedCallback, + std::function getModulatedValue, + juce::String sliderName, + juce::String labelName, + juce::String labelText); + + virtual ~ModulatableParameter() override; + + void resized() override; + + bool isInterestedInDragSource(const SourceDetails& dragSourceDetails) override; + void itemDragEnter(const SourceDetails& dragSourceDetails) override; + void itemDragExit(const SourceDetails& dragSourceDetails) override; + void itemDropped(const SourceDetails& dragSourceDetails) override; + +private: + const std::vector> _sources; + std::function _onSourceSlotAdded; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBar.cpp b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBar.cpp new file mode 100644 index 00000000..bafb315b --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBar.cpp @@ -0,0 +1,415 @@ +#include "ModulationBar.h" +#include "UIUtils.h" + +ModulationBar::ModulationBar(SyndicateAudioProcessor& processor, + juce::DragAndDropContainer* dragContainer) : + _processor(processor), + _dragContainer(dragContainer), + _hasRestoredScroll(false) { + _addButtonLookAndFeel.reset(new AddButtonLookAndFeel()); + + auto setUpButtonsView = [&] (std::unique_ptr& view) { + view.reset(new juce::Viewport()); + view->setViewedComponent(new juce::Component()); + view->setScrollBarsShown(true, false); + view->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + view->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, UIUtils::neutralColour.withAlpha(0.5f)); + view->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + addAndMakeVisible(view.get()); + }; + + setUpButtonsView(_lfoButtonsView); + setUpButtonsView(_envelopeButtonsView); + setUpButtonsView(_rndButtonsView); + + needsRebuild(); + + // Restore the selection + if (_processor.mainWindowState.selectedModulationSource.has_value()) { + const ModulationSourceDefinition definition = _processor.mainWindowState.selectedModulationSource.value(); + + if (definition.type == MODULATION_TYPE::LFO) { + if (definition.id <= _lfoButtons.size()) { + _selectModulationSource(_lfoButtons[definition.id - 1].get()); + } + } else if (definition.type == MODULATION_TYPE::ENVELOPE) { + if (definition.id <= _envelopeButtons.size()) { + _selectModulationSource(_envelopeButtons[definition.id - 1].get()); + } + } else if (definition.type == MODULATION_TYPE::RANDOM) { + if (definition.id <= _rndButtons.size()) { + _selectModulationSource(_rndButtons[definition.id - 1].get()); + } + } + } +} + +ModulationBar::~ModulationBar() { + _processor.mainWindowState.lfoButtonsScrollPosition = _lfoButtonsView->getViewPositionY(); + _processor.mainWindowState.envButtonsScrollPosition = _envelopeButtonsView->getViewPositionY(); + _processor.mainWindowState.rndButtonsScrollPosition = _rndButtonsView->getViewPositionY(); + + // Reset stored state since it might now be invalid + _processor.mainWindowState.selectedModulationSource.reset(); + + // Store the selection + for (int buttonIndex {0}; buttonIndex < _lfoButtons.size() && !_processor.mainWindowState.selectedModulationSource.has_value(); buttonIndex++) { + if (_lfoButtons[buttonIndex]->getIsSelected()) { + _processor.mainWindowState.selectedModulationSource = _lfoButtons[buttonIndex]->definition; + } + } + + for (int buttonIndex {0}; buttonIndex < _envelopeButtons.size() && !_processor.mainWindowState.selectedModulationSource.has_value(); buttonIndex++) { + if (_envelopeButtons[buttonIndex]->getIsSelected()) { + _processor.mainWindowState.selectedModulationSource = _envelopeButtons[buttonIndex]->definition; + } + } + + for (int buttonIndex {0}; buttonIndex < _rndButtons.size() && !_processor.mainWindowState.selectedModulationSource.has_value(); buttonIndex++) { + if (_rndButtons[buttonIndex]->getIsSelected()) { + _processor.mainWindowState.selectedModulationSource = _rndButtons[buttonIndex]->definition; + } + } + + // Usual clean up + _addLfoButton->setLookAndFeel(nullptr); + _addEnvelopeButton->setLookAndFeel(nullptr); + _addRndButton->setLookAndFeel(nullptr); + + _addButtonLookAndFeel = nullptr; + + _lfoButtonsView = nullptr; + _envelopeButtonsView = nullptr; + _rndButtonsView = nullptr; +} + +void ModulationBar::resized() { + juce::Rectangle availableArea = getLocalBounds(); + + auto arrangeColumn = [&availableArea](std::vector>& buttons, + std::unique_ptr& addButton, + std::unique_ptr& buttonsView) { + + const int columnHeight {static_cast(UIUtils::MODULATION_LIST_BUTTON_HEIGHT * (buttons.size() + 1))}; + + const juce::Rectangle visibleButtonsArea = + availableArea.removeFromLeft(UIUtils::MODULATION_LIST_COLUMN_WIDTH); + buttonsView->setBounds(visibleButtonsArea); + + const int scrollPosition {buttonsView->getViewPositionY()}; + juce::Rectangle scrollablebuttonsArea = visibleButtonsArea.withHeight(columnHeight); + buttonsView->getViewedComponent()->setBounds(scrollablebuttonsArea); + buttonsView->setViewPosition(0, scrollPosition); + + // Set the origin to 0 since we're now using it to position buttons relative to the inner + // component + scrollablebuttonsArea.setPosition(0, 0); + + const int scrollbarWidth {buttonsView->getScrollBarThickness()}; + + for (std::unique_ptr& button : buttons) { + button->setBounds(scrollablebuttonsArea.removeFromTop(UIUtils::MODULATION_LIST_BUTTON_HEIGHT).withTrimmedRight(scrollbarWidth)); + } + + if (addButton != nullptr) { + addButton->setBounds(scrollablebuttonsArea.removeFromTop(UIUtils::MODULATION_LIST_BUTTON_HEIGHT).withTrimmedRight(scrollbarWidth)); + } + }; + + // LFO buttons + arrangeColumn(_lfoButtons, _addLfoButton, _lfoButtonsView); + + // Envelope buttons + arrangeColumn(_envelopeButtons, _addEnvelopeButton, _envelopeButtonsView); + + // Random buttons + arrangeColumn(_rndButtons, _addRndButton, _rndButtonsView); + + _selectedSourceComponentArea = availableArea; + + if (_selectedSourceComponent != nullptr) { + _selectedSourceComponent->setBounds(_selectedSourceComponentArea); + } +} + +void ModulationBar::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == _addLfoButton.get()) { + _processor.addLfo(); + _selectModulationSource(_lfoButtons[_lfoButtons.size() - 1].get()); + } else if (buttonThatWasClicked == _addEnvelopeButton.get()) { + _processor.addEnvelope(); + _selectModulationSource(_envelopeButtons[_envelopeButtons.size() - 1].get()); + } else if (buttonThatWasClicked == _addRndButton.get()) { + _processor.addRandomSource(); + } +} + +void ModulationBar::needsRebuild() { + // Get the currently selected definition + std::optional selectedDefinition = _getSelectedDefinition(); + + _resetButtons(); + + // Re-select the already selected source from before the rebuild (if it still exists) + if (selectedDefinition.has_value()) { + _attemptToSelectByDefinition(selectedDefinition.value()); + } else if (_lfoButtons.size() > 0) { + _selectModulationSource(_lfoButtons[0].get()); + } else if (_envelopeButtons.size() > 0) { + _selectModulationSource(_envelopeButtons[0].get()); + } else if (_rndButtons.size() > 0) { + _selectModulationSource(_rndButtons[0].get()); + } +} + +void ModulationBar::needsSelectedSourceRebuild() { + // Find the selected source and reselect it + for (std::unique_ptr& button : _lfoButtons) { + if (button->getIsSelected()) { + _selectModulationSource(button.get()); + return; + } + } + + for (std::unique_ptr& button : _envelopeButtons) { + if (button->getIsSelected()) { + _selectModulationSource(button.get()); + return; + } + } + + for (std::unique_ptr& button : _rndButtons) { + if (button->getIsSelected()) { + _selectModulationSource(button.get()); + return; + } + } +} + +void ModulationBar::AddButtonLookAndFeel::drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& /*backgroundColour*/, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + + constexpr int MARGIN {1}; + juce::Rectangle area = juce::Rectangle(button.getWidth(), button.getHeight()).reduced(MARGIN, MARGIN); + + g.setColour(button.findColour(juce::TextButton::buttonColourId)); + g.fillRoundedRectangle(area.toFloat(), area.getHeight() / 2); +} + +void ModulationBar::_resetButtons() { + // Add the LFO buttons + _lfoButtons.clear(); + ModelInterface::forEachLfo(_processor.manager, [&](int lfoNumber) { + _createModulationSourceButton(ModulationSourceDefinition(lfoNumber, MODULATION_TYPE::LFO)); + }); + + // And the add button + _addLfoButton.reset(new juce::TextButton("Add LFO Button")); + _lfoButtonsView->getViewedComponent()->addAndMakeVisible(_addLfoButton.get()); + _addLfoButton->setButtonText("+ LFO"); + _addLfoButton->addListener(this); + _addLfoButton->setTooltip("Create a new LFO"); + _addLfoButton->setColour(juce::TextButton::buttonColourId, UIUtils::slotBackgroundColour); + _addLfoButton->setColour(juce::TextButton::textColourOffId, UIUtils::getColourForModulationType(MODULATION_TYPE::LFO)); + _addLfoButton->setLookAndFeel(_addButtonLookAndFeel.get()); + + // Add the envelope buttons + _envelopeButtons.clear(); + ModelInterface::forEachEnvelope(_processor.manager, [&](int envelopeNumber) { + _createModulationSourceButton(ModulationSourceDefinition(envelopeNumber, MODULATION_TYPE::ENVELOPE)); + }); + + // And the add button + _addEnvelopeButton.reset(new juce::TextButton("Add Envelope Button")); + _envelopeButtonsView->getViewedComponent()->addAndMakeVisible(_addEnvelopeButton.get()); + _addEnvelopeButton->setButtonText("+ ENV"); + _addEnvelopeButton->addListener(this); + _addEnvelopeButton->setTooltip("Create a new envelope"); + _addEnvelopeButton->setColour(juce::TextButton::buttonColourId, UIUtils::slotBackgroundColour); + _addEnvelopeButton->setColour(juce::TextButton::textColourOffId, UIUtils::getColourForModulationType(MODULATION_TYPE::ENVELOPE)); + _addEnvelopeButton->setLookAndFeel(_addButtonLookAndFeel.get()); + + // Add the random buttons + _rndButtons.clear(); + ModelInterface::forEachRandom(_processor.manager, [&](int randomNumber) { + _createModulationSourceButton(ModulationSourceDefinition(randomNumber, MODULATION_TYPE::RANDOM)); + }); + + // Add the add button + _addRndButton.reset(new juce::TextButton("Add Random Button")); + _rndButtonsView->getViewedComponent()->addAndMakeVisible(_addRndButton.get()); + _addRndButton->setButtonText("+ RND"); + _addRndButton->addListener(this); + _addRndButton->setTooltip("Create a new random source"); + _addRndButton->setColour(juce::TextButton::buttonColourId, UIUtils::slotBackgroundColour); + _addRndButton->setColour(juce::TextButton::textColourOffId, UIUtils::getColourForModulationType(MODULATION_TYPE::RANDOM)); + _addRndButton->setLookAndFeel(_addButtonLookAndFeel.get()); + + resized(); + + // We need to run this only once after the graph view has been constructed to restore the scroll + // position to the same as before the UI was last closed + if (!_hasRestoredScroll) { + _hasRestoredScroll = true; + + _lfoButtonsView->setViewPosition(0, _processor.mainWindowState.lfoButtonsScrollPosition); + _envelopeButtonsView->setViewPosition(0, _processor.mainWindowState.envButtonsScrollPosition); + _rndButtonsView->setViewPosition(0, _processor.mainWindowState.rndButtonsScrollPosition); + } +} + +void ModulationBar::_createModulationSourceButton(ModulationSourceDefinition definition) { + std::unique_ptr newButton; + newButton.reset(new ModulationButton( + definition, + [&](ModulationButton* button) { _selectModulationSource(button); }, + [&, definition]() { _removeModulationSource(definition); }, + _dragContainer)); + + if (definition.type == MODULATION_TYPE::LFO) { + _lfoButtonsView->getViewedComponent()->addAndMakeVisible(newButton.get()); + _lfoButtons.push_back(std::move(newButton)); + } else if (definition.type == MODULATION_TYPE::ENVELOPE) { + _envelopeButtonsView->getViewedComponent()->addAndMakeVisible(newButton.get()); + _envelopeButtons.push_back(std::move(newButton)); + } else if (definition.type == MODULATION_TYPE::RANDOM) { + _rndButtonsView->getViewedComponent()->addAndMakeVisible(newButton.get()); + _rndButtons.push_back(std::move(newButton)); + } +} + +void ModulationBar::_selectModulationSource(ModulationButton* selectedButton) { + // Deactivate all buttons + for (std::unique_ptr& button : _lfoButtons) { + button->setIsSelected(false); + } + + for (std::unique_ptr& button : _envelopeButtons) { + button->setIsSelected(false); + } + + for (std::unique_ptr& button : _rndButtons) { + button->setIsSelected(false); + } + + // Activate just the one that's selected + selectedButton->setIsSelected(true); + + // Draw the modulaton source UI + if (selectedButton->definition.type == MODULATION_TYPE::LFO) { + _selectedSourceComponent.reset(new ModulationBarLfo(_processor, selectedButton->definition.id - 1)); + } else if (selectedButton->definition.type == MODULATION_TYPE::ENVELOPE) { + _selectedSourceComponent.reset(new ModulationBarEnvelope(_processor, selectedButton->definition.id - 1)); + } else if (selectedButton->definition.type == MODULATION_TYPE::RANDOM) { + _selectedSourceComponent.reset(new ModulationBarRandom(_processor, selectedButton->definition.id - 1)); + } + + addAndMakeVisible(_selectedSourceComponent.get()); + _selectedSourceComponent->setBounds(_selectedSourceComponentArea); +} + +void ModulationBar::_removeModulationSource(ModulationSourceDefinition definition) { + // Get the currently selected definition + std::optional selectedDefinition = _getSelectedDefinition(); + + // Delete selected component before changing anything in the processor to make sure nothing in + // it is referring to anything we might change + _selectedSourceComponent.reset(); + + // Remove the modulation source and rebuild the UI + _processor.removeModulationSource(definition); + _resetButtons(); + + if (!selectedDefinition.has_value()) { + return; + } + + // Update the selected ID if we're deleting something ahead of it + if (selectedDefinition.value().id > definition.id) { + selectedDefinition.value().id--; + } + + // Decide which definition should now be selected + _attemptToSelectByDefinition(selectedDefinition.value()); +} + +std::optional ModulationBar::_getSelectedDefinition() { + std::optional selectedDefinition; + for (int buttonIndex {0}; buttonIndex < _lfoButtons.size(); buttonIndex++) { + if (_lfoButtons[buttonIndex]->getIsSelected()) { + return _lfoButtons[buttonIndex]->definition; + } + } + + for (int buttonIndex {0}; buttonIndex < _envelopeButtons.size(); buttonIndex++) { + if (_envelopeButtons[buttonIndex]->getIsSelected()) { + return _envelopeButtons[buttonIndex]->definition; + } + } + + for (int buttonIndex {0}; buttonIndex < _rndButtons.size(); buttonIndex++) { + if (_rndButtons[buttonIndex]->getIsSelected()) { + return _rndButtons[buttonIndex]->definition; + } + } + + return selectedDefinition; +} + +void ModulationBar::_attemptToSelectByDefinition(ModulationSourceDefinition definition) { + if (definition.type == MODULATION_TYPE::LFO) { + if (_lfoButtons.size() > definition.id - 1) { + // Select the previously selected definition + _selectModulationSource(_lfoButtons[definition.id - 1].get()); + } else if (_lfoButtons.size() > 0) { + // We deleted the last definition, so select the current last one + _selectModulationSource(_lfoButtons[_lfoButtons.size() - 1].get()); + } else if (_envelopeButtons.size() > 0) { + // We deleted all the LFOs, so select an envelope follower + _selectModulationSource(_envelopeButtons[0].get()); + } else if (_rndButtons.size() > 0) { + // We deleted all the LFOs and envelope followers, so select a random source + _selectModulationSource(_rndButtons[0].get()); + } else { + // We deleted everything, select nothing + _selectedSourceComponent.reset(); + } + } else if (definition.type == MODULATION_TYPE::ENVELOPE) { + if (_envelopeButtons.size() > definition.id - 1) { + // Restore the previously selected definition + _selectModulationSource(_envelopeButtons[definition.id - 1].get()); + } else if (_envelopeButtons.size() > 0) { + // We deleted the last definition, so select the current last one + _selectModulationSource(_envelopeButtons[0].get()); + } else if (_lfoButtons.size() > 0) { + // We deleted all the envelope followers, so select an LFO + _selectModulationSource(_lfoButtons[0].get()); + } else if (_rndButtons.size() > 0) { + // We deleted all the envelope followers and LFOs, so select a random source + _selectModulationSource(_rndButtons[0].get()); + } else { + // We deleted everything, select nothing + _selectedSourceComponent.reset(); + } + } else if (definition.type == MODULATION_TYPE::RANDOM) { + if (_rndButtons.size() > definition.id - 1) { + // Restore the previously selected definition + _selectModulationSource(_rndButtons[definition.id - 1].get()); + } else if (_rndButtons.size() > 0) { + // We deleted the last definition, so select the current last one + _selectModulationSource(_rndButtons[0].get()); + } else if (_lfoButtons.size() > 0) { + // We deleted all the random sources, so select an LFO + _selectModulationSource(_lfoButtons[0].get()); + } else if (_envelopeButtons.size() > 0) { + // We deleted all the random sources and LFOs, so select an envelope follower + _selectModulationSource(_envelopeButtons[0].get()); + } else { + // We deleted everything, select nothing + _selectedSourceComponent.reset(); + } + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBar.h b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBar.h new file mode 100644 index 00000000..2d40480b --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBar.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "ModulationBarEnvelope.h" +#include "ModulationBarLfo.h" +#include "ModulationBarRandom.hpp" +#include "CoreJUCEPlugin/CoreLookAndFeel.h" +#include "ModulationButton.h" + +class ModulationBar : public juce::Component, + juce::Button::Listener { +public: + ModulationBar(SyndicateAudioProcessor& processor, juce::DragAndDropContainer* dragContainer); + ~ModulationBar() override; + + void resized() override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + + void needsRebuild(); + void needsSelectedSourceRebuild(); + +private: + class AddButtonLookAndFeel : public juce::LookAndFeel_V2 { + public: + AddButtonLookAndFeel() = default; + ~AddButtonLookAndFeel() = default; + + void drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + }; + + SyndicateAudioProcessor& _processor; + + std::vector> _lfoButtons; + std::unique_ptr _addLfoButton; + std::unique_ptr _lfoButtonsView; + + std::vector> _envelopeButtons; + std::unique_ptr _addEnvelopeButton; + std::unique_ptr _envelopeButtonsView; + + std::vector> _rndButtons; + std::unique_ptr _addRndButton; + std::unique_ptr _rndButtonsView; + + juce::Rectangle _selectedSourceComponentArea; + std::unique_ptr _selectedSourceComponent; + juce::DragAndDropContainer* _dragContainer; + std::unique_ptr _addButtonLookAndFeel; + + bool _hasRestoredScroll; + + void _resetButtons(); + void _createModulationSourceButton(ModulationSourceDefinition definition); + void _selectModulationSource(ModulationButton* selectedButton); + void _removeModulationSource(ModulationSourceDefinition definition); + std::optional _getSelectedDefinition(); + void _attemptToSelectByDefinition(ModulationSourceDefinition definition); +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarEnvelope.cpp b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarEnvelope.cpp new file mode 100644 index 00000000..d3370c17 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarEnvelope.cpp @@ -0,0 +1,285 @@ +#include "ParameterData.h" +#include "UIUtils.h" + +#include "ModulationBarEnvelope.h" + +ModulationBarEnvelope::ModulationBarEnvelope(SyndicateAudioProcessor& processor, int envIndex) : + _processor(processor), _envIndex(envIndex) { + + namespace AP = WECore::AREnv::Parameters; + constexpr double INTERVAL {0.01}; + const juce::Colour& baseColour = UIUtils::getColourForModulationType(MODULATION_TYPE::ENVELOPE); + + attackSlider.reset(new WECore::JUCEPlugin::LabelReadoutSlider("ENV Attack Slider")); + addAndMakeVisible(attackSlider.get()); + attackSlider->setTooltip(TRANS("Attack of the envelope follower")); + attackSlider->setRange(AP::ATTACK_MS.minValue, AP::ATTACK_MS.maxValue, INTERVAL); + attackSlider->setDoubleClickReturnValue(true, AP::ATTACK_MS.defaultValue); + attackSlider->setSliderStyle(juce::Slider::RotaryVerticalDrag); + attackSlider->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + attackSlider->addListener(this); + attackSlider->setSkewFactor(0.4); + attackSlider->setLookAndFeel(&_sliderLookAndFeel); + attackSlider->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + + releaseSlider.reset(new WECore::JUCEPlugin::LabelReadoutSlider("ENV RElease Slider")); + addAndMakeVisible(releaseSlider.get()); + releaseSlider->setTooltip(TRANS("Release of the envelope follower")); + releaseSlider->setRange(AP::RELEASE_MS.minValue, AP::RELEASE_MS.maxValue, INTERVAL); + releaseSlider->setDoubleClickReturnValue(true, AP::RELEASE_MS.defaultValue); + releaseSlider->setSliderStyle(juce::Slider::RotaryVerticalDrag); + releaseSlider->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + releaseSlider->addListener(this); + releaseSlider->setSkewFactor(0.4); + releaseSlider->setLookAndFeel(&_sliderLookAndFeel); + releaseSlider->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + + amountSlider.reset(new WECore::JUCEPlugin::LabelReadoutSlider("ENV Amount Slider")); + addAndMakeVisible(amountSlider.get()); + amountSlider->setTooltip(TRANS("Amount of envelope follower modulation")); + amountSlider->setRange(ENVELOPE_AMOUNT.minValue, ENVELOPE_AMOUNT.maxValue, INTERVAL); + amountSlider->setDoubleClickReturnValue(true, ENVELOPE_AMOUNT.defaultValue); + amountSlider->setSliderStyle(juce::Slider::RotaryVerticalDrag); + amountSlider->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + amountSlider->addListener(this); + amountSlider->setLookAndFeel(&_midAnchorSliderLookAndFeel); + amountSlider->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + + attackLabel.reset(new juce::Label("ENV Attack Label", TRANS("Attack"))); + addAndMakeVisible(attackLabel.get()); + UIUtils::setDefaultLabelStyle(attackLabel); + + releaseLabel.reset(new juce::Label("ENV Release Label", TRANS("Release"))); + addAndMakeVisible(releaseLabel.get()); + UIUtils::setDefaultLabelStyle(releaseLabel); + + amountLabel.reset(new juce::Label("ENV Amount Label", TRANS("Amount"))); + addAndMakeVisible(amountLabel.get()); + UIUtils::setDefaultLabelStyle(amountLabel); + + filterButton.reset(new juce::TextButton("ENV Filter Button")); + addAndMakeVisible(filterButton.get()); + filterButton->setButtonText(TRANS("Filter")); + filterButton->setTooltip(TRANS("Enable low/high pass filters on the envelope follower input")); + filterButton->setLookAndFeel(&_buttonLookAndFeel); + filterButton->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + filterButton->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, baseColour); + filterButton->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + filterButton->addListener(this); + + filterSlider.reset(new FilterSlider()); + addAndMakeVisible(filterSlider.get()); + filterSlider->setRange(AP::LOW_CUT.minValue, AP::LOW_CUT.maxValue, INTERVAL); + filterSlider->setTooltip(TRANS("Low/high pass filter cutoff on the envelope follower input")); + filterSlider->setSliderStyle(juce::Slider::TwoValueVertical); + filterSlider->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + filterSlider->setSkewFactor(0.4); + filterSlider->addListener(this); + filterSlider->setLookAndFeel(&_filterSliderLookAndFeel); + filterSlider->setColour(juce::Slider::thumbColourId, baseColour); + filterSlider->setColour(juce::Slider::backgroundColourId, UIUtils::deactivatedColour); + + scInButton.reset(new juce::TextButton("ENV SC In Button")); + addAndMakeVisible(scInButton.get()); + scInButton->setTooltip(TRANS("Use the sidechain input to trigger this envelope follower")); + scInButton->setButtonText(TRANS("SC In")); + scInButton->setLookAndFeel(&_buttonLookAndFeel); + scInButton->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + scInButton->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, baseColour); + scInButton->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + scInButton->addListener(this); + + _envView.reset(new UIUtils::WaveStylusViewer([&processor, envIndex]() { + return ModelInterface::getEnvLastOutput(processor.manager, envIndex) * ModelInterface::getEnvAmount(processor.manager, envIndex); + })); + addAndMakeVisible(_envView.get()); + _envView->setTooltip(TRANS("Output of this envelope follower")); + _envView->setColour(UIUtils::WaveStylusViewer::lineColourId, UIUtils::getColourForModulationType(MODULATION_TYPE::ENVELOPE)); + + // Start slider label readouts + attackSlider->start(attackLabel.get(), attackLabel->getText()); + releaseSlider->start(releaseLabel.get(), releaseLabel->getText()); + amountSlider->start(amountLabel.get(), amountLabel->getText()); + filterSlider->start(filterButton.get(), filterButton->getButtonText()); + + // Load UI state + attackSlider->setValue(ModelInterface::getEnvAttackTimeMs(_processor.manager, _envIndex), juce::dontSendNotification); + releaseSlider->setValue(ModelInterface::getEnvReleaseTimeMs(_processor.manager, _envIndex), juce::dontSendNotification); + amountSlider->setValue(ModelInterface::getEnvAmount(_processor.manager, _envIndex), juce::dontSendNotification); + filterSlider->setMinValue(ModelInterface::getEnvLowCutHz(_processor.manager, _envIndex), juce::dontSendNotification, true); + filterSlider->setMaxValue(ModelInterface::getEnvHighCutHz(_processor.manager, _envIndex), juce::dontSendNotification); + filterButton->setToggleState(ModelInterface::getEnvFilterEnabled(_processor.manager, _envIndex), juce::dontSendNotification); + filterSlider->setEnabled(ModelInterface::getEnvFilterEnabled(_processor.manager, _envIndex)); + scInButton->setToggleState(ModelInterface::getEnvUseSidechainInput(_processor.manager, _envIndex), juce::dontSendNotification); +} + +ModulationBarEnvelope::~ModulationBarEnvelope() { + // Stop slider label readouts + attackSlider->stop(); + releaseSlider->stop(); + amountSlider->stop(); + + attackSlider->setLookAndFeel(nullptr); + releaseSlider->setLookAndFeel(nullptr); + amountSlider->setLookAndFeel(nullptr); + filterButton->setLookAndFeel(nullptr); + scInButton->setLookAndFeel(nullptr); + + attackSlider = nullptr; + releaseSlider = nullptr; + amountSlider = nullptr; + attackLabel = nullptr; + releaseLabel = nullptr; + amountLabel = nullptr; + filterButton = nullptr; + filterSlider = nullptr; + scInButton = nullptr; + _envView = nullptr; +} + +void ModulationBarEnvelope::resized() { + juce::Rectangle availableArea = getLocalBounds(); + availableArea.removeFromLeft(4); + availableArea.removeFromRight(4); + availableArea.removeFromTop(4); + availableArea.removeFromBottom(4); + + juce::Rectangle inputArea = availableArea.removeFromLeft(availableArea.getWidth() / 5); + + // Enforce the max width for this section (otherwise buttons look silly) + constexpr int MAX_INPUT_AREA_WIDTH {100}; + const int excessWidth {std::max(inputArea.getWidth() - MAX_INPUT_AREA_WIDTH, 0)}; + inputArea.removeFromLeft(excessWidth / 2.0); + inputArea.removeFromRight(excessWidth / 2.0 - 4); + + filterButton->setBounds(inputArea.removeFromTop(24)); + inputArea.removeFromTop(4); + scInButton->setBounds(inputArea.removeFromBottom(24)); + filterSlider->setBounds(inputArea); + + juce::Rectangle controlArea = availableArea.removeFromLeft((availableArea.getWidth() * 2) / 5); + juce::Rectangle upperControlArea = controlArea.removeFromTop(controlArea.getHeight() / 2); + + juce::Rectangle attackArea = upperControlArea.removeFromLeft(upperControlArea.getWidth() / 2); + attackLabel->setBounds(attackArea.removeFromBottom(24)); + attackSlider->setBounds(attackArea); + + releaseLabel->setBounds(upperControlArea.removeFromBottom(24)); + releaseSlider->setBounds(upperControlArea); + + amountLabel->setBounds(controlArea.removeFromBottom(24)); + amountSlider->setBounds(controlArea); + + _envView->setBounds(availableArea); +} + +void ModulationBarEnvelope::sliderValueChanged(juce::Slider* sliderThatWasMoved) { + if (sliderThatWasMoved == attackSlider.get()) { + _processor.setEnvAttackTimeMs(_envIndex, attackSlider->getValue()); + } else if (sliderThatWasMoved == releaseSlider.get()) { + _processor.setEnvReleaseTimeMs(_envIndex, releaseSlider->getValue()); + } else if (sliderThatWasMoved == amountSlider.get()) { + _processor.setEnvAmount(_envIndex, amountSlider->getValue()); + } else if (sliderThatWasMoved == filterSlider.get()) { + _processor.setEnvFilterHz(_envIndex, filterSlider->getMinValue(), filterSlider->getMaxValue()); + } +} + +void ModulationBarEnvelope::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == filterButton.get()) { + filterButton->setToggleState(!filterButton->getToggleState(), juce::dontSendNotification); + _processor.setEnvFilterEnabled(_envIndex, filterButton->getToggleState()); + filterSlider->setEnabled(filterButton->getToggleState()); + } else if (buttonThatWasClicked == scInButton.get()) { + scInButton->setToggleState(!scInButton->getToggleState(), juce::dontSendNotification); + _processor.setEnvUseSidechainInput(_envIndex, scInButton->getToggleState()); + } +} + +void ModulationBarEnvelope::FilterSliderLookAndFeel::drawLinearSliderThumb( + juce::Graphics& g, + int /*x*/, + int /*y*/, + int /*width*/, + int /*height*/, + float /*sliderPos*/, + float minSliderPos, + float maxSliderPos, + const juce::Slider::SliderStyle, + juce::Slider& slider) { + if (slider.isEnabled()) { + g.setColour(slider.findColour(juce::Slider::thumbColourId)); + } else { + g.setColour(slider.findColour(juce::Slider::backgroundColourId)); + } + + juce::Path p; + + const int thumbLeftXPos {static_cast(slider.getWidth() / 2 - 1.5 * getSliderThumbRadius(slider))}; + const int thumbRightXPos {slider.getWidth() - thumbLeftXPos}; + + const int columnLeftXPos {static_cast(slider.getWidth() / 2 - getSliderThumbRadius(slider) / 2)}; + const int columnRightXPos {slider.getWidth() - columnLeftXPos}; + constexpr int halfThumbThickness {1}; + const int cornerRadius {2}; + + // Start in the top right and go clockwise + const int topEdgeYPos {static_cast(maxSliderPos - halfThumbThickness)}; + + p.startNewSubPath(thumbRightXPos, topEdgeYPos); + p.lineTo(thumbRightXPos, maxSliderPos + halfThumbThickness); + p.lineTo(columnRightXPos, maxSliderPos + halfThumbThickness); + + // Bottom right rounded corner + const int bottomEdgeYPos {static_cast(minSliderPos + halfThumbThickness)}; + + p.lineTo(columnRightXPos, bottomEdgeYPos - cornerRadius); + p.addCentredArc(columnRightXPos - cornerRadius, + bottomEdgeYPos - cornerRadius, + cornerRadius, + cornerRadius, + 0, + WECore::CoreMath::DOUBLE_PI / 2, + WECore::CoreMath::DOUBLE_PI); + + // Bottom left thumb + p.lineTo(thumbLeftXPos, bottomEdgeYPos); + p.lineTo(thumbLeftXPos, minSliderPos - halfThumbThickness); + p.lineTo(columnLeftXPos, minSliderPos - halfThumbThickness); + + // Top left rounded corner + p.lineTo(columnLeftXPos, topEdgeYPos + cornerRadius); + p.addCentredArc(columnLeftXPos + cornerRadius, + topEdgeYPos + cornerRadius, + cornerRadius, + cornerRadius, + 0, + 1.5 * WECore::CoreMath::DOUBLE_PI, + 2 * WECore::CoreMath::DOUBLE_PI); + p.closeSubPath(); + g.strokePath(p, juce::PathStrokeType(1.0f)); + g.fillPath(p); +} + +int ModulationBarEnvelope::FilterSliderLookAndFeel::getSliderThumbRadius(juce::Slider& slider) { + return slider.getHeight() / 8; +} + +ModulationBarEnvelope::FilterSlider::FilterSlider() : + WECore::JUCEPlugin::LabelReadoutSlider("Filter Slider") { +} + +void ModulationBarEnvelope::FilterSlider::_updateLabel(const juce::MouseEvent& event) { + if (_isRunning && isEnabled()) { + const float minPosition {getPositionOfValue(getMinValue())}; + const float maxPosition {getPositionOfValue(getMaxValue())}; + + const bool closerToMin { + std::abs(event.y - minPosition) < std::abs(event.y - maxPosition) + }; + + const juce::String valueString( + static_cast(closerToMin ? getMinValue() : getMaxValue())); + _targetCallback(valueString + " Hz"); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarEnvelope.h b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarEnvelope.h new file mode 100644 index 00000000..16202460 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarEnvelope.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include "CoreJUCEPlugin/LabelReadoutSlider.h" +#include "UIUtils.h" +#include "DataModelInterface.hpp" +#include "PluginProcessor.h" + +class ModulationBarEnvelope : public juce::Component, + public juce::Slider::Listener, + public juce::Button::Listener { +public: + ModulationBarEnvelope(SyndicateAudioProcessor& processor, int envIndex); + ~ModulationBarEnvelope() override; + + void resized() override; + void sliderValueChanged(juce::Slider* sliderThatWasMoved) override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + +private: + class FilterSliderLookAndFeel : public WECore::JUCEPlugin::CoreLookAndFeel { + public: + void drawLinearSliderThumb(juce::Graphics& g, + int x, int y, int width, int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + const juce::Slider::SliderStyle, + juce::Slider& slider) override; + + int getSliderThumbRadius(juce::Slider& slider) override; + }; + + class FilterSlider : public WECore::JUCEPlugin::LabelReadoutSlider { + public: + FilterSlider(); + + private: + virtual void _updateLabel(const juce::MouseEvent& event) override; + }; + + SyndicateAudioProcessor& _processor; + int _envIndex; + UIUtils::StandardSliderLookAndFeel _sliderLookAndFeel; + UIUtils::MidAnchoredSliderLookAndFeel _midAnchorSliderLookAndFeel; + UIUtils::ToggleButtonLookAndFeel _buttonLookAndFeel; + FilterSliderLookAndFeel _filterSliderLookAndFeel; + + std::unique_ptr> attackSlider; + std::unique_ptr> releaseSlider; + std::unique_ptr> amountSlider; + std::unique_ptr attackLabel; + std::unique_ptr releaseLabel; + std::unique_ptr amountLabel; + std::unique_ptr filterButton; + std::unique_ptr filterSlider; + std::unique_ptr scInButton; + std::unique_ptr _envView; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationBarEnvelope) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarLfo.cpp b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarLfo.cpp new file mode 100644 index 00000000..b65cc051 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarLfo.cpp @@ -0,0 +1,396 @@ +#include "UIUtils.h" + +#include "ModulationBarLfo.h" +namespace { + const juce::Colour& baseColour = UIUtils::getColourForModulationType(MODULATION_TYPE::LFO); +} + +ModulationBarLfo::ModulationBarLfo(SyndicateAudioProcessor& processor, int lfoIndex) : + _processor(processor), _lfoIndex(lfoIndex) { + + namespace RP = WECore::Richter::Parameters; + constexpr double INTERVAL {0.01}; + + depthSlider.reset(new ModulatableParameter( + ModelInterface::getLFODepthModulationSources(_processor.manager, _lfoIndex), + [&](ModulationSourceDefinition definition) { + _processor.addSourceToLFODepth(_lfoIndex, definition); + }, + [&](ModulationSourceDefinition definition) { + _processor.removeSourceFromLFODepth(_lfoIndex, definition); + }, + [&](int sourceIndex, float value) { + _processor.setLFODepthModulationAmount(_lfoIndex, sourceIndex, value); + }, + [&]() { + return ModelInterface::getLFOModulatedDepthValue(_processor.manager, _lfoIndex); + }, + "LFO Depth Slider", + "LFO Depth Label", + "Depth")); + addAndMakeVisible(depthSlider.get()); + depthSlider->slider->setTooltip(TRANS("Depth of the LFO")); + depthSlider->slider->setRange(RP::DEPTH.minValue, RP::DEPTH.maxValue, INTERVAL); + depthSlider->slider->setDoubleClickReturnValue(true, WECore::Richter::Parameters::DEPTH.defaultValue); + depthSlider->slider->setLookAndFeel(&_sliderLookAndFeel); + depthSlider->slider->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + depthSlider->slider->onValueChange = [&]() { + _processor.setLfoDepth(_lfoIndex, depthSlider->slider->getValue()); + _updateWaveView(); + }; + + freqSlider.reset(new ModulatableParameter( + ModelInterface::getLFOFreqModulationSources(_processor.manager, _lfoIndex), + [&](ModulationSourceDefinition definition) { + _processor.addSourceToLFOFreq(_lfoIndex, definition); + }, + [&](ModulationSourceDefinition definition) { + _processor.removeSourceFromLFOFreq(_lfoIndex, definition); + }, + [&](int sourceIndex, float value) { + _processor.setLFOFreqModulationAmount(_lfoIndex, sourceIndex, value); + }, + [&]() { + return ModelInterface::getLFOModulatedFreqValue(_processor.manager, _lfoIndex); + }, + "LFO Freq Slider", + "LFO Freq Label", + "Rate")); + addAndMakeVisible(freqSlider.get()); + freqSlider->slider->setTooltip(TRANS("Frequency of the LFO in Hz")); + freqSlider->slider->setRange(RP::FREQ.minValue, RP::FREQ.maxValue, INTERVAL); + freqSlider->slider->setDoubleClickReturnValue(true, WECore::Richter::Parameters::FREQ.defaultValue); + freqSlider->slider->setLookAndFeel(&_sliderLookAndFeel); + freqSlider->slider->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + freqSlider->slider->onValueChange = [&]() { + _processor.setLfoFreq(_lfoIndex, freqSlider->slider->getValue()); + }; + + waveComboBox.reset(new juce::ComboBox("LFO Wave")); + addAndMakeVisible(waveComboBox.get()); + waveComboBox->setTooltip(TRANS("Wave shape for this LFO")); + waveComboBox->setEditableText(false); + waveComboBox->setJustificationType(juce::Justification::centredLeft); + waveComboBox->setTextWhenNothingSelected(juce::String()); + waveComboBox->setTextWhenNoChoicesAvailable(TRANS("(no choices)")); + waveComboBox->addItem(TRANS("Sine"), 1); + waveComboBox->addItem(TRANS("Square"), 2); + waveComboBox->addItem(TRANS("Saw"), 3); + waveComboBox->addItem(TRANS("SC Comp"), 4); + waveComboBox->setLookAndFeel(&_comboBoxLookAndFeel); + waveComboBox->setColour(juce::ComboBox::textColourId, baseColour); + waveComboBox->setColour(juce::ComboBox::arrowColourId, baseColour); + waveComboBox->addListener(this); + + _comboBoxLookAndFeel.setColour(juce::PopupMenu::backgroundColourId, UIUtils::slotBackgroundColour); + _comboBoxLookAndFeel.setColour(juce::PopupMenu::textColourId, baseColour); + _comboBoxLookAndFeel.setColour(juce::PopupMenu::highlightedBackgroundColourId, baseColour); + _comboBoxLookAndFeel.setColour(juce::PopupMenu::highlightedTextColourId, UIUtils::slotBackgroundColour); + + tempoSyncButton.reset(new juce::TextButton("LFO Tempo Sync Button")); + addAndMakeVisible(tempoSyncButton.get()); + tempoSyncButton->setTooltip(TRANS("Sync LFO frequency to host tempo")); + tempoSyncButton->setButtonText(TRANS("Tempo")); + tempoSyncButton->setLookAndFeel(&_buttonLookAndFeel); + tempoSyncButton->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + tempoSyncButton->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, baseColour); + tempoSyncButton->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + tempoSyncButton->addListener(this); + + tempoNumerSlider.reset(new juce::Slider("LFO Tempo Numer Slider")); + addAndMakeVisible(tempoNumerSlider.get()); + tempoNumerSlider->setTooltip(TRANS("Numerator of the tempo sync value")); + tempoNumerSlider->setRange(RP::TEMPONUMER.minValue, RP::TEMPONUMER.maxValue, 1); + tempoNumerSlider->setSliderStyle(juce::Slider::IncDecButtons); + tempoNumerSlider->setTextBoxStyle(juce::Slider::TextBoxLeft, false, 40, 20); + tempoNumerSlider->setColour(juce::Slider::textBoxTextColourId, baseColour); + tempoNumerSlider->setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0x00000000)); + tempoNumerSlider->setColour(juce::Slider::textBoxOutlineColourId, juce::Colour(0x00000000)); + tempoNumerSlider->setColour(juce::TextButton::textColourOnId, baseColour); + tempoNumerSlider->addListener(this); + tempoNumerSlider->setIncDecButtonsMode(juce::Slider::incDecButtonsDraggable_Vertical); + tempoNumerSlider->setLookAndFeel(&_tempoSliderLookAndFeel); + + tempoDenomSlider.reset(new juce::Slider("LFO Tempo Denom Slider")); + addAndMakeVisible(tempoDenomSlider.get()); + tempoDenomSlider->setTooltip(TRANS("Denominator of the tempo sync value")); + tempoDenomSlider->setRange(RP::TEMPODENOM.minValue, RP::TEMPODENOM.maxValue, 1); + tempoDenomSlider->setSliderStyle(juce::Slider::IncDecButtons); + tempoDenomSlider->setTextBoxStyle(juce::Slider::TextBoxLeft, false, 40, 20); + tempoDenomSlider->setColour(juce::Slider::textBoxTextColourId, baseColour); + tempoDenomSlider->setColour(juce::Slider::textBoxBackgroundColourId, juce::Colour(0x00000000)); + tempoDenomSlider->setColour(juce::Slider::textBoxOutlineColourId, juce::Colour(0x00000000)); + tempoDenomSlider->setColour(juce::TextButton::textColourOnId, baseColour); + tempoDenomSlider->addListener(this); + tempoDenomSlider->setIncDecButtonsMode(juce::Slider::incDecButtonsDraggable_Vertical); + tempoDenomSlider->setLookAndFeel(&_tempoSliderLookAndFeel); + + phaseSlider.reset(new ModulatableParameter( + ModelInterface::getLFOPhaseModulationSources(_processor.manager, _lfoIndex), + [&](ModulationSourceDefinition definition) { + _processor.addSourceToLFOPhase(_lfoIndex, definition); + }, + [&](ModulationSourceDefinition definition) { + _processor.removeSourceFromLFOPhase(_lfoIndex, definition); + }, + [&](int sourceIndex, float value) { + _processor.setLFOPhaseModulationAmount(_lfoIndex, sourceIndex, value); + }, + [&]() { + return ModelInterface::getLFOModulatedPhaseValue(_processor.manager, _lfoIndex); + }, + "LFO Phase Slider", + "LFO Phase Label", + "Phase")); + addAndMakeVisible(phaseSlider.get()); + phaseSlider->slider->setTooltip(TRANS("Phase shift the LFO by up to 360 degrees")); + phaseSlider->slider->setRange(RP::PHASE.minValue, RP::PHASE.maxValue, INTERVAL); + phaseSlider->slider->setDoubleClickReturnValue(true, WECore::Richter::Parameters::PHASE.defaultValue); + phaseSlider->slider->setLookAndFeel(&_sliderLookAndFeel); + phaseSlider->slider->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + phaseSlider->slider->onValueChange = [&]() { + _processor.setLfoManualPhase(_lfoIndex, phaseSlider->slider->getValue()); + _updateWaveView(); + }; + + waveView.reset(new WECore::Richter::WaveViewer()); + addAndMakeVisible(waveView.get()); + waveView->setName("LFO Wave View"); + waveView->setTooltip(TRANS("Wave shape that will be output by this LFO")); + waveView->setColour(WECore::Richter::WaveViewer::highlightColourId, baseColour); + + invertButton.reset(new juce::TextButton("LFO Invert Button")); + addAndMakeVisible(invertButton.get()); + invertButton->setTooltip(TRANS("Invert the LFO output")); + invertButton->setButtonText(TRANS("Invert")); + invertButton->setLookAndFeel(&_buttonLookAndFeel); + invertButton->setColour(juce::TextButton::buttonOnColourId, baseColour); + invertButton->setColour(juce::TextButton::textColourOnId, UIUtils::backgroundColour); + invertButton->setColour(juce::TextButton::textColourOffId, baseColour); + invertButton->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + invertButton->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, baseColour); + invertButton->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + invertButton->addListener(this); + + outputModeButtons.reset(new UIUtils::UniBiModeButtons( + [&processor, lfoIndex]() { processor.setLfoOutputMode(lfoIndex, WECore::Richter::Parameters::OUTPUTMODE.UNIPOLAR); }, + [&processor, lfoIndex]() { processor.setLfoOutputMode(lfoIndex, WECore::Richter::Parameters::OUTPUTMODE.BIPOLAR); }, + [&processor, lfoIndex]() { return ModelInterface::getLfoOutputMode(processor.manager, lfoIndex) == 1 ? true : false; }, + [&processor, lfoIndex]() { return ModelInterface::getLfoOutputMode(processor.manager, lfoIndex) == 2 ? true : false; }, + baseColour)); + addAndMakeVisible(outputModeButtons.get()); + + // Load UI state + tempoSyncButton->setToggleState(ModelInterface::getLfoTempoSyncSwitch(_processor.manager, _lfoIndex), juce::dontSendNotification); + invertButton->setToggleState(ModelInterface::getLfoInvertSwitch(_processor.manager, _lfoIndex), juce::dontSendNotification); + waveComboBox->setSelectedId(ModelInterface::getLfoWave(_processor.manager, _lfoIndex), juce::dontSendNotification); + depthSlider->slider->setValue(ModelInterface::getLfoDepth(_processor.manager, _lfoIndex), juce::dontSendNotification); + freqSlider->slider->setValue(ModelInterface::getLfoFreq(_processor.manager, _lfoIndex), juce::dontSendNotification); + phaseSlider->slider->setValue(ModelInterface::getLfoManualPhase(_processor.manager, _lfoIndex), juce::dontSendNotification); + tempoNumerSlider->setValue(ModelInterface::getLfoTempoNumer(_processor.manager, _lfoIndex), juce::dontSendNotification); + tempoDenomSlider->setValue(ModelInterface::getLfoTempoDenom(_processor.manager, _lfoIndex), juce::dontSendNotification); + + _tempoSliderLookAndFeel.setColour(juce::TextButton::textColourOnId, baseColour); + + _updateTempoToggles(); + _updateWaveView(); +} + +ModulationBarLfo::~ModulationBarLfo() { + freqSlider->slider->setLookAndFeel(nullptr); + depthSlider->slider->setLookAndFeel(nullptr); + phaseSlider->slider->setLookAndFeel(nullptr); + invertButton->setLookAndFeel(nullptr); + waveComboBox->setLookAndFeel(nullptr); + tempoNumerSlider->setLookAndFeel(nullptr); + tempoDenomSlider->setLookAndFeel(nullptr); + + freqSlider = nullptr; + depthSlider = nullptr; + waveComboBox = nullptr; + tempoSyncButton = nullptr; + tempoNumerSlider = nullptr; + tempoDenomSlider = nullptr; + phaseSlider = nullptr; + waveView = nullptr; + invertButton = nullptr; +} + +void ModulationBarLfo::resized() { + juce::Rectangle availableArea = getLocalBounds(); + availableArea.reduce(4, 4); + + constexpr int BUTTON_HEIGHT {24}; + + const int minParameterWidth {availableArea.getWidth() / 6}; + + // Freq/tempo area + juce::Rectangle freqArea = availableArea.removeFromLeft(minParameterWidth); + + // Enforce the max width for this section (otherwise buttons look silly) + constexpr int MAX_FREQ_AREA_WIDTH {100}; + const int excessWidth {std::max(freqArea.getWidth() - MAX_FREQ_AREA_WIDTH, 0)}; + freqArea = freqArea.reduced(excessWidth / 2.0, 0); + + const int freqSliderSize {freqArea.getHeight() / 2 - BUTTON_HEIGHT}; + tempoSyncButton->setBounds(freqArea.removeFromTop(BUTTON_HEIGHT)); + + // Copy this now, we'll need it later + juce::Rectangle tempoSlidersArea = freqArea; + + freqSlider->setBounds(freqArea); + + // Now the tempo sliders + juce::FlexBox tempoSlidersFlexBox; + tempoSlidersFlexBox.flexDirection = juce::FlexBox::Direction::column; + tempoSlidersFlexBox.flexWrap = juce::FlexBox::Wrap::wrap; + tempoSlidersFlexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + tempoSlidersFlexBox.alignContent = juce::FlexBox::AlignContent::center; + + tempoSlidersFlexBox.items.add(juce::FlexItem(*tempoNumerSlider.get()).withMinWidth(tempoSlidersArea.getWidth() * 0.75).withMinHeight(40)); + tempoSlidersFlexBox.items.add(juce::FlexItem(*tempoDenomSlider.get()).withMinWidth(tempoSlidersArea.getWidth() * 0.75).withMinHeight(40)); + + tempoSlidersFlexBox.performLayout(tempoSlidersArea); + + depthSlider->setBounds(availableArea.removeFromLeft(minParameterWidth).withTrimmedTop(BUTTON_HEIGHT)); + phaseSlider->setBounds(availableArea.removeFromLeft(minParameterWidth).withTrimmedTop(BUTTON_HEIGHT)); + + // Wave area + constexpr int MAX_WAVE_AREA_WIDTH {450}; + const int excessWaveWidth {std::max(availableArea.getWidth() - MAX_WAVE_AREA_WIDTH, 0)}; + availableArea.removeFromLeft(excessWaveWidth / 2.0); + availableArea.removeFromRight(excessWaveWidth / 2.0); + + juce::FlexBox waveFlexBox; + waveFlexBox.flexDirection = juce::FlexBox::Direction::row; + waveFlexBox.flexWrap = juce::FlexBox::Wrap::wrap; + waveFlexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + waveFlexBox.alignContent = juce::FlexBox::AlignContent::center; + + const int buttonWidth {static_cast(availableArea.getWidth() * 0.3)}; + waveFlexBox.items.add(juce::FlexItem(*invertButton.get()).withMinWidth(buttonWidth).withMinHeight(BUTTON_HEIGHT)); + waveFlexBox.items.add(juce::FlexItem(*outputModeButtons.get()).withMinWidth(buttonWidth).withMinHeight(BUTTON_HEIGHT)); + waveFlexBox.items.add(juce::FlexItem(*waveComboBox.get()).withMinWidth(buttonWidth).withMinHeight(BUTTON_HEIGHT)); + + waveFlexBox.performLayout(availableArea.removeFromBottom(BUTTON_HEIGHT).toFloat()); + + waveView->setBounds(availableArea); +} + +void ModulationBarLfo::sliderValueChanged(juce::Slider* sliderThatWasMoved) { + if (sliderThatWasMoved == tempoNumerSlider.get()) { + _processor.setLfoTempoNumer(_lfoIndex, tempoNumerSlider->getValue()); + } else if (sliderThatWasMoved == tempoDenomSlider.get()) { + _processor.setLfoTempoDenom(_lfoIndex, tempoDenomSlider->getValue()); + } + + _updateWaveView(); +} + +void ModulationBarLfo::comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) { + if (comboBoxThatHasChanged == waveComboBox.get()) { + _processor.setLfoWave(_lfoIndex, waveComboBox->getSelectedId()); + } + + _updateWaveView(); +} + +void ModulationBarLfo::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == tempoSyncButton.get()) { + tempoSyncButton->setToggleState(!tempoSyncButton->getToggleState(), juce::dontSendNotification); + _processor.setLfoTempoSyncSwitch(_lfoIndex, tempoSyncButton->getToggleState()); + } else if (buttonThatWasClicked == invertButton.get()) { + invertButton->setToggleState(!invertButton->getToggleState(), juce::dontSendNotification); + _processor.setLfoInvertSwitch(_lfoIndex, invertButton->getToggleState()); + } + + _updateTempoToggles(); + _updateWaveView(); +} + + +void ModulationBarLfo::TempoSliderLookAndFeel::drawButtonBackground( + juce::Graphics& /*g*/, + juce::Button& /*button*/, + const juce::Colour& /*backgroundColour*/, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) { + // do nothing +} + +void ModulationBarLfo::TempoSliderLookAndFeel::drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) { + g.setColour(findColour(juce::TextButton::textColourOnId)); + + juce::Rectangle area = textButton.getLocalBounds().withPosition(0, 0).reduced(8); + + constexpr int MAX_CARAT_WIDTH {10}; + const int excessWidth {std::max(area.getWidth() - MAX_CARAT_WIDTH, 0)}; + area = area.reduced(excessWidth / 2.0, 0); + + const int horizontalMid {area.getWidth() / 2 + area.getX()}; + const int centreY {area.getY() + (textButton.getButtonText() == "+" ? 0 : area.getHeight())}; + const int endY {area.getY() + (textButton.getButtonText() == "+" ? area.getHeight() : 0)}; + juce::Path p; + + p.startNewSubPath(area.getX(), endY); + p.lineTo(horizontalMid, centreY); + p.lineTo(area.getX() + area.getWidth(), endY); + + g.strokePath(p, juce::PathStrokeType(1)); +} + +juce::Slider::SliderLayout ModulationBarLfo::TempoSliderLookAndFeel::getSliderLayout( + juce::Slider& slider) { + juce::Rectangle area = slider.getLocalBounds(); + + juce::Slider::SliderLayout retVal; + retVal.sliderBounds = area.removeFromRight(area.getWidth() / 2); + retVal.textBoxBounds = area; + + return retVal; +} + +juce::Label* ModulationBarLfo::TempoSliderLookAndFeel::createSliderTextBox(juce::Slider& slider) { + juce::Label* label = UIUtils::StandardSliderLookAndFeel::createSliderTextBox(slider); + + // Use a bigger font + label->setFont(juce::Font(20.00f, juce::Font::plain).withTypefaceStyle("Regular")); + + return label; +} + +void ModulationBarLfo::_updateWaveView() { + const double* wave {nullptr}; + + const int waveValue {ModelInterface::getLfoWave(_processor.manager, _lfoIndex)}; + if (waveValue == 1) { + wave = WECore::Richter::Wavetables::getInstance()->getSine(); + } else if (waveValue == 2) { + wave = WECore::Richter::Wavetables::getInstance()->getSquare(); + } else if (waveValue == 3) { + wave = WECore::Richter::Wavetables::getInstance()->getSaw(); + } else { + wave = WECore::Richter::Wavetables::getInstance()->getSidechain(); + } + + waveView->setWave(wave, + ModelInterface::getLfoDepth(_processor.manager, _lfoIndex), + ModelInterface::getLfoManualPhase(_processor.manager, _lfoIndex), + ModelInterface::getLfoInvertSwitch(_processor.manager, _lfoIndex)); + waveView->repaint(); +} + +void ModulationBarLfo::_updateTempoToggles() { + if (tempoSyncButton->getToggleState()) { + freqSlider->setVisible(false); + tempoNumerSlider->setVisible(true); + tempoDenomSlider->setVisible(true); + } else { + freqSlider->setVisible(true); + tempoNumerSlider->setVisible(false); + tempoDenomSlider->setVisible(false); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarLfo.h b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarLfo.h new file mode 100644 index 00000000..69640c18 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarLfo.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include "CoreJUCEPlugin/LabelReadoutSlider.h" +#include "RichterLFO/UI/RichterWaveViewer.h" +#include "RichterLFO/RichterLFO.h" +#include "UIUtils.h" +#include "PluginProcessor.h" +#include "ModulatableParameter.hpp" + +class ModulationBarLfo : public juce::Component, + public juce::Slider::Listener, + public juce::ComboBox::Listener, + public juce::Button::Listener { +public: + ModulationBarLfo(SyndicateAudioProcessor& processor, int lfoIndex); + ~ModulationBarLfo() override; + + void resized() override; + void sliderValueChanged(juce::Slider* sliderThatWasMoved) override; + void comboBoxChanged(juce::ComboBox* comboBoxThatHasChanged) override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + +private: + class TempoSliderLookAndFeel : public UIUtils::StandardSliderLookAndFeel { + public: + void drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool isMouseOverButton, + bool isButtonDown) override; + + void drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool isMouseOverButton, + bool isButtonDown) override; + + juce::Slider::SliderLayout getSliderLayout(juce::Slider& slider) override; + + juce::Label* createSliderTextBox(juce::Slider& slider) override; + }; + + SyndicateAudioProcessor& _processor; + int _lfoIndex; + UIUtils::StandardSliderLookAndFeel _sliderLookAndFeel; + UIUtils::ToggleButtonLookAndFeel _buttonLookAndFeel; + UIUtils::StandardComboBoxLookAndFeel _comboBoxLookAndFeel; + TempoSliderLookAndFeel _tempoSliderLookAndFeel; + + void _updateWaveView(); + void _updateTempoToggles(); + + std::unique_ptr freqSlider; + std::unique_ptr depthSlider; + std::unique_ptr phaseSlider; + std::unique_ptr waveComboBox; + std::unique_ptr tempoSyncButton; + std::unique_ptr tempoNumerSlider; + std::unique_ptr tempoDenomSlider; + std::unique_ptr waveView; + std::unique_ptr invertButton; + std::unique_ptr outputModeButtons; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationBarLfo) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarRandom.cpp b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarRandom.cpp new file mode 100644 index 00000000..d1771d35 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarRandom.cpp @@ -0,0 +1,123 @@ +#include "UIUtils.h" + +#include "ModulationBarRandom.hpp" + +namespace { + const juce::Colour& baseColour = UIUtils::getColourForModulationType(MODULATION_TYPE::RANDOM); +} + +ModulationBarRandom::ModulationBarRandom(SyndicateAudioProcessor& processor, int randomIndex) : + _processor(processor), _randomIndex(randomIndex) { + + namespace PP = WECore::Perlin::Parameters; + constexpr double INTERVAL {0.01}; + + freqSlider.reset(new ModulatableParameter( + ModelInterface::getRandomFreqModulationSources(_processor.manager, _randomIndex), + [&](ModulationSourceDefinition definition) { + _processor.addSourceToRandomFreq(_randomIndex, definition); + }, + [&](ModulationSourceDefinition definition) { + _processor.removeSourceFromRandomFreq(_randomIndex, definition); + }, + [&](int sourceIndex, float value) { + _processor.setRandomFreqModulationAmount(_randomIndex, sourceIndex, value); + }, + [&]() { + return ModelInterface::getRandomModulatedFreqValue(_processor.manager, _randomIndex); + }, + "Random Freq Slider", + "Random Freq Label", + "Rate")); + addAndMakeVisible(freqSlider.get()); + freqSlider->slider->setTooltip(TRANS("Frequency of the random source in Hz")); + freqSlider->slider->setRange(PP::FREQ.minValue, PP::FREQ.maxValue, INTERVAL); + freqSlider->slider->setDoubleClickReturnValue(true, PP::FREQ.defaultValue); + freqSlider->slider->setLookAndFeel(&_sliderLookAndFeel); + freqSlider->slider->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + freqSlider->slider->onValueChange = [&]() { + _processor.setRandomFreq(_randomIndex, freqSlider->slider->getValue()); + }; + + depthSlider.reset(new ModulatableParameter( + ModelInterface::getRandomDepthModulationSources(_processor.manager, _randomIndex), + [&](ModulationSourceDefinition definition) { + _processor.addSourceToRandomDepth(_randomIndex, definition); + }, + [&](ModulationSourceDefinition definition) { + _processor.removeSourceFromRandomDepth(_randomIndex, definition); + }, + [&](int sourceIndex, float value) { + _processor.setRandomDepthModulationAmount(_randomIndex, sourceIndex, value); + }, + [&]() { + return ModelInterface::getRandomModulatedDepthValue(_processor.manager, _randomIndex); + }, + "Random Depth Slider", + "Random Depth Label", + "Depth")); + addAndMakeVisible(depthSlider.get()); + depthSlider->slider->setTooltip(TRANS("Depth of the random source")); + depthSlider->slider->setRange(PP::DEPTH.minValue, PP::DEPTH.maxValue, INTERVAL); + depthSlider->slider->setDoubleClickReturnValue(true, PP::DEPTH.defaultValue); + depthSlider->slider->setLookAndFeel(&_sliderLookAndFeel); + depthSlider->slider->setColour(juce::Slider::rotarySliderFillColourId, baseColour); + depthSlider->slider->onValueChange = [&]() { + _processor.setRandomDepth(_randomIndex, depthSlider->slider->getValue()); + }; + + _sourceView.reset(new UIUtils::WaveStylusViewer([&processor, randomIndex]() { + return ModelInterface::getRandomLastOutput(processor.manager, randomIndex); + })); + addAndMakeVisible(_sourceView.get()); + _sourceView->setTooltip(TRANS("Output of this random source")); + _sourceView->setColour(UIUtils::WaveStylusViewer::lineColourId, UIUtils::getColourForModulationType(MODULATION_TYPE::RANDOM)); + + outputModeButtons.reset(new UIUtils::UniBiModeButtons( + [&processor, randomIndex]() { processor.setRandomOutputMode(randomIndex, WECore::Perlin::Parameters::OUTPUTMODE.UNIPOLAR); }, + [&processor, randomIndex]() { processor.setRandomOutputMode(randomIndex, WECore::Perlin::Parameters::OUTPUTMODE.BIPOLAR); }, + [&processor, randomIndex]() { return ModelInterface::getRandomOutputMode(processor.manager, randomIndex) == 1 ? true : false; }, + [&processor, randomIndex]() { return ModelInterface::getRandomOutputMode(processor.manager, randomIndex) == 2 ? true : false; }, + baseColour)); + addAndMakeVisible(outputModeButtons.get()); + + // Load UI state + freqSlider->slider->setValue(ModelInterface::getRandomFreq(_processor.manager, _randomIndex), juce::dontSendNotification); + depthSlider->slider->setValue(ModelInterface::getRandomDepth(_processor.manager, _randomIndex), juce::dontSendNotification); +} + +ModulationBarRandom::~ModulationBarRandom() { + freqSlider->slider->setLookAndFeel(nullptr); + depthSlider->slider->setLookAndFeel(nullptr); + + freqSlider = nullptr; + depthSlider = nullptr; +} + +void ModulationBarRandom::resized() { + juce::Rectangle availableArea = getLocalBounds(); + availableArea.reduce(4, 4); + + const int minParameterWidth {availableArea.getWidth() / 4}; + + // Sliders + freqSlider->setBounds(availableArea.removeFromLeft(minParameterWidth)); + depthSlider->setBounds(availableArea.removeFromLeft(minParameterWidth)); + + // Buttons + constexpr int BUTTON_HEIGHT {24}; + + juce::FlexBox waveFlexBox; + waveFlexBox.flexDirection = juce::FlexBox::Direction::row; + waveFlexBox.flexWrap = juce::FlexBox::Wrap::wrap; + waveFlexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + waveFlexBox.alignContent = juce::FlexBox::AlignContent::center; + + const int buttonWidth {static_cast(availableArea.getWidth() * 0.3)}; + waveFlexBox.items.add(juce::FlexItem(*outputModeButtons.get()).withMinWidth(buttonWidth).withMinHeight(BUTTON_HEIGHT)); + + waveFlexBox.performLayout(availableArea.removeFromBottom(BUTTON_HEIGHT).toFloat()); + + // Wave area + _sourceView->setBounds(availableArea); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarRandom.hpp b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarRandom.hpp new file mode 100644 index 00000000..4ed3c6a4 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationBarRandom.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "ModulatableParameter.hpp" +#include "UIUtils.h" + +class ModulationBarRandom : public juce::Component { +public: + ModulationBarRandom(SyndicateAudioProcessor& processor, int randomIndex); + ~ModulationBarRandom() override; + + void resized() override; + +private: + SyndicateAudioProcessor& _processor; + int _randomIndex; + + UIUtils::StandardSliderLookAndFeel _sliderLookAndFeel; + + std::unique_ptr freqSlider; + std::unique_ptr depthSlider; + std::unique_ptr _sourceView; + std::unique_ptr outputModeButtons; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ModulationBarRandom) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationButton.cpp b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationButton.cpp new file mode 100644 index 00000000..9c346de6 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationButton.cpp @@ -0,0 +1,171 @@ +#include "ModulationButton.h" + +#include "UIUtils.h" + +namespace { + juce::String definitionToButtonName(const ModulationSourceDefinition& definition) { + juce::String name; + + if (definition.type == MODULATION_TYPE::LFO) { + name = "LFO "; + } else if (definition.type == MODULATION_TYPE::ENVELOPE) { + name = "ENV "; + } else if (definition.type == MODULATION_TYPE::RANDOM) { + name = "RND "; + } + + return name + juce::String(definition.id); + } +} + +ModulationButton::ModulationButton(ModulationSourceDefinition newDefinition, + std::function onSelectCallback, + std::function onRemoveCallback, + juce::DragAndDropContainer* dragContainer) : + definition(newDefinition), + _onSelectCallback(onSelectCallback), + _dragContainer(dragContainer) { + + _buttonLookAndFeel.reset(new ButtonLookAndFeel()); + + const juce::String buttonName = definitionToButtonName(definition); + const juce::String tooltipString( + buttonName + " - Drag the handle to a plugin modulation tray or right click to remove"); + + // Set up the select button + selectButton.reset(new ModulationSelectButton(onRemoveCallback)); + addAndMakeVisible(selectButton.get()); + selectButton->setButtonText(TRANS(buttonName)); + selectButton->addListener(this); + selectButton->setLookAndFeel(_buttonLookAndFeel.get()); + selectButton->setColour(juce::TextButton::buttonColourId, UIUtils::getColourForModulationType(definition.type)); + selectButton->setColour(juce::TextButton::textColourOffId, UIUtils::getColourForModulationType(definition.type)); + selectButton->setColour(juce::TextButton::textColourOnId, UIUtils::slotBackgroundColour); + selectButton->setTooltip(tooltipString); + + // Set up the drag handle + dragHandle.reset(new UIUtils::DragHandle()); + addAndMakeVisible(dragHandle.get()); + dragHandle->setColour(UIUtils::DragHandle::handleColourId, UIUtils::getColourForModulationType(definition.type)); + dragHandle->setTooltip(tooltipString); + dragHandle->addMouseListener(this, false); +} + +ModulationButton::~ModulationButton() { + selectButton->setLookAndFeel(nullptr); + dragHandle->setLookAndFeel(nullptr); + + _buttonLookAndFeel = nullptr; +} + +void ModulationButton::setIsSelected(bool isSelected) { + selectButton->setToggleState(isSelected, juce::dontSendNotification); +} + +bool ModulationButton::getIsSelected() const { + return selectButton->getToggleState(); +} + +void ModulationButton::resized() { + const int labelWidth {getHeight()}; + const int selectButtonWidth {getWidth() - labelWidth}; + + juce::Rectangle area = getLocalBounds(); + + selectButton->setBounds(area.removeFromLeft(selectButtonWidth)); + dragHandle->setBounds(area); +} + +void ModulationButton::paint(juce::Graphics& g) { + // Draw the outline + constexpr int MARGIN {1}; + juce::Rectangle area = dragHandle->getBounds() + .withTrimmedTop(MARGIN).withTrimmedBottom(MARGIN).withTrimmedRight(MARGIN); + + juce::Path p; + const int centreY {area.getHeight() / 2 + area.getY()}; + const int centreX {area.getHeight() / 2 + area.getX()}; + const int radius {area.getHeight() / 2}; + + p.clear(); + p.startNewSubPath(area.getX(), area.getY()); + p.addCentredArc(centreX, + centreY, + radius, + radius, + 0, + 0, + WECore::CoreMath::DOUBLE_PI); + p.lineTo(area.getX(), area.getHeight() + area.getY()); + + g.setColour(UIUtils::slotBackgroundColour); + g.fillPath(p); +} + +void ModulationButton::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == selectButton.get()) { + _onSelectCallback(this); + } +} + +void ModulationButton::mouseDrag(const juce::MouseEvent& /*event*/) { + _dragContainer->startDragging(definition.toVariant(), this); +} + +void ModulationButton::ButtonLookAndFeel::drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& /*backgroundColour*/, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + + constexpr int MARGIN {1}; + juce::Rectangle area = juce::Rectangle(button.getWidth(), button.getHeight()) + .withTrimmedTop(MARGIN).withTrimmedBottom(MARGIN).withTrimmedLeft(MARGIN); + + juce::Path p; + const int centreY {area.getHeight() / 2 + area.getY()}; + const int centreX {area.getHeight() / 2 + area.getX()}; + const int radius {area.getHeight() / 2}; + + p.startNewSubPath(area.getWidth() + area.getX(), area.getY()); + p.lineTo(centreX, area.getY()); + p.addCentredArc(centreX, + centreY, + radius, + radius, + 0, + 0, + -WECore::CoreMath::DOUBLE_PI); + p.lineTo(button.getWidth(), area.getHeight() + area.getY()); + + if (button.getToggleState()) { + g.setColour(button.findColour(juce::TextButton::buttonColourId)); + } else { + g.setColour(button.findColour(juce::TextButton::textColourOnId)); + } + + p.closeSubPath(); + g.fillPath(p); + +} + +void ModulationButton::ModulationSelectButton::mouseDown(const juce::MouseEvent& event) { + // If this is a right click, we need to remove the target on mouseUp + if (event.mods.isRightButtonDown()) { + _isRightClick = true; + } else { + _isRightClick = false; + } + + juce::Button::mouseDown(event); +} + +void ModulationButton::ModulationSelectButton::mouseUp(const juce::MouseEvent& event) { + // If it's a right click make sure the mouse is still over the button + if (_isRightClick && isDown() && isOver()) { + // Don't send an event, just call the callback + _onRemoveCallback(); + } else { + juce::Button::mouseUp(event); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationButton.h b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationButton.h new file mode 100644 index 00000000..6e8dfb2a --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationBar/ModulationButton.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include "ModulationSourceDefinition.hpp" +#include "UIUtils.h" + +class ModulationButton : public juce::Component, + public juce::Button::Listener { +public: + const ModulationSourceDefinition definition; + + ModulationButton(ModulationSourceDefinition newDefinition, + std::function onSelectCallback, + std::function onRemoveCallback, + juce::DragAndDropContainer* dragContainer); + + ~ModulationButton(); + + void setIsSelected(bool isSelected); + bool getIsSelected() const; + + void resized() override; + void paint(juce::Graphics& g) override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + void mouseDrag(const juce::MouseEvent& e) override; + +private: + class ButtonLookAndFeel : public juce::LookAndFeel_V2 { + public: + ButtonLookAndFeel() = default; + ~ButtonLookAndFeel() = default; + + void drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + }; + + class ModulationSelectButton : public juce::TextButton { + public: + ModulationSelectButton(std::function onRemoveCallback) : _onRemoveCallback(onRemoveCallback), _isRightClick(false) {} + + void mouseDown(const juce::MouseEvent& event) override; + void mouseUp(const juce::MouseEvent& event) override; + + private: + std::function _onRemoveCallback; + bool _isRightClick; + }; + + std::unique_ptr selectButton; + std::unique_ptr dragHandle; + std::function _onSelectCallback; + juce::DragAndDropContainer* _dragContainer; + std::unique_ptr _buttonLookAndFeel; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSlider.cpp b/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSlider.cpp new file mode 100644 index 00000000..8ee431fd --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSlider.cpp @@ -0,0 +1,50 @@ +#include "ModulationTargetSlider.hpp" + +#include "General/CoreMath.h" + + +ModulationTargetSlider::ModulationTargetSlider(const juce::String& componentName, + std::function getModulatedValue) : + WECore::JUCEPlugin::LabelReadoutSlider(componentName), + _getModulatedValue(getModulatedValue), + _modulatedValue(0) { + startTimerHz(15); +} + +void ModulationTargetSlider::paint(juce::Graphics& g) { + // Draw the normal slider using LookAndFeel + juce::Slider::paint(g); + + // Draw the inner modulation ring + g.setColour(juce::Colours::grey); + + constexpr double arcGap {WECore::CoreMath::DOUBLE_TAU / 4}; + constexpr double rangeOfMotion {WECore::CoreMath::DOUBLE_TAU - arcGap}; + + const double sliderNormalisedValue {(getValue() - getMinimum()) / + (getMaximum() - getMinimum())}; + const double arcStartPoint {sliderNormalisedValue * rangeOfMotion + arcGap / 2}; + const double arcEndPoint {_modulatedValue * rangeOfMotion + arcGap / 2}; + + constexpr double margin {1.5}; + const double diameter {static_cast(getHeight() - margin * 2)}; + + constexpr int arcSpacing {6}; + juce::Path p; + p.addCentredArc(getWidth() / 2, + getHeight() / 2, + diameter / 2 - arcSpacing, + diameter / 2 - arcSpacing, + WECore::CoreMath::DOUBLE_PI, + arcStartPoint, + arcEndPoint, + true); + + g.strokePath(p, juce::PathStrokeType(2.0f)); +} + +void ModulationTargetSlider::timerCallback() { + // _modulatedValue should be between 0 and 1 + _modulatedValue = _getModulatedValue() / getRange().getLength(); + repaint(); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSlider.hpp b/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSlider.hpp new file mode 100644 index 00000000..c54a681d --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSlider.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include "CoreJUCEPlugin/LabelReadoutSlider.h" + +class ModulationTargetSlider : public WECore::JUCEPlugin::LabelReadoutSlider, + public juce::Timer { +public: + ModulationTargetSlider(const juce::String& componentName, std::function getModulatedValue); + ~ModulationTargetSlider() = default; + + void paint(juce::Graphics& g) override; + + void timerCallback() override; + +private: + std::function _getModulatedValue; + float _modulatedValue; +}; \ No newline at end of file diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSourceSlider.cpp b/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSourceSlider.cpp new file mode 100644 index 00000000..1ecfe590 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSourceSlider.cpp @@ -0,0 +1,168 @@ +#include "ModulationTargetSourceSlider.hpp" +#include "UIUtils.h" + +ModulationTargetSourceSlider::ModulationTargetSourceSlider( + ModulationSourceDefinition definition, + std::function onRemoveCallback) + : _definition(definition), _onRemoveCallback(onRemoveCallback), _isRightClick(false) { + setRange(-1, 1, 0); + setDoubleClickReturnValue(true, 0); + + _idLabel.reset(new juce::Label("ID Label", juce::String(_definition.id))); + addAndMakeVisible(_idLabel.get()); + _idLabel->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle("Regular")); + _idLabel->setJustificationType(juce::Justification::centred); + _idLabel->setEditable(true, true, false); + _idLabel->setColour(juce::Label::textColourId, UIUtils::neutralColour); + _idLabel->setColour(juce::Label::backgroundColourId, juce::Colour(0x00000000)); + _idLabel->setInterceptsMouseClicks(false, false); + + // TODO use custom effect that can be thicker without feathering like the glow effect does + _glowEffect.reset(new juce::GlowEffect()); + _glowEffect->setGlowProperties(2, UIUtils::backgroundColour); + _idLabel->setComponentEffect(_glowEffect.get()); +} + +void ModulationTargetSourceSlider::resized() { + _idLabel->setBounds(getLocalBounds()); +} + +void ModulationTargetSourceSlider::paint(juce::Graphics& g) { + // Figure out the dimensions of the circle we need to draw + const int centreX {getWidth() / 2}; + const int centreY {getHeight() / 2}; + const int radius {getHeight() / 2}; + const double circleHeight {radius * getValue()}; + const double centreAngle {std::asin(circleHeight / radius)}; + + // Start with the left arc + juce::Path p; + p.addCentredArc(centreX, + centreY, + radius, + radius, + -0.5 * WECore::CoreMath::DOUBLE_PI, + centreAngle, + 0, + true); + + // Then the horizontal centre line + p.lineTo(getWidth(), centreY); + + // Finally the right arc + p.addCentredArc(centreX, + centreY, + radius, + radius, + 0.5 * WECore::CoreMath::DOUBLE_PI, + 0, + -centreAngle); + + // Fill the path + p.closeSubPath(); + g.setColour(UIUtils::getColourForModulationType(_definition.type)); + g.fillPath(p); +} + +void ModulationTargetSourceSlider::mouseDown(const juce::MouseEvent& event) { + // If this is a right click, we need to remove the source on mouseUp + if (event.mods.isRightButtonDown()) { + _isRightClick = true; + } else { + _isRightClick = false; + + // If it's not a right click, pass the event through so the slider can be moved + juce::Slider::mouseDown(event); + } +} + +void ModulationTargetSourceSlider::mouseUp(const juce::MouseEvent& event) { + if (_isRightClick) { + // If it's a right click make sure the mouse is still over the slider + if (contains(event.getPosition())) { + // Don't send an event, just call the callback + _onRemoveCallback(_definition); + } + } else { + // Not a right click, pass the event through as normal + juce::Slider::mouseUp(event); + } +} + +ModulationTargetSourceSliders::ModulationTargetSourceSliders( + std::vector> sources, + std::function onSliderMovedCallback, + std::function onRemoveCallback) : + _onSliderMovedCallback(onSliderMovedCallback), + _onRemoveCallback(onRemoveCallback) { + + // Restoring target slots + for (const std::shared_ptr thisSource : sources) { + addSource(ModulationSourceDefinition(thisSource->definition.id, thisSource->definition.type)); + _sliders[_sliders.size() - 1]->setValue(thisSource->modulationAmount, juce::dontSendNotification); + } +} + +void ModulationTargetSourceSliders::resized() { + _refreshSlotPositions(); +} + +void ModulationTargetSourceSliders::paint(juce::Graphics& g) { + // Draw an empty source slot if there are no sources + if (_sliders.size() == 0) { + constexpr int MARGIN {1}; + const int diameter {getHeight() - 2 * MARGIN}; + juce::Path p; + p.addEllipse( + getWidth() / 2 - diameter / 2, + MARGIN, + diameter, + diameter); + + // Draw the outline + const float dashLengths[] = {4.0f, 4.0f}; + g.setColour(UIUtils::neutralColour.withAlpha(0.5f)); + g.strokePath(p, juce::PathStrokeType(1.0f)); + } +} + +void ModulationTargetSourceSliders::sliderValueChanged(juce::Slider* sliderThatWasMoved) { + for (int slotIndex {0}; slotIndex < _sliders.size(); slotIndex++) { + if (sliderThatWasMoved == _sliders[slotIndex].get()) { + _onSliderMovedCallback(slotIndex, sliderThatWasMoved->getValue()); + } + } +} + +void ModulationTargetSourceSliders::addSource(ModulationSourceDefinition definition) { + auto newModulationSlot = std::make_unique( + definition, [&](ModulationSourceDefinition definition) { _onRemoveCallback(definition); }); + + addAndMakeVisible(newModulationSlot.get()); + newModulationSlot->setSliderStyle(juce::Slider::RotaryVerticalDrag); + newModulationSlot->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + newModulationSlot->addListener(this); + newModulationSlot->setTooltip("Drag up or down to add positive or negative modulation, right click to remove"); + _sliders.push_back(std::move(newModulationSlot)); + _refreshSlotPositions(); +} + +void ModulationTargetSourceSliders::removeLastSource() { + _sliders.pop_back(); + _refreshSlotPositions(); +} + +void ModulationTargetSourceSliders::_refreshSlotPositions() { + juce::FlexBox flexBox; + flexBox.flexDirection = juce::FlexBox::Direction::row; + flexBox.flexWrap = juce::FlexBox::Wrap::noWrap; + flexBox.justifyContent = juce::FlexBox::JustifyContent::center; + flexBox.alignContent = juce::FlexBox::AlignContent::center; + + for (std::unique_ptr& slider : _sliders) { + flexBox.items.add(juce::FlexItem(*slider).withMinHeight(getHeight()).withMinWidth(UIUtils::PLUGIN_MOD_TARGET_SLIDER_WIDTH)); + } + + juce::Rectangle availableArea = getLocalBounds(); + flexBox.performLayout(availableArea.toFloat()); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSourceSlider.hpp b/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSourceSlider.hpp new file mode 100644 index 00000000..0422228a --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/ModulationTargetSourceSlider.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include "ModulationSourceDefinition.hpp" +#include "ChainSlots.hpp" + +/** + * Displays the small modulation source slider below the modulation target. A single target may have + * multiple slots each representing a different modulation source. + */ +class ModulationTargetSourceSlider : public juce::Slider { +public: + + ModulationTargetSourceSlider(ModulationSourceDefinition definition, + std::function onRemoveCallback); + ~ModulationTargetSourceSlider() = default; + + void resized() override; + void paint(juce::Graphics& g) override; + + void mouseDown(const juce::MouseEvent& event) override; + void mouseUp(const juce::MouseEvent& event) override; + +private: + const ModulationSourceDefinition _definition; + std::function _onRemoveCallback; + + std::unique_ptr _idLabel; + std::unique_ptr _glowEffect; + bool _isRightClick; +}; + +class ModulationTargetSourceSliders : public juce::Component, + public juce::Slider::Listener { +public: + ModulationTargetSourceSliders( + std::vector> sources, + std::function onSliderMovedCallback, + std::function onRemoveCallback); + ~ModulationTargetSourceSliders() = default; + + void resized() override; + void paint(juce::Graphics& g) override; + void sliderValueChanged(juce::Slider* sliderThatWasMoved) override; + + void addSource(ModulationSourceDefinition definition); + void removeLastSource(); + +private: + std::function _onSliderMovedCallback; + std::function _onRemoveCallback; + std::vector> _sliders; + + void _refreshSlotPositions(); +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/OutputComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/OutputComponent.cpp new file mode 100644 index 00000000..0791591d --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/OutputComponent.cpp @@ -0,0 +1,156 @@ +#include "ParameterData.h" + +#include "OutputComponent.h" +#include "PluginConfigurator.hpp" + +OutputMeter::OutputMeter(const SyndicateAudioProcessor& processor) : + _processor(processor) { + setFramesPerSecond(20); +} + +void OutputMeter::paint(juce::Graphics& g) { + g.fillAll(UIUtils::backgroundColour); + + constexpr float MIN_DB {-60}; + constexpr float MAX_DB {20}; + const float meterRange = MAX_DB - MIN_DB; + + auto dBToHeight = [&](float dBValue) { + return std::max(((std::min(dBValue, MAX_DB) - MIN_DB) / meterRange) * getHeight(), 0.0f); + }; + + const int zeroLineHeight = static_cast(dBToHeight(0)); + + // Draw the meter for each channel + const int numChannels {canDoStereoSplitTypes(_processor.getBusesLayout()) ? 2 : 1}; + constexpr int MARGIN {4}; + const int availableWidth {getWidth() - ((1 + numChannels) * MARGIN)}; + const int channelWidth {availableWidth / numChannels}; + juce::Rectangle availableArea = getLocalBounds(); + + + for (int channel {0}; channel < numChannels; channel++) { + const float gaindB = WECore::CoreMath::linearTodB(_processor.meterEnvelopes[channel].getLastOutput()); + const int meterHeight = static_cast(dBToHeight(gaindB)); + + availableArea.removeFromLeft(MARGIN); + juce::Rectangle meterArea = availableArea.removeFromLeft(channelWidth).withTrimmedTop(availableArea.getHeight() - meterHeight); + + // Draw the zero line + g.setColour(juce::Colours::grey); + g.drawLine(meterArea.getX(), getHeight() - zeroLineHeight, meterArea.getX() + meterArea.getWidth(), getHeight() - zeroLineHeight); + + // Draw the lower part of the meter + g.setColour(UIUtils::highlightColour.withBrightness(0.5)); + g.fillRect(meterArea.removeFromBottom(std::min(meterArea.getHeight(), zeroLineHeight))); + + // Draw the area above 0dB + if (meterHeight > zeroLineHeight) { + g.setColour(UIUtils::highlightColour.withLightness(0.5).withBrightness(0.5)); + g.fillRect(meterArea); + } + } +} + +OutputComponent::OutputComponent(SyndicateAudioProcessor& processor) : _processor(processor) { + panSlider.reset(new WECore::JUCEPlugin::LabelReadoutSlider("Balance Slider")); + addAndMakeVisible(panSlider.get()); + panSlider->setRange(OUTPUTPAN.minValue, OUTPUTPAN.maxValue, 0.01); + panSlider->setDoubleClickReturnValue(true, OUTPUTPAN.defaultValue); + panSlider->setSliderStyle(juce::Slider::RotaryVerticalDrag); + panSlider->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + panSlider->addListener(this); + panSlider->setLookAndFeel(&_panSliderLookAndFeel); + panSlider->setColour(juce::Slider::rotarySliderFillColourId, UIUtils::highlightColour); + panSlider->setColour(juce::Slider::rotarySliderOutlineColourId, UIUtils::deactivatedColour); + panSlider->setTooltip("Balance applied to the output (if in stereo)"); + + panLabel.reset(new juce::Label("Balance Label", TRANS("Balance"))); + addAndMakeVisible(panLabel.get()); + UIUtils::setDefaultLabelStyle(panLabel); + + outputMeter.reset(new OutputMeter(_processor)); + addAndMakeVisible(outputMeter.get()); + outputMeter->setTooltip("Output level"); + + outputGainSlider.reset(new WECore::JUCEPlugin::LabelReadoutSlider("Output Gain Slider")); + addAndMakeVisible(outputGainSlider.get()); + outputGainSlider->setRange(OUTPUTGAIN.minValue, OUTPUTGAIN.maxValue, 0.1); + outputGainSlider->setDoubleClickReturnValue(true, OUTPUTGAIN.defaultValue); + outputGainSlider->setSliderStyle(juce::Slider::LinearVertical); + outputGainSlider->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + outputGainSlider->addListener(this); + outputGainSlider->setLookAndFeel(&_gainSliderLookAndFeel); + outputGainSlider->setColour(juce::Slider::thumbColourId, UIUtils::highlightColour); + outputGainSlider->setColour(juce::Slider::trackColourId, UIUtils::highlightColour); + outputGainSlider->setTooltip("Gain applied to the output in dB"); + + outputGainLabel.reset(new juce::Label("Output Gain Label", TRANS("Output"))); + addAndMakeVisible(outputGainLabel.get()); + UIUtils::setDefaultLabelStyle(outputGainLabel); + + panSlider->setEnabled(canDoStereoSplitTypes(_processor.getBusesLayout())); + + panSlider->start(panLabel.get(), panLabel->getText()); + outputGainSlider->setValueToString([](double value) { return juce::String(value, 1) + " dB";}); + outputGainSlider->start(outputGainLabel.get(), outputGainLabel->getText()); +} + +OutputComponent::~OutputComponent() { + panSlider->setLookAndFeel(nullptr); + outputGainSlider->setLookAndFeel(nullptr); + + panSlider->stop(); + outputGainSlider->stop(); + + panSlider = nullptr; + panLabel = nullptr; + outputGainSlider = nullptr; + outputGainLabel = nullptr; +} + +void OutputComponent::resized() { + juce::Rectangle availableArea = getLocalBounds(); + availableArea.removeFromTop(10); + availableArea.removeFromLeft(8); + availableArea.removeFromRight(8); + + panSlider->setBounds(availableArea.removeFromTop(availableArea.getWidth())); + panLabel->setBounds(availableArea.removeFromTop(24)); + availableArea.removeFromTop(20); + + availableArea.removeFromBottom(8); + outputGainLabel->setBounds(availableArea.removeFromBottom(24)); + + outputMeter->setBounds(availableArea.removeFromLeft(availableArea.getWidth() / 2)); + outputGainSlider->setBounds(availableArea); +} + +void OutputComponent::sliderValueChanged(juce::Slider* sliderThatWasMoved) { + if (sliderThatWasMoved == outputGainSlider.get()) { + _processor.setParameterValueInternal(_processor.outputGainLog, outputGainSlider->getValue()); + } else if (sliderThatWasMoved == panSlider.get()) { + _processor.setParameterValueInternal(_processor.outputPan, panSlider->getValue()); + } +} + +void OutputComponent::sliderDragStarted(juce::Slider* slider) { + if (slider == outputGainSlider.get()) { + _processor.outputGainLog->beginChangeGesture(); + } else if (slider == panSlider.get()) { + _processor.outputPan->beginChangeGesture(); + } +} + +void OutputComponent::sliderDragEnded(juce::Slider* slider) { + if (slider == outputGainSlider.get()) { + _processor.outputGainLog->endChangeGesture(); + } else if (slider == panSlider.get()) { + _processor.outputPan->endChangeGesture(); + } +} + +void OutputComponent::onParameterUpdate() { + outputGainSlider->setValue(_processor.outputGainLog->get(), juce::dontSendNotification); + panSlider->setValue(_processor.outputPan->get(), juce::dontSendNotification); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/OutputComponent.h b/ports-juce7/syndicate/Syndicate/UI/OutputComponent.h new file mode 100644 index 00000000..5aad7a07 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/OutputComponent.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include "CoreJUCEPlugin/LabelReadoutSlider.h" +#include "PluginProcessor.h" +#include "UIUtils.h" + +/** + * Displays the output amplitude of this gain stage. + */ +class OutputMeter : public juce::AnimatedAppComponent, + public juce::SettableTooltipClient { +public: + OutputMeter(const SyndicateAudioProcessor& processor); + ~OutputMeter() = default; + + void update() override { + // Do nothing + } + + void paint(juce::Graphics& g) override; + +private: + const SyndicateAudioProcessor& _processor; +}; + +class OutputComponent : public juce::Component, public juce::Slider::Listener { +public: + OutputComponent(SyndicateAudioProcessor& processor); + ~OutputComponent() override; + + void onParameterUpdate(); + + void resized() override; + void sliderValueChanged(juce::Slider* sliderThatWasMoved) override; + void sliderDragStarted(juce::Slider* slider) override; + void sliderDragEnded(juce::Slider* slider) override; + +private: + SyndicateAudioProcessor& _processor; + UIUtils::StandardSliderLookAndFeel _gainSliderLookAndFeel; + UIUtils::MidAnchoredSliderLookAndFeel _panSliderLookAndFeel; + + std::unique_ptr> panSlider; + std::unique_ptr panLabel; + std::unique_ptr outputMeter; + std::unique_ptr> outputGainSlider; + std::unique_ptr outputGainLabel; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OutputComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginEditor.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginEditor.cpp new file mode 100644 index 00000000..e1961e5f --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginEditor.cpp @@ -0,0 +1,350 @@ +/* + ============================================================================== + + This is an automatically generated GUI class created by the Projucer! + + Be careful when adding custom code to these files, as only the code within + the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded + and re-saved. + + Created with Projucer version: 7.0.12 + + ------------------------------------------------------------------------------ + + The Projucer is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited. + + ============================================================================== +*/ + +//[Headers] You can add your own extra header files here... +#include "CoreJUCEPlugin/CoreProcessorEditor.h" +#include "LeftrightSplitterSubComponent.h" +#include "MidsideSplitterSubComponent.h" +#include "MultibandSplitterSubComponent.h" +#include "ParallelSplitterSubComponent.h" +#include "ParameterData.h" +#include "SeriesSplitterSubComponent.h" +//[/Headers] + +#include "PluginEditor.h" + + +//[MiscUserDefs] You can add your own user definitions and misc code here... +//[/MiscUserDefs] + +//============================================================================== +SyndicateAudioProcessorEditor::SyndicateAudioProcessorEditor (SyndicateAudioProcessor& ownerProcessor) + : CoreProcessorEditor(ownerProcessor), _processor(ownerProcessor), _isHeaderInitialised(false), _previousSplitType(SPLIT_TYPE::SERIES) +{ + //[Constructor_pre] You can add your own custom stuff here.. + //[/Constructor_pre] + + + //[UserPreSize] + _importExportComponent.reset(new ImportExportComponent(_processor, *this)); + addAndMakeVisible(_importExportComponent.get()); + _importExportComponent->setName("Import/Export"); + + _undoRedoComponent.reset(new UndoRedoComponent(_processor)); + addAndMakeVisible(_undoRedoComponent.get()); + _undoRedoComponent->setName("Undo/Redo"); + + _macrosSidebar.reset(new MacrosComponent(this, _processor.macros, _processor.macroNames)); + addAndMakeVisible(_macrosSidebar.get()); + _macrosSidebar->setName("Macros"); + + _splitterButtonsBar.reset(new SplitterButtonsComponent(_processor)); + addAndMakeVisible(_splitterButtonsBar.get()); + _splitterButtonsBar->setName("Splitter Buttons"); + + _headerExtensionComponent.reset(new juce::Component()); + addAndMakeVisible(_headerExtensionComponent.get()); + _headerExtensionComponent->setName("Header Extension Component"); + + _outputSidebar.reset(new OutputComponent(_processor)); + addAndMakeVisible(_outputSidebar.get()); + _outputSidebar->setName("Output"); + + _graphView.reset(new GraphViewComponent(_processor)); + addAndMakeVisible(_graphView.get()); + _graphView->setName("Graph View"); + + _splitterHeader.reset(new SeriesSplitterSubComponent(_processor, _graphView->getViewport())); + addAndMakeVisible(_splitterHeader.get()); + _splitterHeader->setName("Splitter Header"); + + _modulationBar.reset(new ModulationBar(_processor, this)); + addAndMakeVisible(_modulationBar.get()); + _modulationBar->setName("Modulation"); + + _tooltipLbl.reset(new juce::Label("Tooltip Label", juce::String())); + addAndMakeVisible(_tooltipLbl.get()); + _tooltipLbl->setFont(juce::Font (15.00f, juce::Font::plain).withTypefaceStyle("Regular")); + _tooltipLbl->setJustificationType(juce::Justification::centred); + _tooltipLbl->setEditable(false, false, false); + _tooltipLbl->setColour(juce::Label::textColourId, UIUtils::tooltipColour); + _tooltipLbl->setColour(juce::TextEditor::textColourId, juce::Colours::black); + _tooltipLbl->setColour(juce::TextEditor::backgroundColourId, juce::Colour(0x00000000)); + //[/UserPreSize] + + //[Constructor] You can add your own custom stuff here.. + setResizable(true, true); + + _processor.setEditor(this); + setBounds(_processor.mainWindowState.bounds); + + _setSliderRanges(); + + // Start tooltip label + addMouseListener(&_tooltipLabelUpdater, true); + _tooltipLabelUpdater.start( + _tooltipLbl.get(), + getAudioProcessor()->wrapperType +#ifdef DEMO_BUILD + ,true +#endif + ); + + // Call this once to force an update + needsGraphRebuild(); + _onParameterUpdate(); + + _displayErrorsIfNeeded(); + + //[/Constructor] +} + +SyndicateAudioProcessorEditor::~SyndicateAudioProcessorEditor() +{ + //[Destructor_pre]. You can add your own custom destruction code here.. + _processor.mainWindowState.bounds = getBounds(); + _processor.setEditor(nullptr); + _tooltipLabelUpdater.stop(); + + _importExportComponent = nullptr; + _undoRedoComponent = nullptr; + _macrosSidebar = nullptr; + _splitterButtonsBar = nullptr; + _splitterHeader = nullptr; + _headerExtensionComponent = nullptr; + _outputSidebar = nullptr; + _graphView = nullptr; + _modulationBar = nullptr; + _tooltipLbl = nullptr; + //[/Destructor_pre] + + + + //[Destructor]. You can add your own custom destruction code here.. + //[/Destructor] +} + +//============================================================================== +void SyndicateAudioProcessorEditor::paint (juce::Graphics& g) +{ + //[UserPrePaint] Add your own custom painting code here.. + //[/UserPrePaint] + + g.fillAll (juce::Colour (0xff272727)); + + //[UserPaint] Add your own custom painting code here.. + //[/UserPaint] +} + +void SyndicateAudioProcessorEditor::resized() +{ + //[UserPreResize] Add your own custom resize code here.. + if (_errorPopover != nullptr) { + _errorPopover->setBounds(getLocalBounds()); + } + + constexpr int SIDEBAR_WIDTH {64}; + constexpr int SPLITTER_BUTTONS_HEIGHT {40}; + constexpr int TOOLTIP_HEIGHT {24}; + + juce::Rectangle availableArea = getLocalBounds().withTrimmedBottom(1); + + _importExportComponent->setBounds(availableArea.removeFromTop(28)); + _tooltipLbl->setBounds(availableArea.removeFromBottom(TOOLTIP_HEIGHT)); + + juce::Rectangle leftArea = availableArea.removeFromLeft(SIDEBAR_WIDTH); + _undoRedoComponent->setBounds(leftArea.removeFromTop(8 + 24 + 4 + 24 + 8)); + _macrosSidebar->setBounds(leftArea); + + juce::Rectangle rightArea = availableArea.removeFromRight(SIDEBAR_WIDTH).withTrimmedTop(SPLITTER_BUTTONS_HEIGHT); + _headerExtensionComponent->setBounds(rightArea.removeFromTop(80)); + _outputSidebar->setBounds(rightArea); + + _splitterButtonsBar->setBounds(availableArea.removeFromTop(SPLITTER_BUTTONS_HEIGHT)); + _splitterHeader->setBounds(availableArea.removeFromTop(80)); + + _modulationBar->setBounds(availableArea.removeFromBottom(130)); + + _graphView->setBounds(availableArea); + //[/UserPreResize] + + //[UserResized] Add your own custom resize handling here.. + //[/UserResized] +} + + + +//[MiscUserCode] You can add your own definitions of your custom methods or any other code here... +void SyndicateAudioProcessorEditor::needsGraphRebuild() { + _splitterButtonsBar->onParameterUpdate(); + _updateSplitterHeader(); + _graphView->onParameterUpdate(); + needsChainButtonsRefresh(); + needsUndoRedoRefresh(); +} + +void SyndicateAudioProcessorEditor::needsModulationBarRebuild() { + _modulationBar->needsRebuild(); + needsUndoRedoRefresh(); +} + +void SyndicateAudioProcessorEditor::needsSelectedModulationSourceRebuild() { + _modulationBar->needsSelectedSourceRebuild(); + needsUndoRedoRefresh(); +} + +void SyndicateAudioProcessorEditor::needsChainButtonsRefresh() { + _splitterHeader->refreshChainButtons(); + needsUndoRedoRefresh(); +} + +void SyndicateAudioProcessorEditor::needsImportExportRefresh() { + _importExportComponent->refresh(); +} + +void SyndicateAudioProcessorEditor::needsUndoRedoRefresh() { + _undoRedoComponent->refresh(); + + juce::Component* mouseOverComponent = getComponentAt(getMouseXYRelative()); + if (mouseOverComponent != nullptr) { + _tooltipLabelUpdater.refreshTooltip(mouseOverComponent); + } +} + +void SyndicateAudioProcessorEditor::needsToRefreshAll() { + needsGraphRebuild(); + _onParameterUpdate(); + needsModulationBarRebuild(); + _macrosSidebar->updateNames(_processor.macroNames); +} + +void SyndicateAudioProcessorEditor::closeGuestPluginWindows() { + _graphView->closeGuestPluginWindows(); +} + +void SyndicateAudioProcessorEditor::_enableDoubleClickToDefault() { + // TODO +} + +void SyndicateAudioProcessorEditor::_startSliderReadouts() { + // TODO +} + +void SyndicateAudioProcessorEditor::_stopSliderReadouts() { + // TODO +} + +void SyndicateAudioProcessorEditor::_setSliderRanges() { + // TODO +} + +void SyndicateAudioProcessorEditor::_onParameterUpdate() { + { + std::scoped_lock lock(_splitterHeaderMutex); + _splitterHeader->onParameterUpdate(); + } + + _outputSidebar->onParameterUpdate(); + _macrosSidebar->onParameterUpdate(); + + _undoRedoComponent->refresh(); +} + +void SyndicateAudioProcessorEditor::_updateSplitterHeader() { + std::scoped_lock lock(_splitterHeaderMutex); + + // Cache the previous value so we don't have recreate the component when nothing has changed + if (_processor.getSplitType() != _previousSplitType || !_isHeaderInitialised) { + _previousSplitType = _processor.getSplitType(); + _isHeaderInitialised = true; + const juce::Rectangle bounds = _splitterHeader->getBounds(); + + switch (_processor.getSplitType()) { + case SPLIT_TYPE::SERIES: + _splitterHeader.reset(new SeriesSplitterSubComponent(_processor, _graphView->getViewport())); + break; + case SPLIT_TYPE::PARALLEL: + _splitterHeader.reset(new ParallelSplitterSubComponent(_processor, _headerExtensionComponent.get(), _graphView->getViewport())); + break; + case SPLIT_TYPE::MULTIBAND: + _splitterHeader.reset(new MultibandSplitterSubComponent(_processor, _headerExtensionComponent.get(), _graphView->getViewport())); + break; + case SPLIT_TYPE::LEFTRIGHT: + _splitterHeader.reset(new LeftrightSplitterSubComponent(_processor, _graphView->getViewport())); + break; + case SPLIT_TYPE::MIDSIDE: + _splitterHeader.reset(new MidsideSplitterSubComponent(_processor, _graphView->getViewport())); + break; + } + + addAndMakeVisible(_splitterHeader.get()); + _splitterHeader->setName("Splitter Header"); + _splitterHeader->setBounds(bounds); + } + + _splitterHeader->onParameterUpdate(); +} + +void SyndicateAudioProcessorEditor::_displayErrorsIfNeeded() { + + if (_processor.restoreErrors.size() > 0) { + juce::String bodyText; + + for (juce::String& error : _processor.restoreErrors) { + bodyText += error; + bodyText += "\n"; + } + + _errorPopover.reset(new UIUtils::PopoverComponent("Encountered the following errors while restoring plugin state:", bodyText, [&]() {_errorPopover.reset(); })); + addAndMakeVisible(_errorPopover.get()); + _errorPopover->setBounds(getLocalBounds()); + + _processor.restoreErrors.clear(); + } +} + +//[/MiscUserCode] + + +//============================================================================== +#if 0 +/* -- Projucer information section -- + + This is where the Projucer stores the metadata that describe this GUI layout, so + make changes in here at your peril! + +BEGIN_JUCER_METADATA + + + + + +END_JUCER_METADATA +*/ +#endif + + +//[EndFile] You can add extra defines here... +//[/EndFile] + diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginEditor.h b/ports-juce7/syndicate/Syndicate/UI/PluginEditor.h new file mode 100644 index 00000000..72aef059 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginEditor.h @@ -0,0 +1,110 @@ +/* + ============================================================================== + + This is an automatically generated GUI class created by the Projucer! + + Be careful when adding custom code to these files, as only the code within + the "//[xyz]" and "//[/xyz]" sections will be retained when the file is loaded + and re-saved. + + Created with Projucer version: 7.0.12 + + ------------------------------------------------------------------------------ + + The Projucer is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited. + + ============================================================================== +*/ + +#pragma once + +//[Headers] -- You can add your own extra header files here -- +#include +#include "CoreJUCEPlugin/CoreProcessorEditor.h" +#include "CoreJUCEPlugin/TooltipLabelUpdater.h" +#include "PluginProcessor.h" +#include "ImportExportComponent.h" +#include "UndoRedoComponent.h" +#include "MacrosComponent.h" +#include "OutputComponent.h" +#include "SplitterButtonsComponent.h" +#include "SplitterHeaderComponent.h" +#include "PluginSlotComponent.h" +#include "GraphViewComponent.h" +#include "ModulationBar.h" +//[/Headers] + + + +//============================================================================== +/** + //[Comments] + An auto-generated component, created by the Projucer. + + Describe your class and how it works here! + //[/Comments] +*/ +class SyndicateAudioProcessorEditor : public WECore::JUCEPlugin::CoreProcessorEditor, + public juce::DragAndDropContainer +{ +public: + //============================================================================== + SyndicateAudioProcessorEditor (SyndicateAudioProcessor& ownerProcessor); + ~SyndicateAudioProcessorEditor() override; + + //============================================================================== + //[UserMethods] -- You can add your own custom methods in this section. + void needsGraphRebuild(); + void needsModulationBarRebuild(); + void needsSelectedModulationSourceRebuild(); + void needsChainButtonsRefresh(); + void needsImportExportRefresh(); + void needsUndoRedoRefresh(); + void needsToRefreshAll(); + void closeGuestPluginWindows(); + //[/UserMethods] + + void paint (juce::Graphics& g) override; + void resized() override; + + + +private: + //[UserVariables] -- You can add your own custom variables in this section. + SyndicateAudioProcessor& _processor; + WECore::JUCEPlugin::TooltipLabelUpdater _tooltipLabelUpdater; + std::unique_ptr _splitterHeader; + bool _isHeaderInitialised; + std::unique_ptr _errorPopover; + std::unique_ptr _importExportComponent; + std::unique_ptr _undoRedoComponent; + std::unique_ptr _macrosSidebar; + std::unique_ptr _splitterButtonsBar; + std::unique_ptr _headerExtensionComponent; + std::unique_ptr _outputSidebar; + std::unique_ptr _graphView; + std::unique_ptr _modulationBar; + std::unique_ptr _tooltipLbl; + std::mutex _splitterHeaderMutex; + SPLIT_TYPE _previousSplitType; + + void _enableDoubleClickToDefault(); + void _startSliderReadouts(); + void _stopSliderReadouts(); + void _setSliderRanges(); + void _onParameterUpdate() override; + void _updateSplitterHeader(); + void _displayErrorsIfNeeded(); + //[/UserVariables] + + //============================================================================== + + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SyndicateAudioProcessorEditor) +}; + +//[EndFile] You can add extra defines here... +//[/EndFile] + diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/BaseSlotComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/BaseSlotComponent.cpp new file mode 100644 index 00000000..e74b7d74 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/BaseSlotComponent.cpp @@ -0,0 +1,51 @@ +#include "BaseSlotComponent.h" + +BaseSlotComponent::BaseSlotComponent() : _chainNumber(0), _slotNumber(0) { +} + + +BaseSlotComponent::BaseSlotComponent(int chainNumber, int slotNumber) : _chainNumber(chainNumber), + _slotNumber(slotNumber) { + // Set up the drag label + dragHandle.reset(new UIUtils::DragHandle()); + addAndMakeVisible(dragHandle.get()); + dragHandle->setColour(UIUtils::DragHandle::handleColourId, UIUtils::highlightColour); + dragHandle->setTooltip(TRANS("Drag to move this slot to another position - hold " + UIUtils::getCopyKeyName() + " key to copy")); + dragHandle->addMouseListener(this, false); +} + +void BaseSlotComponent::resized() { + if (dragHandle != nullptr) { + dragHandle->setBounds(juce::Rectangle(getWidth(), UIUtils::PLUGIN_SLOT_HEIGHT).removeFromLeft(UIUtils::SLOT_DRAG_HANDLE_WIDTH)); + } +} + +void BaseSlotComponent::paint(juce::Graphics& g) { + const juce::Rectangle fillArea = juce::Rectangle(getWidth(), UIUtils::PLUGIN_SLOT_HEIGHT).reduced(MARGIN, MARGIN).toFloat(); + const float cornerRadius {fillArea.getHeight() / 2}; + + g.setColour(UIUtils::slotBackgroundColour); + g.fillRoundedRectangle(fillArea, cornerRadius); +} + +juce::Rectangle BaseSlotComponent::getAvailableSlotArea() { + return dragHandle == nullptr ? getLocalBounds() : + juce::Rectangle(getWidth(), UIUtils::PLUGIN_SLOT_HEIGHT).withTrimmedLeft(dragHandle->getWidth()); +} + +void BaseSlotComponent::mouseDrag(const juce::MouseEvent& e) { + if (dragHandle != nullptr && e.originalComponent == dragHandle.get()) { + juce::DragAndDropContainer* container = juce::DragAndDropContainer::findParentDragContainerFor(this); + + if (container != nullptr) { + juce::var details; + details.append(_chainNumber); + details.append(_slotNumber); + + // This is a copy if alt is down, otherwise move + details.append(juce::ModifierKeys::currentModifiers.isAltDown()); + + container->startDragging(details, this); + } + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/BaseSlotComponent.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/BaseSlotComponent.h new file mode 100644 index 00000000..e1b5f7bc --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/BaseSlotComponent.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include "UIUtils.h" + +class BaseSlotComponent : public juce::Component { +public: + /** + * Default construct without a drag handle. + */ + BaseSlotComponent(); + + /** + * Construct with a drag handle on the left of the slot. + */ + BaseSlotComponent(int chainNumber, int slotNumber); + + virtual ~BaseSlotComponent() = default; + + /** + * Returns the area available for the slot to draw in. + */ + juce::Rectangle getAvailableSlotArea(); + + virtual void mouseDrag(const juce::MouseEvent& e) override; + + virtual void resized() override; + + virtual void paint(juce::Graphics& g) override; + +private: + std::unique_ptr dragHandle; + +protected: + int _chainNumber; + int _slotNumber; + + static constexpr int MARGIN {1}; + + void _drawBackground(juce::Graphics& g) const; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/ChainViewComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/ChainViewComponent.cpp new file mode 100644 index 00000000..fe8643ce --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/ChainViewComponent.cpp @@ -0,0 +1,197 @@ +#include "ChainViewComponent.h" +#include "UIUtils.h" +#include "EmptyPluginSlotComponent.h" +#include "GainStageSlotComponent.h" +#include "PluginSlotComponent.h" +#include "ChainMutators.hpp" + +namespace { + std::tuple slotDetailsFromVariant(juce::var variant) { + bool isValid {false}; + int chainNumber {0}; + int slotNumber {0}; + bool isCopy {false}; + + if (variant.isArray() && variant.size() == 3 && variant[0].isInt() && variant[1].isInt() && variant[2].isBool()) { + isValid = true; + chainNumber = variant[0]; + slotNumber = variant[1]; + isCopy = variant[2]; + } + + return std::make_tuple(isValid, chainNumber, slotNumber, isCopy); + } +} + +ChainViewComponent::ChainViewComponent(int chainNumber, + PluginSelectionInterface& pluginSelectionInterface, + PluginModulationInterface& pluginModulationInterface) : + _chainNumber(chainNumber), + _pluginSelectionInterface(pluginSelectionInterface), + _pluginModulationInterface(pluginModulationInterface), + _shouldDrawDragHint(false), + _dragHintSlotNumber(0) { + + _viewPort.reset(new juce::Viewport()); + _viewPort->setViewedComponent(new juce::Component()); + _viewPort->setScrollBarsShown(true, false); + _viewPort->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + _viewPort->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, UIUtils::neutralColour.withAlpha(0.5f)); + _viewPort->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + addAndMakeVisible(_viewPort.get()); + _viewPort->setBounds(getLocalBounds()); +} + +ChainViewComponent::~ChainViewComponent() { + _viewPort = nullptr; +} + +void ChainViewComponent::setPlugins(std::shared_ptr newChain) { + // Clear all slots and rebuild the chain + _pluginSlots.clear(); + + for (size_t index {0}; index < ChainMutators::getNumSlots(newChain); index++) { + // Add the slot + std::unique_ptr newSlot; + if (_pluginSelectionInterface.isPluginSlot(_chainNumber, index)) { + newSlot.reset(new PluginSlotComponent(_pluginSelectionInterface, _pluginModulationInterface, _chainNumber, index)); + } else { + newSlot.reset(new GainStageSlotComponent(_pluginSelectionInterface, _chainNumber, index)); + } + + // Add the component for this slot + _viewPort->getViewedComponent()->addAndMakeVisible(newSlot.get()); + _pluginSlots.push_back(std::move(newSlot)); + } + + // Add the empty slot at the end + std::unique_ptr newSlot( + new EmptyPluginSlotComponent(_pluginSelectionInterface, _chainNumber, _pluginSlots.size())); + + _viewPort->getViewedComponent()->addAndMakeVisible(newSlot.get()); + _pluginSlots.push_back(std::move(newSlot)); + + resized(); +} + +void ChainViewComponent::resized() { + juce::Rectangle availableArea = getLocalBounds(); + availableArea.removeFromLeft(1); + availableArea.removeFromRight(1); + + const int scrollPosition {_viewPort->getViewPositionY()}; + + _viewPort->setBounds(availableArea); + + // First pass to calculate the scrollable height + // We need to do this since we don't know if the modulation tray will be open or not + // There's always an empty slot so start with that + constexpr int MARGIN_HEIGHT {2}; + int scrollableHeight {UIUtils::PLUGIN_SLOT_HEIGHT}; + for (int slotNumber {0}; slotNumber < _pluginSlots.size() - 1; slotNumber++) { + const bool hasModulationTray {_pluginModulationInterface.getPluginModulationConfig(_chainNumber, slotNumber).isActive}; + scrollableHeight += (UIUtils::PLUGIN_SLOT_HEIGHT + MARGIN_HEIGHT + + (hasModulationTray ? UIUtils::PLUGIN_SLOT_MOD_TRAY_HEIGHT : 0)); + } + + // Take the full height if the scrollable area is smaller + scrollableHeight = std::max(availableArea.getHeight(), scrollableHeight); + const int scrollableWidth {availableArea.getWidth()}; + _viewPort->getViewedComponent()->setBounds(juce::Rectangle(scrollableWidth, scrollableHeight)); + + juce::Rectangle scrollableArea = _viewPort->getViewedComponent()->getLocalBounds(); + + const int scrollbarWidth {_viewPort->getScrollBarThickness()}; + + for (int slotNumber {0}; slotNumber < _pluginSlots.size(); slotNumber++) { + if (slotNumber < _pluginSlots.size() - 1) { + const bool hasModulationTray {_pluginModulationInterface.getPluginModulationConfig(_chainNumber, slotNumber).isActive}; + const int slotHeight { + UIUtils::PLUGIN_SLOT_HEIGHT + (hasModulationTray ? UIUtils::PLUGIN_SLOT_MOD_TRAY_HEIGHT : 0) + }; + + _pluginSlots[slotNumber]->setBounds(scrollableArea.removeFromTop(slotHeight).withTrimmedRight(scrollbarWidth)); + scrollableArea.removeFromTop(MARGIN_HEIGHT); + } else { + // The last slot is an empty one + _pluginSlots[slotNumber]->setBounds(scrollableArea.removeFromTop(UIUtils::PLUGIN_SLOT_HEIGHT).withTrimmedRight(scrollbarWidth)); + } + } + + // Maintain the previous scroll position + _viewPort->setViewPosition(0, scrollPosition); +} + +void ChainViewComponent::paint(juce::Graphics& g) { + if (_shouldDrawDragHint) { + g.setColour(UIUtils::neutralColour); + const int hintYPos {_dragHintSlotNumber < _pluginSlots.size() ? + _pluginSlots[_dragHintSlotNumber]->getY() - _viewPort->getViewPositionY() : 0 + }; + g.drawLine(0, hintYPos, getWidth(), hintYPos, 2.0f); + } +} + +bool ChainViewComponent::isInterestedInDragSource(const SourceDetails& dragSourceDetails) { + auto [isValid, fromChainNumber, fromSlotNumber, isCopy] = slotDetailsFromVariant(dragSourceDetails.description); + + // TODO only interested if slot has actually moved or isCopy + + return isValid; +} + +void ChainViewComponent::itemDragEnter(const SourceDetails& dragSourceDetails) { + auto [isValid, fromChainNumber, fromSlotNumber, isCopy] = slotDetailsFromVariant(dragSourceDetails.description); + + if (isValid) { + _shouldDrawDragHint = true; + _dragHintSlotNumber = _dragCursorPositionToSlotNumber(dragSourceDetails.localPosition); + repaint(); + } +} + +void ChainViewComponent::itemDragMove(const SourceDetails& dragSourceDetails) { + auto [isValid, fromChainNumber, fromSlotNumber, isCopy] = slotDetailsFromVariant(dragSourceDetails.description); + + if (isValid) { + _dragHintSlotNumber = _dragCursorPositionToSlotNumber(dragSourceDetails.localPosition); + repaint(); + } +} + +void ChainViewComponent::itemDragExit(const SourceDetails& /*dragSourceDetails*/) { + _shouldDrawDragHint = false; + repaint(); +} + +void ChainViewComponent::itemDropped(const SourceDetails& dragSourceDetails) { + _shouldDrawDragHint = false; + repaint(); + + // Actually move the slot + auto [isValid, fromChainNumber, fromSlotNumber, isCopy] = slotDetailsFromVariant(dragSourceDetails.description); + + if (isValid) { + if (isCopy) { + _pluginSelectionInterface.copySlot(fromChainNumber, fromSlotNumber, _chainNumber, _dragHintSlotNumber); + } else { + _pluginSelectionInterface.moveSlot(fromChainNumber, fromSlotNumber, _chainNumber, _dragHintSlotNumber); + } + } +} + +int ChainViewComponent::_dragCursorPositionToSlotNumber(juce::Point cursorPosition) { + int retVal {static_cast(_pluginSlots.size() - 1)}; + + // Check which component the drag is over + const int scrollPosition {_viewPort->getViewPositionY()}; + for (size_t slotNumber {0}; slotNumber < _pluginSlots.size(); slotNumber++) { + + if (cursorPosition.getY() < _pluginSlots[slotNumber]->getBottom() - scrollPosition) { + retVal = slotNumber; + break; + } + } + + return retVal; +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/ChainViewComponent.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/ChainViewComponent.h new file mode 100644 index 00000000..728f38ec --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/ChainViewComponent.h @@ -0,0 +1,44 @@ +#include +#include "BaseSlotComponent.h" +#include "PluginChain.hpp" +#include "PluginSelectionInterface.h" +#include "PluginModulationInterface.h" + +/** + * Manages a single chain of plugins. + */ +class ChainViewComponent : public juce::Component, + public juce::DragAndDropTarget { +public: + ChainViewComponent(int chainNumber, + PluginSelectionInterface& pluginSelectionInterface, + PluginModulationInterface& pluginModulationInterface); + ~ChainViewComponent(); + + void setPlugins(std::shared_ptr newChain); + + void resized() override; + + void paint(juce::Graphics& g) override; + + bool isInterestedInDragSource(const SourceDetails& dragSourceDetails) override; + void itemDragEnter(const SourceDetails& dragSourceDetails) override; + void itemDragMove(const SourceDetails& dragSourceDetails) override; + void itemDragExit(const SourceDetails& dragSourceDetails) override; + void itemDropped(const SourceDetails& dragSourceDetails) override; + + void setScrollPosition(int val) { _viewPort->setViewPosition(0, val); } + int getScrollPosition() const { return _viewPort->getViewPositionY(); } + +private: + int _chainNumber; + PluginSelectionInterface& _pluginSelectionInterface; + PluginModulationInterface& _pluginModulationInterface; + std::vector> _pluginSlots; + std::unique_ptr _viewPort; + bool _shouldDrawDragHint; + int _dragHintSlotNumber; + + int _dragCursorPositionToSlotNumber(juce::Point cursorPosition); + +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.cpp new file mode 100644 index 00000000..12e64580 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.cpp @@ -0,0 +1,49 @@ +#include "EmptyPluginSlotComponent.h" + +EmptyPluginSlotComponent::EmptyPluginSlotComponent(PluginSelectionInterface& pluginSelectionInterface, + int chainNumber, + int pluginNumber) : + _pluginSelectionInterface(pluginSelectionInterface), + _chainNumber(chainNumber), + _pluginNumber(pluginNumber) { + _buttonLookAndFeel.reset(new UIUtils::TextOnlyButtonLookAndFeel()); + + _addPluginButton.reset(new juce::TextButton("Add Plugin Button")); + addAndMakeVisible(_addPluginButton.get()); + _addPluginButton->setButtonText(TRANS("+ Plugin")); + _addPluginButton->setTooltip(TRANS("Add a new plugin to the chain")); + _addPluginButton->setLookAndFeel(_buttonLookAndFeel.get()); + _addPluginButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + _addPluginButton->addListener(this); + + _addGainStageButton.reset(new juce::TextButton("Add Gain Stage Button")); + addAndMakeVisible(_addGainStageButton.get()); + _addGainStageButton->setButtonText(TRANS("+ Gain Stage")); + _addGainStageButton->setTooltip(TRANS("Add a new gain stage to the chain")); + _addGainStageButton->setLookAndFeel(_buttonLookAndFeel.get()); + _addGainStageButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + _addGainStageButton->addListener(this); +} + +EmptyPluginSlotComponent::~EmptyPluginSlotComponent() { + _addPluginButton->setLookAndFeel(nullptr); + _addGainStageButton->setLookAndFeel(nullptr); + + _addPluginButton = nullptr; + _addGainStageButton = nullptr; +} + +void EmptyPluginSlotComponent::resized() { + juce::Rectangle availableArea = getLocalBounds(); + _addPluginButton->setBounds(availableArea.removeFromLeft(availableArea.getWidth() / 2)); + _addGainStageButton->setBounds(availableArea); +} + + +void EmptyPluginSlotComponent::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == _addPluginButton.get()) { + _pluginSelectionInterface.selectNewPlugin(_chainNumber, _pluginNumber); + } else if (buttonThatWasClicked == _addGainStageButton.get()) { + _pluginSelectionInterface.insertGainStage(_chainNumber, _pluginNumber); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.h new file mode 100644 index 00000000..255c32c9 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include "BaseSlotComponent.h" +#include "PluginSelectionInterface.h" +#include "UIUtils.h" + +class EmptyPluginSlotComponent : public BaseSlotComponent, + public juce::Button::Listener { +public: + EmptyPluginSlotComponent(PluginSelectionInterface& pluginSelectionInterface, + int chainNumber, + int pluginNumber); + ~EmptyPluginSlotComponent() override; + + void resized() override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + +private: + PluginSelectionInterface& _pluginSelectionInterface; + int _chainNumber; + int _pluginNumber; + + std::unique_ptr _buttonLookAndFeel; + std::unique_ptr _addPluginButton; + std::unique_ptr _addGainStageButton; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GainStageSlotComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GainStageSlotComponent.cpp new file mode 100644 index 00000000..05613221 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GainStageSlotComponent.cpp @@ -0,0 +1,180 @@ +#include "GainStageSlotComponent.h" +#include "ParameterData.h" +#include "UIUtils.h" + +namespace { + int dBToXPos(float dBValue, int meterWidth) { + constexpr float MIN_DB {-60}; + constexpr float MAX_DB {20}; + constexpr float meterRange = MAX_DB - MIN_DB; + + return std::max(((std::min(dBValue, MAX_DB) - MIN_DB) / meterRange) * meterWidth, 0.0f); + } +} + +GainStageMeter::GainStageMeter(const PluginSelectionInterface& pluginSelectionInterface, + int chainNumber, + int slotNumber) : + _pluginSelectionInterface(pluginSelectionInterface), + _chainNumber(chainNumber), + _slotNumber(slotNumber) { + start(); +} + +void GainStageMeter::paint(juce::Graphics& g) { + _stopEvent.reset(); + + const int zeroLineXPos = dBToXPos(0, getWidth()); + + // Draw the meter for each channel + const int numChannels {_pluginSelectionInterface.getNumMainChannels()}; + constexpr int MARGIN {4}; + juce::Rectangle availableArea = getLocalBounds(); + const int availableHeight {getHeight() - ((1 + numChannels) * MARGIN)}; + const int channelHeight {availableHeight / numChannels}; + + + for (int channel {0}; channel < numChannels; channel++) { + const float gaindB = WECore::CoreMath::linearTodB(_pluginSelectionInterface.getGainStageOutputAmplitude(_chainNumber, _slotNumber, channel)); + const int meterWidth = dBToXPos(gaindB, getWidth()); + + availableArea.removeFromTop(MARGIN); + juce::Rectangle meterArea = availableArea.removeFromTop(channelHeight).withWidth(meterWidth); + + // Draw the zero line + g.setColour(juce::Colours::grey); + g.drawLine(zeroLineXPos, meterArea.getY(), zeroLineXPos, meterArea.getY() + meterArea.getHeight()); + + // Draw the lower part of the meter + g.setColour(UIUtils::highlightColour.withBrightness(0.5)); + g.fillRect(meterArea.removeFromLeft(std::min(meterArea.getWidth(), zeroLineXPos))); + + // Draw the area above 0dB + if (meterWidth > zeroLineXPos) { + g.setColour(UIUtils::highlightColour.withLightness(0.5).withBrightness(0.5)); + g.fillRect(meterArea); + } + } + + _stopEvent.signal(); +} + +GainStageSlotComponent::GainStageSlotComponent( + PluginSelectionInterface& pluginSelectionInterface, + int chainNumber, + int slotNumber) : BaseSlotComponent(chainNumber, slotNumber), + _pluginSelectionInterface(pluginSelectionInterface) { + + _buttonLookAndFeel.reset(new UIUtils::TextOnlyButtonLookAndFeel()); + + const juce::String meterTooltip("Output of this gain stage"); + + levelMeter.reset(new GainStageMeter(_pluginSelectionInterface, chainNumber, slotNumber)); + addAndMakeVisible(levelMeter.get()); + levelMeter->setTooltip(meterTooltip); + + valueLabel.reset(new juce::Label("Value Label", "")); + addAndMakeVisible(valueLabel.get()); + valueLabel->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle ("Regular")); + valueLabel->setJustificationType(juce::Justification::centred); + valueLabel->setEditable(true, true, false); + valueLabel->setColour(juce::Label::textColourId, UIUtils::highlightColour); + + // This is set to match the tooltip of the meter since it's overlaid on the meter + valueLabel->setTooltip(meterTooltip); + + gainSld.reset(new WECore::JUCEPlugin::LabelReadoutSlider("Gain Slider")); + addAndMakeVisible(gainSld.get()); + gainSld->setRange(OUTPUTGAIN.minValue, OUTPUTGAIN.maxValue, 0); + gainSld->setDoubleClickReturnValue(true, OUTPUTGAIN.defaultValue); + gainSld->setSliderStyle(juce::Slider::RotaryVerticalDrag); + gainSld->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + gainSld->addListener(this); + gainSld->setColour(juce::Slider::rotarySliderFillColourId, UIUtils::highlightColour); + gainSld->setLookAndFeel(&_gainSliderLookAndFeel); + gainSld->setTooltip(TRANS("Gain applied by this gain stage")); + + panSld.reset(new WECore::JUCEPlugin::LabelReadoutSlider("Pan Slider")); + addAndMakeVisible(panSld.get()); + panSld->setRange(-1, 1, 0); + panSld->setDoubleClickReturnValue(true, 0); + panSld->setSliderStyle(juce::Slider::RotaryVerticalDrag); + panSld->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + panSld->addListener(this); + panSld->setColour(juce::Slider::rotarySliderFillColourId, UIUtils::highlightColour); + panSld->setLookAndFeel(&_panSliderLookAndFeel); + panSld->setTooltip(TRANS("Balance applied by this gain stage (if in stereo)")); + + removeBtn.reset(new UIUtils::CrossButton("Remove Button")); + addAndMakeVisible(removeBtn.get()); + removeBtn->setTooltip(TRANS("Remove this gain stage")); + removeBtn->setColour(UIUtils::CrossButton::enabledColour, UIUtils::highlightColour); + removeBtn->setColour(UIUtils::CrossButton::disabledColour, UIUtils::deactivatedColour); + removeBtn->addListener(this); + + // Initialise the slider values + // (we don't need to set this again in an onParameterUpdate() or anything like that as nothing + // else can change the internal value but this slider) + auto [linearGain, pan] = _pluginSelectionInterface.getGainStageGainAndPan(_chainNumber, _slotNumber); + const double logGain {WECore::CoreMath::linearTodB(linearGain)}; + gainSld->setValue(logGain, juce::dontSendNotification); + + panSld->setValue(pan, juce::dontSendNotification); + panSld->setEnabled(_pluginSelectionInterface.getNumMainChannels() == 2); + + gainSld->setValueToString([](double value) { return juce::String(value, 1) + " dB";}); + gainSld->start(valueLabel.get(), valueLabel->getText()); + panSld->start(valueLabel.get(), valueLabel->getText()); +} + +GainStageSlotComponent::~GainStageSlotComponent() { + gainSld->setLookAndFeel(nullptr); + panSld->setLookAndFeel(nullptr); + removeBtn->setLookAndFeel(nullptr); + + gainSld->stop(); + panSld->stop(); + + levelMeter = nullptr; + valueLabel = nullptr; + gainSld = nullptr; + panSld = nullptr; + removeBtn = nullptr; +} + +void GainStageSlotComponent::resized() { + BaseSlotComponent::resized(); + + juce::Rectangle availableArea = getAvailableSlotArea().reduced(1, 1); + + const int meterWidth {availableArea.getWidth() / 2}; + valueLabel->setBounds(availableArea.withWidth(dBToXPos(0, meterWidth))); + + if (levelMeter != nullptr) { + levelMeter->setBounds(availableArea.removeFromLeft(meterWidth)); + } + + const int elementWidth {availableArea.getWidth() / 3}; + gainSld->setBounds(availableArea.removeFromLeft(elementWidth)); + panSld->setBounds(availableArea.removeFromLeft(elementWidth)); + removeBtn->setBounds(availableArea.removeFromLeft(elementWidth)); +} + +void GainStageSlotComponent::sliderValueChanged(juce::Slider* sliderThatWasMoved) { + if (sliderThatWasMoved == gainSld.get()) { + // Convert log gain to linear + const double linearGain {WECore::CoreMath::dBToLinear(gainSld->getValue())}; + _pluginSelectionInterface.setGainStageGain(_chainNumber, _slotNumber, linearGain); + } else if (sliderThatWasMoved == panSld.get()) { + _pluginSelectionInterface.setGainStagePan(_chainNumber, _slotNumber, panSld->getValue()); + } +} + +void GainStageSlotComponent::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == removeBtn.get()) { + // Stop the meter before deleting anything that it relies on + levelMeter->stop(); + + _pluginSelectionInterface.removePlugin(_chainNumber, _slotNumber); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GainStageSlotComponent.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GainStageSlotComponent.h new file mode 100644 index 00000000..36670982 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GainStageSlotComponent.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include "BaseSlotComponent.h" +#include "PluginSelectionInterface.h" +#include "UIUtils.h" +#include "General/CoreMath.h" +#include "CoreJUCEPlugin/LabelReadoutSlider.h" + +/** + * Displays the output amplitude of this gain stage. + */ +class GainStageMeter : public UIUtils::SafeAnimatedComponent { +public: + GainStageMeter(const PluginSelectionInterface& pluginSelectionInterface, int chainNumber, int slotNumber); + ~GainStageMeter() = default; + + void paint(juce::Graphics& g) override; + +private: + const PluginSelectionInterface& _pluginSelectionInterface; + const int _chainNumber; + const int _slotNumber; +}; + +/** + * Represents a gain stage in a chain. + */ +class GainStageSlotComponent : public BaseSlotComponent, + public juce::Slider::Listener, + public juce::Button::Listener { +public: + GainStageSlotComponent(PluginSelectionInterface& pluginSelectionInterface, + int chainNumber, + int slotNumber); + ~GainStageSlotComponent() override; + + void resized() override; + void sliderValueChanged(juce::Slider* sliderThatWasMoved) override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + +private: + UIUtils::StandardSliderLookAndFeel _gainSliderLookAndFeel; + UIUtils::MidAnchoredSliderLookAndFeel _panSliderLookAndFeel; + + PluginSelectionInterface& _pluginSelectionInterface; + + std::unique_ptr _buttonLookAndFeel; + std::unique_ptr levelMeter; + std::unique_ptr valueLabel; + std::unique_ptr> gainSld; + std::unique_ptr> panSld; + std::unique_ptr removeBtn; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GraphViewComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GraphViewComponent.cpp new file mode 100644 index 00000000..f1489ed2 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GraphViewComponent.cpp @@ -0,0 +1,120 @@ +#include "GraphViewComponent.h" +#include "UIUtils.h" +#include "ModelInterface.hpp" + +namespace { + std::vector getChainViewScrollPositions(const std::vector>& chainViews) { + std::vector scrollPositions; + for (const std::unique_ptr& chainView : chainViews) { + scrollPositions.push_back(chainView->getScrollPosition()); + } + + return scrollPositions; + } + + void setChainViewScrollPositions(std::vector>& chainViews, const std::vector& scrollPositions) { + const size_t numChains { + std::min(chainViews.size(), scrollPositions.size()) + }; + + for (int chainIndex {0}; chainIndex < numChains; chainIndex++) { + chainViews[chainIndex]->setScrollPosition(scrollPositions[chainIndex]); + } + } +} + +GraphViewComponent::GraphViewComponent(SyndicateAudioProcessor& processor) + : _processor(processor), + _pluginSelectionInterface(processor), + _pluginModulationInterface(processor), + _hasRestoredScroll(false) { + + _viewPort.reset(new UIUtils::LinkedScrollView()); + _viewPort->setViewedComponent(new juce::Component()); + _viewPort->setScrollBarsShown(false, true); + _viewPort->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + _viewPort->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, UIUtils::neutralColour.withAlpha(0.5f)); + _viewPort->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + addAndMakeVisible(_viewPort.get()); +} + +GraphViewComponent::~GraphViewComponent() { + // Store scroll positions + _processor.mainWindowState.graphViewScrollPosition = _viewPort->getViewPositionX(); + _processor.mainWindowState.chainViewScrollPositions = getChainViewScrollPositions(_chainViews); + + _viewPort = nullptr; +} + +void GraphViewComponent::resized() { + std::scoped_lock lock(_graphMutex); + const int scrollPosition {_viewPort->getViewPositionX()}; + + _viewPort->setBounds(getLocalBounds()); + + const int scrollableWidth {std::max(getWidth(), static_cast(UIUtils::CHAIN_WIDTH * _chainViews.size()))}; + const int scrollableHeight {getHeight()}; + _viewPort->getViewedComponent()->setBounds(juce::Rectangle(scrollableWidth, scrollableHeight)); + + juce::Rectangle scrollableArea = _viewPort->getViewedComponent()->getLocalBounds(); + if (scrollableWidth == getWidth()) { + // If the scrollable area is the same as the width we need to remove from the left to + // make the chains centred properly (otherwise we just left align them since the scrolling + // will make it appear correct) + scrollableArea.removeFromLeft(UIUtils::getChainXPos(0, _chainViews.size(), getWidth())); + } + + for (std::unique_ptr& chainView : _chainViews) { + chainView->setBounds(scrollableArea.removeFromLeft(UIUtils::CHAIN_WIDTH)); + } + + // Maintain the previous scroll position + _viewPort->setViewPosition(scrollPosition, 0); +} + +void GraphViewComponent::onParameterUpdate() { + // Lock here because we're in onParameterUpdate, so the UI thread could change something while + // we're here + std::scoped_lock lock(_graphMutex); + + // Store the scroll positions of each chain + std::vector chainScrollPositions = getChainViewScrollPositions(_chainViews); + + _chainViews.clear(); + + const int totalNumChains {static_cast(ModelInterface::getNumChains(_processor.manager))}; + + const int numChainsToDisplay { + _processor.getSplitType() == SPLIT_TYPE::SERIES ? 1 : + _processor.getSplitType() == SPLIT_TYPE::PARALLEL || _processor.getSplitType() == SPLIT_TYPE::MULTIBAND ? totalNumChains : + _processor.getSplitType() == SPLIT_TYPE::LEFTRIGHT || _processor.getSplitType() == SPLIT_TYPE::MIDSIDE ? 2 : 1 + }; + + ModelInterface::forEachChain(_processor.manager, [&](int chainNumber, std::shared_ptr chain) { + if (chainNumber < numChainsToDisplay) { + _chainViews.push_back(std::make_unique(chainNumber, _pluginSelectionInterface, _pluginModulationInterface)); + _viewPort->getViewedComponent()->addAndMakeVisible(_chainViews[chainNumber].get()); + _chainViews[chainNumber]->setPlugins(chain); + } + }); + + setChainViewScrollPositions(_chainViews, chainScrollPositions); + + resized(); + + // We need to run this only once after the graph view has been constructed to restore the scroll + // position to the same as before the UI was last closed + if (!_hasRestoredScroll) { + _hasRestoredScroll = true; + + // Graph horizontal scroll + _viewPort->setViewPosition(_processor.mainWindowState.graphViewScrollPosition, 0); + + // Chains vertical scroll + setChainViewScrollPositions(_chainViews, _processor.mainWindowState.chainViewScrollPositions); + } +} + +void GraphViewComponent::closeGuestPluginWindows() { + _pluginSelectionInterface.closeGuestPluginWindows(); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GraphViewComponent.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GraphViewComponent.h new file mode 100644 index 00000000..da07947c --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/GraphViewComponent.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "PluginSlotComponent.h" +#include "ChainViewComponent.h" +#include "UIUtils.h" + +class GraphViewComponent : public juce::Component { +public: + GraphViewComponent(SyndicateAudioProcessor& processor); + ~GraphViewComponent(); + + void resized() override; + + void onParameterUpdate(); + + void closeGuestPluginWindows(); + + UIUtils::LinkedScrollView* getViewport() { return _viewPort.get(); } + +private: + SyndicateAudioProcessor& _processor; + std::vector> _chainViews; + PluginSelectionInterface _pluginSelectionInterface; + PluginModulationInterface _pluginModulationInterface; + std::unique_ptr _viewPort; + bool _hasRestoredScroll; + std::recursive_mutex _graphMutex; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationInterface.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationInterface.cpp new file mode 100644 index 00000000..fec94c5f --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationInterface.cpp @@ -0,0 +1,129 @@ +#include "PluginModulationInterface.h" +#include "GraphViewComponent.h" +#include "ModelInterface.hpp" + +namespace { + juce::Array getParamsExcludingSelected( + const juce::Array& pluginParameters, + PluginModulationConfig config) { + // Get the list of all parameters, and create a subset that includes only ones that haven't + // been selected yet + // TODO this might break if the plugin changes its list of parameters after this list has + // been created + juce::Array availableParameters; + + for (juce::AudioProcessorParameter* thisParam : pluginParameters) { + bool shouldCopy {true}; + + // If this parameter name is already in the config, don't add it to the list + for (const std::shared_ptr paramConfig : config.parameterConfigs) { + const juce::String thisParamName = thisParam->getName(PluginParameterModulationConfig::PLUGIN_PARAMETER_NAME_LENGTH_LIMIT); + if (thisParamName == paramConfig->targetParameterName) { + shouldCopy = false; + break; + } + } + + if (shouldCopy) { + availableParameters.add(thisParam); + } + } + + return availableParameters; + } +} + +PluginModulationInterface::PluginModulationInterface(SyndicateAudioProcessor& processor) + : _processor(processor) { +} + +PluginModulationConfig PluginModulationInterface::getPluginModulationConfig(int chainNumber, int pluginNumber) { + return ModelInterface::getPluginModulationConfig(_processor.manager, chainNumber, pluginNumber); +} + +void PluginModulationInterface::togglePluginModulationActive(int chainNumber, int pluginNumber) { + PluginModulationConfig config = ModelInterface::getPluginModulationConfig(_processor.manager, chainNumber, pluginNumber); + _processor.setPluginModulationIsActive(chainNumber, pluginNumber, !config.isActive); +} + +void PluginModulationInterface::addSourceToTarget(int chainNumber, int pluginNumber, int targetNumber, ModulationSourceDefinition source) { + _processor.addModulationSourceToTarget(chainNumber, pluginNumber, targetNumber, source); +} + +void PluginModulationInterface::removeSourceFromTarget(int chainNumber, int pluginNumber, int targetNumber, ModulationSourceDefinition source) { + _processor.removeModulationSourceFromTarget(chainNumber, pluginNumber, targetNumber, source); +} + +void PluginModulationInterface::setModulationTargetValue(int chainNumber, int pluginNumber, int targetNumber, float val) { + _processor.setModulationTargetValue(chainNumber, pluginNumber, targetNumber, val); +} + +void PluginModulationInterface::setModulationSourceValue(int chainNumber, int pluginNumber, int targetNumber, int sourceNumber, float val) { + _processor.setModulationSourceValue(chainNumber, pluginNumber, targetNumber, sourceNumber, val); +} + +void PluginModulationInterface::selectModulationTarget(int chainNumber, int pluginNumber, int targetNumber) { + // Collect the parameter list for this plugin + std::shared_ptr plugin = + ModelInterface::getPlugin(_processor.manager, chainNumber, pluginNumber); + + if (plugin != nullptr) { + // Create the selector + const PluginModulationConfig config = ModelInterface::getPluginModulationConfig(_processor.manager, chainNumber, pluginNumber); + + const bool isReplacingParameter {targetNumber < config.parameterConfigs.size()}; + + PluginParameterSelectorListParameters parameters { + _processor.pluginParameterSelectorState, + getParamsExcludingSelected(plugin->getParameters(), config), + [&, chainNumber, pluginNumber, targetNumber](juce::AudioProcessorParameter* parameter, bool shouldClose) { _onPluginParameterSelected(parameter, chainNumber, pluginNumber, targetNumber, shouldClose); }, + isReplacingParameter + }; + + const juce::String title = isReplacingParameter + ? "Replacing parameter " + config.parameterConfigs[targetNumber]->targetParameterName + : "New parameter for plugin " + plugin->getPluginDescription().name; + + _parameterSelectorWindow.reset(new PluginParameterSelectorWindow( + [&]() { _parameterSelectorWindow.reset(); }, parameters, title)); + + _parameterSelectorWindow->takeFocus(); + } +} + +void PluginModulationInterface::removeModulationTarget(int chainNumber, int pluginNumber, int targetNumber) { + _processor.removeModulationTarget(chainNumber, pluginNumber, targetNumber); +} + +juce::AudioProcessorParameter* PluginModulationInterface::getPluginParameterForTarget(int chainNumber, int pluginNumber, int targetNumber) { + juce::AudioProcessorParameter* retVal {nullptr}; + + PluginModulationConfig config = ModelInterface::getPluginModulationConfig(_processor.manager, chainNumber, pluginNumber); + + if (config.parameterConfigs.size() > targetNumber) { + std::shared_ptr plugin = ModelInterface::getPlugin(_processor.manager, chainNumber, pluginNumber); + + if (plugin != nullptr) { + const juce::String paramName(config.parameterConfigs[targetNumber]->targetParameterName); + + const juce::Array& parameters = plugin->getParameters(); + for (juce::AudioProcessorParameter* thisParam : parameters) { + if (thisParam->getName(PluginParameterModulationConfig::PLUGIN_PARAMETER_NAME_LENGTH_LIMIT) == paramName) { + retVal = thisParam; + break; + } + } + } + } + + return retVal; +} + +void PluginModulationInterface::_onPluginParameterSelected(juce::AudioProcessorParameter* parameter, int chainNumber, int pluginNumber, int targetNumber, bool shouldClose) { + _processor.setModulationTarget(chainNumber, pluginNumber, targetNumber, parameter->getName(PluginParameterModulationConfig::PLUGIN_PARAMETER_NAME_LENGTH_LIMIT)); + _parameterSelectorWindow.reset(); + + if (!shouldClose) { + selectModulationTarget(chainNumber, pluginNumber, targetNumber + 1); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationInterface.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationInterface.h new file mode 100644 index 00000000..bb425921 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationInterface.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "PluginParameterSelectorWindow.h" + +class GraphViewComponent; + +/** + * The interface between the processor and parts of the UI that control modulation. + */ +class PluginModulationInterface { +public: + PluginModulationInterface(SyndicateAudioProcessor& processor); + ~PluginModulationInterface() = default; + + PluginModulationConfig getPluginModulationConfig(int chainNumber, int pluginNumber); + void togglePluginModulationActive(int chainNumber, int pluginNumber); + void addSourceToTarget(int chainNumber, int pluginNumber, int targetNumber, ModulationSourceDefinition source); + void removeSourceFromTarget(int chainNumber, int pluginNumber, int targetNumber, ModulationSourceDefinition source); + void setModulationTargetValue(int chainNumber, int pluginNumber, int targetNumber, float val); + void setModulationSourceValue(int chainNumber, int pluginNumber, int targetNumber, int sourceNumber, float val); + void selectModulationTarget(int chainNumber, int pluginNumber, int targetNumber); + void removeModulationTarget(int chainNumber, int pluginNumber, int targetNumber); + juce::AudioProcessorParameter* getPluginParameterForTarget(int chainNumber, int pluginNumber, int targetNumber); + +private: + SyndicateAudioProcessor& _processor; + std::unique_ptr _parameterSelectorWindow; + + void _onPluginParameterSelected(juce::AudioProcessorParameter* parameter, int chainNumber, int pluginNumber, int targetNumber, bool shouldClose); +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationTarget.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationTarget.cpp new file mode 100644 index 00000000..94b1e0e5 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationTarget.cpp @@ -0,0 +1,201 @@ +#include "PluginModulationTarget.h" +#include "UIUtils.h" + +void PluginModulationTargetButton::mouseDown(const juce::MouseEvent& event) { + // If this is a right click, we need to remove the target on mouseUp + if (event.mods.isRightButtonDown()) { + _isRightClick = true; + } else { + _isRightClick = false; + } + + juce::Button::mouseDown(event); +} + +void PluginModulationTargetButton::mouseUp(const juce::MouseEvent& event) { + // If it's a right click make sure the mouse is still over the button + if (_isRightClick && isDown() && isOver()) { + // Don't send an event, just call the callback + _onRemoveCallback(); + } else { + juce::Button::mouseUp(event); + } +} + +PluginModulationTarget::PluginModulationTarget(PluginModulationInterface& pluginModulationInterface, + int chainNumber, + int pluginNumber, + int targetNumber) : + _pluginModulationInterface(pluginModulationInterface), + _chainNumber(chainNumber), + _pluginNumber(pluginNumber), + _targetNumber(targetNumber) { + + juce::AudioProcessorParameter* param = _pluginModulationInterface.getPluginParameterForTarget(chainNumber, pluginNumber, targetNumber); + _targetSlider.reset(new ModulationTargetSlider("Plugin parameter slider", [param]() { return param == nullptr ? 0.0f : param->getValue(); })); + addAndMakeVisible(_targetSlider.get()); + _targetSlider->setRange(0, 1, 0); + _targetSlider->setSliderStyle(juce::Slider::RotaryVerticalDrag); + _targetSlider->setTextBoxStyle(juce::Slider::NoTextBox, false, 80, 20); + _targetSlider->setLookAndFeel(&_sliderLookAndFeel); + _targetSlider->setColour(juce::Slider::rotarySliderFillColourId, UIUtils::highlightColour); + _targetSlider->setTooltip("Control for the selected plugin parameter, drag a source here to modulate"); + _targetSlider->onValueChange = [&]() { + _pluginModulationInterface.setModulationTargetValue(_chainNumber, _pluginNumber, _targetNumber, _targetSlider->getValue()); + }; + + _targetSelectButton.reset(new PluginModulationTargetButton([&]() { _pluginModulationInterface.removeModulationTarget(_chainNumber, _pluginNumber, _targetNumber); })); + addAndMakeVisible(_targetSelectButton.get()); + _targetSelectButton->addListener(this); + _targetSelectButton->setLookAndFeel(&_buttonLookAndFeel); + _targetSelectButton->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + _targetSelectButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + _targetSelectButton->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + + _targetAddButton.reset(new juce::TextButton("Param")); + addAndMakeVisible(_targetAddButton.get()); + _targetAddButton->addListener(this); + _targetAddButton->setLookAndFeel(&_addButtonLookAndFeel); + _targetAddButton->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + _targetAddButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + _targetAddButton->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + _targetAddButton->setTooltip("Add a plugin parameter to modulate"); + + const PluginModulationConfig& modulationConfig = + _pluginModulationInterface.getPluginModulationConfig(_chainNumber, _pluginNumber); + + std::vector> sources; + if (modulationConfig.parameterConfigs.size() > _targetNumber) { + sources = modulationConfig.parameterConfigs[_targetNumber]->sources; + } + + _sourceSliders.reset(new ModulationTargetSourceSliders( + sources, + [&](int slotIndex, float value) { _pluginModulationInterface.setModulationSourceValue(_chainNumber, _pluginNumber, _targetNumber, slotIndex, value); }, + [&](ModulationSourceDefinition definition) { _removeTargetSlot(definition); })); + addAndMakeVisible(_sourceSliders.get()); + + _reloadState(); +} + +PluginModulationTarget::~PluginModulationTarget() { + _targetSlider->setLookAndFeel(nullptr); + _targetSelectButton->setLookAndFeel(nullptr); + _targetAddButton->setLookAndFeel(nullptr); + + _targetSlider = nullptr; + _targetSelectButton = nullptr; + _targetAddButton = nullptr; +} + +void PluginModulationTarget::resized() { + juce::Rectangle availableArea = getLocalBounds(); + + _targetSlider->setBounds(availableArea.removeFromTop(availableArea.getHeight() / 2)); + _targetSelectButton->setBounds(availableArea.removeFromTop(availableArea.getHeight() / 2)); + _targetAddButton->setBounds(getLocalBounds().withTrimmedBottom(availableArea.getHeight())); + _sourceSliders->setBounds(availableArea); +} + +void PluginModulationTarget::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == _targetSelectButton.get()) { + if (juce::ModifierKeys::currentModifiers.isRightButtonDown()) { + // Remove this target + _pluginModulationInterface.removeModulationTarget(_chainNumber, _pluginNumber, _targetNumber); + } else { + // Select a target + _pluginModulationInterface.selectModulationTarget(_chainNumber, _pluginNumber, _targetNumber); + } + } else if (buttonThatWasClicked == _targetAddButton.get()) { + // Select a target + _pluginModulationInterface.selectModulationTarget(_chainNumber, _pluginNumber, _targetNumber); + } +} + +bool PluginModulationTarget::isInterestedInDragSource(const SourceDetails& dragSourceDetails) { + const std::optional draggedDefinition = + ModulationSourceDefinition::fromVariant(dragSourceDetails.description); + + // Check dragged item contains a valid definition + if (!draggedDefinition.has_value()) { + return false; + } + + // Check if we already have this source assigned to this target + PluginModulationConfig modulationConfig = + _pluginModulationInterface.getPluginModulationConfig(_chainNumber, _pluginNumber); + + if (modulationConfig.parameterConfigs.size() > _targetNumber) { + std::vector> sources = modulationConfig.parameterConfigs[_targetNumber]->sources; + + for (const auto source : sources) { + if (source->definition == draggedDefinition.value()) { + return false; + } + } + } else { + // This is an empty target + return false; + } + + return true; +} + +void PluginModulationTarget::itemDragEnter(const SourceDetails& dragSourceDetails) { + // Make the modulation slot visible + const std::optional draggedDefinition = + ModulationSourceDefinition::fromVariant(dragSourceDetails.description); + + if (draggedDefinition.has_value()) { + // Add a slot to the UI (but not on the backend) + _sourceSliders->addSource(draggedDefinition.value()); + } +} + +void PluginModulationTarget::itemDragExit(const SourceDetails& /*dragSourceDetails*/) { + // Remove the modulation slot from the UI + _sourceSliders->removeLastSource(); +} + +void PluginModulationTarget::itemDropped(const SourceDetails& dragSourceDetails) { + const std::optional draggedDefinition = + ModulationSourceDefinition::fromVariant(dragSourceDetails.description); + + // Add another source to the target + if (draggedDefinition.has_value()) { + _pluginModulationInterface.addSourceToTarget(_chainNumber, _pluginNumber, _targetNumber, draggedDefinition.value()); + } +} + +void PluginModulationTarget::_removeTargetSlot(ModulationSourceDefinition definition) { + _pluginModulationInterface.removeSourceFromTarget(_chainNumber, _pluginNumber, _targetNumber, definition); +} + +void PluginModulationTarget::_reloadState() { + // Try to restore UI state from the modulation config stored in the processor + const PluginModulationConfig& modulationConfig = + _pluginModulationInterface.getPluginModulationConfig(_chainNumber, _pluginNumber); + + if (modulationConfig.parameterConfigs.size() > _targetNumber) { + _targetAddButton->setEnabled(false); + _targetAddButton->setVisible(false); + + const std::shared_ptr thisParameterConfig = modulationConfig.parameterConfigs[_targetNumber]; + + // Restoring slider position + _targetSlider->setEnabled(true); + _targetSlider->setVisible(true); + _targetSlider->setValue(thisParameterConfig->restValue, + juce::NotificationType::dontSendNotification); + + // Restoring button text + _targetSelectButton->setButtonText(thisParameterConfig->targetParameterName); + _targetSelectButton->setTooltip(thisParameterConfig->targetParameterName + " - Click to select a different plugin parameter, right click to remove"); + } else { + _targetSlider->setEnabled(false); + _targetSlider->setVisible(false); + _targetSelectButton->setEnabled(false); + _targetSelectButton->setVisible(false); + _sourceSliders->setVisible(false); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationTarget.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationTarget.h new file mode 100644 index 00000000..53e50725 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginModulationTarget.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include "PluginModulationInterface.h" +#include "ModulationSourceDefinition.hpp" +#include "UIUtils.h" +#include "ModulationTargetSlider.hpp" +#include "ModulationTargetSourceSlider.hpp" + +class PluginModulationTargetButton : public juce::TextButton { +public: + PluginModulationTargetButton(std::function onRemoveCallback) : _onRemoveCallback(onRemoveCallback), _isRightClick(false) {} + + void mouseDown(const juce::MouseEvent& event) override; + + void mouseUp(const juce::MouseEvent& event) override; + +private: + std::function _onRemoveCallback; + bool _isRightClick; +}; + +/** + * Contains all the UI components needed for a particular modulation target, eg. the target slider, + * target select button, and modulation slots. + */ +class PluginModulationTarget : public juce::Component, + public juce::Button::Listener, + public juce::DragAndDropTarget { +public: + PluginModulationTarget(PluginModulationInterface& pluginModulationInterface, + int chainNumber, + int pluginNumber, + int targetNumber); + + ~PluginModulationTarget(); + + void resized() override; + + void buttonClicked(juce::Button* buttonThatWasClicked) override; + + bool isInterestedInDragSource(const SourceDetails& dragSourceDetails) override; + void itemDragEnter(const SourceDetails& dragSourceDetails) override; + void itemDragExit(const SourceDetails& dragSourceDetails) override; + void itemDropped(const SourceDetails& dragSourceDetails) override; + +private: + PluginModulationInterface& _pluginModulationInterface; + int _chainNumber; + int _pluginNumber; + int _targetNumber; + std::unique_ptr _targetSlider; + std::unique_ptr _targetSelectButton; + std::unique_ptr _targetAddButton; + std::unique_ptr _sourceSliders; + UIUtils::StandardSliderLookAndFeel _sliderLookAndFeel; + UIUtils::StaticButtonLookAndFeel _buttonLookAndFeel; + UIUtils::AddButtonLookAndFeel _addButtonLookAndFeel; + + void _removeTargetSlot(ModulationSourceDefinition definition); + void _reloadState(); +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.cpp new file mode 100644 index 00000000..23ef0a7a --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.cpp @@ -0,0 +1,105 @@ +#include "PluginParameterSelectorComponent.h" + +PluginParameterSelectorComponent::PluginParameterSelectorComponent( + PluginParameterSelectorListParameters selectorListParameters, + std::function onCloseCallback) + : _state(selectorListParameters.state), + _onCloseCallback(onCloseCallback) { + _searchTextEditor.reset(new juce::TextEditor("Search Text Editor")); + addAndMakeVisible(_searchTextEditor.get()); + _searchTextEditor->setMultiLine(false); + _searchTextEditor->setReturnKeyStartsNewLine(false); + _searchTextEditor->setReadOnly(false); + _searchTextEditor->setScrollbarsShown(true); + _searchTextEditor->setCaretVisible(true); + _searchTextEditor->setPopupMenuEnabled(true); + _searchTextEditor->setText(juce::String()); + _searchTextEditor->setEscapeAndReturnKeysConsumed(false); + _searchTextEditor->setSelectAllWhenFocused(true); + _searchTextEditor->setWantsKeyboardFocus(true); + _searchTextEditor->addListener(this); + _searchTextEditor->setLookAndFeel(&_searchBarLookAndFeel); + _searchTextEditor->setColour(juce::TextEditor::outlineColourId, UIUtils::highlightColour); + _searchTextEditor->setColour(juce::TextEditor::backgroundColourId, UIUtils::backgroundColour); + _searchTextEditor->setColour(juce::TextEditor::textColourId, UIUtils::highlightColour); + _searchTextEditor->setColour(juce::TextEditor::highlightColourId, UIUtils::highlightColour); + _searchTextEditor->setColour(juce::TextEditor::highlightedTextColourId, UIUtils::neutralColour); + _searchTextEditor->setColour(juce::CaretComponent::caretColourId, UIUtils::highlightColour); + + _parameterTableListBox.reset(new PluginParameterSelectorTableListBox(selectorListParameters)); + addAndMakeVisible(_parameterTableListBox.get()); + _parameterTableListBox->setName("Plugin Parameter Table List Box"); + _parameterTableListBox->getHeader().setLookAndFeel(&_tableHeaderLookAndFeel); + _parameterTableListBox->getHeader().setColour(juce::TableHeaderComponent::textColourId, UIUtils::highlightColour); + _parameterTableListBox->getHeader().setColour(juce::TableHeaderComponent::outlineColourId, UIUtils::highlightColour); + _parameterTableListBox->getHeader().setColour(juce::TableHeaderComponent::backgroundColourId, UIUtils::backgroundColour); + _parameterTableListBox->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + _parameterTableListBox->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, UIUtils::neutralColour.withAlpha(0.5f)); + _parameterTableListBox->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + _parameterTableListBox->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + _parameterTableListBox->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, UIUtils::neutralColour.withAlpha(0.5f)); + _parameterTableListBox->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + + _hintLabel.reset(new juce::Label("Hint Label", juce::String())); + addAndMakeVisible(_hintLabel.get()); + _hintLabel->setFont(juce::Font (15.00f, juce::Font::plain).withTypefaceStyle("Regular")); + _hintLabel->setJustificationType(juce::Justification::centred); + _hintLabel->setEditable(false, false, false); + _hintLabel->setColour(juce::Label::textColourId, UIUtils::tooltipColour); + _hintLabel->setColour(juce::TextEditor::textColourId, juce::Colours::black); + _hintLabel->setColour(juce::TextEditor::backgroundColourId, juce::Colour(0x00000000)); + if (selectorListParameters.isReplacingParameter) { + _hintLabel->setText("Double click a parameter to select it", juce::NotificationType::dontSendNotification); + } else { + _hintLabel->setText("Double click a parameter to select it\nHold " + UIUtils::getCmdKeyName() + " to keep the window open and select another", juce::NotificationType::dontSendNotification); + } + + // Recall UI from state + _searchTextEditor->setText(_state.filterString, false); +} + +PluginParameterSelectorComponent::~PluginParameterSelectorComponent() { + // Save the scroll position + _state.scrollPosition = _parameterTableListBox->getVerticalPosition(); + + _searchTextEditor->setLookAndFeel(nullptr); + _parameterTableListBox->getHeader().setLookAndFeel(nullptr); + + _searchTextEditor = nullptr; + _parameterTableListBox = nullptr; + _hintLabel = nullptr; +} + +void PluginParameterSelectorComponent::resized() { + constexpr int MARGIN_SIZE {10}; + constexpr int ROW_HEIGHT {24}; + + juce::Rectangle area = getLocalBounds().reduced(MARGIN_SIZE); + _searchTextEditor->setBounds(area.removeFromTop(ROW_HEIGHT)); + _hintLabel->setBounds(area.removeFromBottom(ROW_HEIGHT * 2)); + area.removeFromTop(MARGIN_SIZE).removeFromBottom(MARGIN_SIZE); + _parameterTableListBox->setBounds(area); +} + +void PluginParameterSelectorComponent::textEditorTextChanged(juce::TextEditor& textEditor) { + _state.filterString = _searchTextEditor->getText(); + _parameterTableListBox->onFilterUpdate(); +} + +void PluginParameterSelectorComponent::paint(juce::Graphics& g) { + g.fillAll(UIUtils::backgroundColour); +} + +bool PluginParameterSelectorComponent::keyPressed(const juce::KeyPress& key) { + if (key.isKeyCode(juce::KeyPress::escapeKey)) { + // Close the window + _onCloseCallback(); + return true; + } + + return false; +} + +void PluginParameterSelectorComponent::restoreScrollPosition() { + _parameterTableListBox->setVerticalPosition(_state.scrollPosition); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.h new file mode 100644 index 00000000..3a6a4283 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include "PluginParameterSelectorList.h" +#include "PluginParameterSelectorListParameters.h" +#include "UIUtils.h" + +class PluginParameterSelectorComponent : public juce::Component, + public juce::TextEditor::Listener { +public: + PluginParameterSelectorComponent(PluginParameterSelectorListParameters selectorListParameters, std::function onCloseCallback); + ~PluginParameterSelectorComponent(); + + void textEditorTextChanged(juce::TextEditor& textEditor) override; + + void resized() override; + void paint(juce::Graphics& g) 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: + PluginParameterSelectorState& _state; + std::function _onCloseCallback; + + UIUtils::SearchBarLookAndFeel _searchBarLookAndFeel; + UIUtils::TableHeaderLookAndFeel _tableHeaderLookAndFeel; + + std::unique_ptr _searchTextEditor; + std::unique_ptr _parameterTableListBox; + std::unique_ptr _hintLabel; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorList.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorList.cpp new file mode 100644 index 00000000..2be92024 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorList.cpp @@ -0,0 +1,121 @@ +#include "PluginParameterSelectorList.h" +#include "PluginChain.hpp" +#include "PluginParameterSelectorState.h" +#include "UIUtils.h" + +PluginParameterListSorter::PluginParameterListSorter( + PluginParameterSelectorState& newState, + const juce::Array& fullParameterList) + : state(newState), + _fullParameterList(fullParameterList) { +} + + +juce::Array PluginParameterListSorter::getFilteredParameterList() const { + juce::Array filteredParameterList; + + // Do filtering first if needed + if (_isFilterNeeded()) { + for (juce::AudioProcessorParameter* thisParameter : _fullParameterList) { + if (_passesFilter(thisParameter)) { + filteredParameterList.add(thisParameter); + } + } + } else { + filteredParameterList = _fullParameterList; + } + + // Now sort the list + filteredParameterList.sort(*this, false); + + return filteredParameterList; +} + +int PluginParameterListSorter::compareElements(juce::AudioProcessorParameter* first, juce::AudioProcessorParameter* second) const { + return first->getName(PluginParameterModulationConfig::PLUGIN_PARAMETER_NAME_LENGTH_LIMIT).compare( + second->getName(PluginParameterModulationConfig::PLUGIN_PARAMETER_NAME_LENGTH_LIMIT)); +} + +bool PluginParameterListSorter::_isFilterNeeded() const { + return state.filterString.isNotEmpty(); +} + +bool PluginParameterListSorter::_passesFilter(const juce::AudioProcessorParameter* parameter) const { + return parameter->getName(PluginParameterModulationConfig::PLUGIN_PARAMETER_NAME_LENGTH_LIMIT).containsIgnoreCase(state.filterString) || state.filterString.isEmpty(); +} + + +PluginParameterSelectorTableListBoxModel::PluginParameterSelectorTableListBoxModel( + PluginParameterSelectorListParameters selectorListParameters) + : _parameterListSorter(selectorListParameters.state, + selectorListParameters.fullParameterList), + _parameterSelectedCallback(selectorListParameters.parameterSelectedCallback), + _isReplacingParameter(selectorListParameters.isReplacingParameter) { + _parameterList = _parameterListSorter.getFilteredParameterList(); +} + +void PluginParameterSelectorTableListBoxModel::onFilterUpdate() { + _parameterList = _parameterListSorter.getFilteredParameterList(); +} + +int PluginParameterSelectorTableListBoxModel::getNumRows() { + return _parameterList.size(); +} + +void PluginParameterSelectorTableListBoxModel::paintRowBackground(juce::Graphics& g, + int /*rowNumber*/, + int /*width*/, + int /*height*/, + bool /*rowIsSelected*/) { + g.fillAll(UIUtils::backgroundColour); +} + +void PluginParameterSelectorTableListBoxModel::paintCell(juce::Graphics& g, + int rowNumber, + int /*columnId*/, + int width, + int height, + bool /*rowIsSelected*/) { + if (rowNumber < _parameterList.size()) { + const juce::String text = _parameterList[rowNumber]->getName(PluginParameterModulationConfig::PLUGIN_PARAMETER_NAME_LENGTH_LIMIT); + + g.setColour(UIUtils::neutralColour); + g.drawText(text, 2, 0, width - 4, height, juce::Justification::centredLeft, true); + } +} + +void PluginParameterSelectorTableListBoxModel::cellDoubleClicked(int rowNumber, + int /*columnId*/, + const juce::MouseEvent& /*event*/) { + const bool shouldCloseWindow {!juce::ModifierKeys::currentModifiers.isCommandDown() || _isReplacingParameter}; + _parameterSelectedCallback(_parameterList[rowNumber], shouldCloseWindow); +} + +PluginParameterSelectorTableListBox::PluginParameterSelectorTableListBox( + PluginParameterSelectorListParameters selectorListParameters) + : _parameterTableListBoxModel(selectorListParameters) { + constexpr int flags {juce::TableHeaderComponent::visible}; + + getHeader().addColumn("Name", + 1, // ID + 0, // Width - will be set by resized() + -1, // Min width + -1, // Max width + flags); + + setModel(&_parameterTableListBoxModel); + setColour(juce::ListBox::backgroundColourId, juce::Colour(0x00000000)); +} + +void PluginParameterSelectorTableListBox::resized() { + constexpr int scrollBarWidth {10}; + getHeader().setColumnWidth(1, getWidth() - scrollBarWidth); + + // Need to call this otherwise rows won't draw properly + juce::TableListBox::resized(); +} + +void PluginParameterSelectorTableListBox::onFilterUpdate() { + _parameterTableListBoxModel.onFilterUpdate(); + updateContent(); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorList.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorList.h new file mode 100644 index 00000000..5f85cf05 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorList.h @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include "PluginParameterSelectorListParameters.h" +#include "PluginParameterSelectorState.h" + +class PluginParameterListSorter { +public: + PluginParameterSelectorState& state; + + PluginParameterListSorter(PluginParameterSelectorState& newState, + const juce::Array& fullParameterList); + ~PluginParameterListSorter() = default; + + juce::Array getFilteredParameterList() const; + + int compareElements(juce::AudioProcessorParameter* first, juce::AudioProcessorParameter* second) const; + +private: + // We need to take ownership of this array here + const juce::Array _fullParameterList; + + bool _isFilterNeeded() const; + bool _passesFilter(const juce::AudioProcessorParameter* parameter) const; +}; + +class PluginParameterSelectorTableListBoxModel : public juce::TableListBoxModel { +public: + PluginParameterSelectorTableListBoxModel(PluginParameterSelectorListParameters selectorListParameters); + ~PluginParameterSelectorTableListBoxModel() = default; + + void onFilterUpdate(); + + int getNumRows() override; + + void paintRowBackground(juce::Graphics& g, int rowNumber, int width, int height, bool rowIsSelected) override; + + 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; + +private: + PluginParameterListSorter _parameterListSorter; + juce::Array _parameterList; + std::function _parameterSelectedCallback; + const bool _isReplacingParameter; +}; + +class PluginParameterSelectorTableListBox : public juce::TableListBox { +public: + PluginParameterSelectorTableListBox(PluginParameterSelectorListParameters selectorListParameters); + ~PluginParameterSelectorTableListBox() = default; + + virtual void resized() override; + + void onFilterUpdate(); + +private: + PluginParameterSelectorTableListBoxModel _parameterTableListBoxModel; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorListParameters.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorListParameters.h new file mode 100644 index 00000000..50d59126 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorListParameters.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "PluginParameterSelectorState.h" + +struct PluginParameterSelectorListParameters { + PluginParameterSelectorState& state; + const juce::Array fullParameterList; + std::function parameterSelectedCallback; + bool isReplacingParameter; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorState.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorState.cpp new file mode 100644 index 00000000..2e1be126 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorState.cpp @@ -0,0 +1,38 @@ +#include "PluginParameterSelectorState.h" + +namespace { + const char* XML_SORT_FILTER_STRING_STR {"filterString"}; + const char* XML_SORT_BOUNDS_STR {"bounds"}; + const char* XML_SORT_SCROLL_POSITION_STR {"scrollPosition"}; +} + +void PluginParameterSelectorState::restoreFromXml(juce::XmlElement* element) { + 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::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 PluginParameterSelectorState::writeToXml(juce::XmlElement* element) const { + 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); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorState.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorState.h new file mode 100644 index 00000000..4a8fc920 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorState.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +struct PluginParameterSelectorState { + // 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> bounds; + + // Stores scroll position (value from 0 to 1) + double scrollPosition; + + PluginParameterSelectorState() : scrollPosition(0) { } + + void restoreFromXml(juce::XmlElement* element); + void writeToXml(juce::XmlElement* element) const; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.cpp new file mode 100644 index 00000000..2813b113 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.cpp @@ -0,0 +1,58 @@ +#include "PluginParameterSelectorWindow.h" +#include "UIUtils.h" + +namespace { + const juce::Colour BACKGROUND_COLOUR(0, 0, 0); + constexpr int TITLE_BAR_BUTTONS { + juce::DocumentWindow::TitleBarButtons::minimiseButton | + juce::DocumentWindow::TitleBarButtons::closeButton + }; +} + +PluginParameterSelectorWindow::PluginParameterSelectorWindow( + std::function onCloseCallback, + PluginParameterSelectorListParameters selectorListParameters, + juce::String title) : + juce::DocumentWindow(title, + BACKGROUND_COLOUR, + TITLE_BAR_BUTTONS, + true), + _onCloseCallback(onCloseCallback), + _content(nullptr), + _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 {300}; + constexpr int DEFAULT_HEIGHT {500}; + centreWithSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); + } + + setVisible(true); + setResizable(true, true); + setAlwaysOnTop(true); + _content = new PluginParameterSelectorComponent(selectorListParameters, onCloseCallback); + setContentOwned(_content, false); + _content->restoreScrollPosition(); + + juce::Logger::writeToLog("Created PluginParameterSelectorWindow"); +} + +PluginParameterSelectorWindow::~PluginParameterSelectorWindow() { + juce::Logger::writeToLog("Closing PluginParameterSelectorWindow"); + _state.bounds = getBounds(); + clearContentComponent(); +} + +void PluginParameterSelectorWindow::closeButtonPressed() { + _onCloseCallback(); +} + +void PluginParameterSelectorWindow::takeFocus() { + if (_content != nullptr) { + _content->grabKeyboardFocus(); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.h new file mode 100644 index 00000000..4c4b3bd3 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include "PluginParameterSelectorComponent.h" +#include "PluginParameterSelectorListParameters.h" + +class PluginParameterSelectorWindow : public juce::DocumentWindow { +public: + PluginParameterSelectorWindow(std::function onCloseCallback, + PluginParameterSelectorListParameters selectorListParameters, + juce::String title); + virtual ~PluginParameterSelectorWindow(); + + virtual void closeButtonPressed() override; + + void takeFocus(); + +private: + std::function _onCloseCallback; + PluginParameterSelectorComponent* _content; + + // We need to keep a reference to state to update the bounds on resize + PluginParameterSelectorState& _state; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSelectionInterface.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSelectionInterface.cpp new file mode 100644 index 00000000..48b4e24a --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSelectionInterface.cpp @@ -0,0 +1,236 @@ +#include "PluginSelectionInterface.h" +#include "UIUtils.h" +#include "PluginUtils.h" + +PluginSelectionInterface::PluginSelectionInterface(SyndicateAudioProcessor& processor) + : _processor(processor), + _chainNumber(0), + _pluginNumber(0) { +} + +void PluginSelectionInterface::selectNewPlugin(int chainNumber, int pluginNumber) { + _chainNumber = chainNumber; + _pluginNumber = pluginNumber; + + const bool isReplacingPlugin {ModelInterface::getPlugin(_processor.manager, chainNumber, pluginNumber) != nullptr}; + + PluginSelectorListParameters parameters { + _processor.pluginScanClient, + _processor.pluginSelectorState, + _processor.formatManager, + [&](std::unique_ptr plugin, const juce::String& error, bool shouldClose) { _onPluginSelected(std::move(plugin), error, shouldClose); }, + [&]() { return _processor.getSampleRate(); }, + [&]() { return _processor.getBlockSize(); }, + isReplacingPlugin + }; + + std::unique_ptr style = std::make_unique( + UIUtils::backgroundColour, + UIUtils::slotBackgroundColour, + UIUtils::neutralColour, + UIUtils::highlightColour, + UIUtils::deactivatedColour, + std::make_unique(), + std::make_unique(), + std::make_unique(), + std::make_unique() + ); + + + const juce::String title = isReplacingPlugin + ? "Replacing plugin " + getPluginName(chainNumber, pluginNumber) + : "New plugin for chain " + juce::String(chainNumber + 1); + + _pluginSelectorWindow = std::make_unique( + [&]() { _errorPopover.reset(); _pluginSelectorWindow.reset(); }, + parameters, + std::move(style), + title + ); + + _pluginSelectorWindow->takeFocus(); +} + +juce::String PluginSelectionInterface::getPluginName(int chainNumber, int pluginNumber) { + juce::String retVal; + + std::shared_ptr plugin = + ModelInterface::getPlugin(_processor.manager, chainNumber, pluginNumber); + + if (plugin != nullptr) { + retVal = plugin->getPluginDescription().name; + } + + return retVal; +} + +void PluginSelectionInterface::openPluginEditor(int chainNumber, int pluginNumber) { + std::shared_ptr plugin = + ModelInterface::getPlugin(_processor.manager, chainNumber, pluginNumber); + + if (plugin != nullptr) { + // Check if a window is already open for this plugin + for (const std::unique_ptr& window : _guestPluginWindows) { + if (window->plugin == plugin) { + // Don't open another window for this plugin + return; + } + } + + std::shared_ptr editorBounds = + ModelInterface::getPluginEditorBounds(_processor.manager, chainNumber, pluginNumber); + if (editorBounds != nullptr) { + _guestPluginWindows.emplace_back(new GuestPluginWindow([&, plugin]() { _onPluginWindowClose(plugin); }, plugin, editorBounds)); + } + } +} + +void PluginSelectionInterface::removePlugin(int chainNumber, int pluginNumber) { + std::shared_ptr plugin = + ModelInterface::getPlugin(_processor.manager, chainNumber, pluginNumber); + + if (plugin != nullptr) { + // Check if the plugin owns an open editor window and close it + for (int index {0}; index < _guestPluginWindows.size(); index++) { + if (_guestPluginWindows[index]->plugin == plugin) { + _guestPluginWindows.erase(_guestPluginWindows.begin() + index); + break; + } + } + } + + // Actually remove the plugin + _processor.removePlugin(chainNumber, pluginNumber); +} + +void PluginSelectionInterface::togglePluginBypass(int chainNumber, int pluginNumber) { + const bool newBypass { + !ModelInterface::getSlotBypass(_processor.manager, chainNumber, pluginNumber) + }; + _processor.setSlotBypass(chainNumber, pluginNumber, newBypass); +} + +bool PluginSelectionInterface::getPluginBypass(int chainNumber, int pluginNumber) { + return ModelInterface::getSlotBypass(_processor.manager, chainNumber, pluginNumber); +} + +void PluginSelectionInterface::insertGainStage(int chainNumber, int pluginNumber) { + _processor.insertGainStage(chainNumber, pluginNumber); +} + +void PluginSelectionInterface::copySlot(int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber) { + _processor.copySlot(fromChainNumber, fromSlotNumber, toChainNumber, toSlotNumber); +} + +void PluginSelectionInterface::moveSlot(int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber) { + _processor.moveSlot(fromChainNumber, fromSlotNumber, toChainNumber, toSlotNumber); +} + +void PluginSelectionInterface::_onPluginSelected(std::unique_ptr plugin, const juce::String& error, bool shouldClose) { + auto createErrorPopover = [&](juce::String errorText) { + if (_pluginSelectorWindow != nullptr) { + _errorPopover.reset(new UIUtils::PopoverComponent( + "Failed to load plugin", errorText, [&]() {_errorPopover.reset(); })); + + juce::Component* targetComponent = _pluginSelectorWindow->findChildWithID(Utils::pluginSelectorComponentID); + if (targetComponent != nullptr) { + targetComponent->addAndMakeVisible(_errorPopover.get()); + _errorPopover->setBounds(targetComponent->getLocalBounds()); + } + } + }; + + if (plugin != nullptr) { + juce::Logger::writeToLog("PluginSelectionInterface::_onPluginSelected: Loading plugin"); + + // Create the shared pointer here as we need it for the window + std::shared_ptr sharedPlugin = std::move(plugin); + + // If we are replacing a previous plugin, we need to check if it had a window open and close + // it + const std::shared_ptr previousPlugin = + ModelInterface::getPlugin(_processor.manager, _chainNumber, _pluginNumber); + + if (previousPlugin != nullptr) { + for (int index {0}; index < _guestPluginWindows.size(); index++) { + if (_guestPluginWindows[index]->plugin == previousPlugin) { + _guestPluginWindows.erase(_guestPluginWindows.begin() + index); + break; + } + } + } + + // Pass the plugin to the processor + if (_processor.onPluginSelectedByUser(sharedPlugin, _chainNumber, _pluginNumber)) { + juce::Logger::writeToLog("PluginSelectionInterface::_onPluginSelected: Loaded successfully"); + + // Create the new plugin window + std::shared_ptr editorBounds = + ModelInterface::getPluginEditorBounds(_processor.manager, _chainNumber, _pluginNumber); + if (editorBounds != nullptr) { + _guestPluginWindows.emplace_back( + new GuestPluginWindow([&, sharedPlugin]() { _onPluginWindowClose(sharedPlugin); }, + sharedPlugin, + editorBounds)); + } + + // Close the selector window + _pluginSelectorWindow.reset(); + + if (!shouldClose) { + selectNewPlugin(_chainNumber, _pluginNumber + 1); + } + } else { + juce::Logger::writeToLog("PluginSelectionInterface::_onPluginSelected: Failed to load " + sharedPlugin->getPluginDescription().name); + + // Failed to load plugin, show error in selector window + createErrorPopover(sharedPlugin->getPluginDescription().name + " doesn't support the required inputs/outputs\nThis may be synth, or a mono only plugin being loaded into a stereo instance of Syndicate or vice versa"); + } + } else { + // Plugin failed to load + juce::Logger::writeToLog("PluginSelectionInterface::_onPluginSelected: Failed to load plugin: " + error); + createErrorPopover(error); + } +} + +void PluginSelectionInterface::_onPluginWindowClose(std::shared_ptr plugin) { + // Close/delete the window + for (int index {0}; index < _guestPluginWindows.size(); index++) { + if (_guestPluginWindows[index]->plugin == plugin) { + _guestPluginWindows.erase(_guestPluginWindows.begin() + index); + break; + } + } +} + +bool PluginSelectionInterface::isPluginSlot(int chainNumber, int slotNumber) { + // TODO implement a more reliable way of checking this + std::shared_ptr plugin = + ModelInterface::getPlugin(_processor.manager, chainNumber, slotNumber); + + return plugin != nullptr; +} + +void PluginSelectionInterface::setGainStageGain(int chainNumber, int slotNumber, float gain) { + _processor.setSlotGainLinear(chainNumber, slotNumber, gain); +} + +std::tuple PluginSelectionInterface::getGainStageGainAndPan(int chainNumber, int slotNumber) { + return ModelInterface::getGainLinearAndPan(_processor.manager, chainNumber, slotNumber); +} + +void PluginSelectionInterface::setGainStagePan(int chainNumber, int slotNumber, float pan) { + _processor.setSlotPan(chainNumber, slotNumber, pan); +} + +int PluginSelectionInterface::getNumMainChannels() const { + return _processor.getBusesLayout().getMainInputChannels(); +} + +float PluginSelectionInterface::getGainStageOutputAmplitude(int chainNumber, int slotNumber, int channelNumber) const { + return ModelInterface::getGainStageOutputAmplitude(_processor.manager, chainNumber, slotNumber, channelNumber); +} + +void PluginSelectionInterface::closeGuestPluginWindows() { + _guestPluginWindows.clear(); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSelectionInterface.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSelectionInterface.h new file mode 100644 index 00000000..ba90eeff --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSelectionInterface.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include "PluginSelectorWindow.h" +#include "GuestPluginWindow.h" +#include "PluginProcessor.h" + +/** + * The interface between the processor and parts of the UI that control plugin selection. + */ +class PluginSelectionInterface { +public: + PluginSelectionInterface(SyndicateAudioProcessor& processor); + ~PluginSelectionInterface() = default; + + void selectNewPlugin(int chainNumber, int pluginNumber); + juce::String getPluginName(int chainNumber, int pluginNumber); + void openPluginEditor(int chainNumber, int pluginNumber); + void removePlugin(int chainNumber, int pluginNumber); + void togglePluginBypass(int chainNumber, int pluginNumber); + bool getPluginBypass(int chainNumber, int pluginNumber); + void insertGainStage(int chainNumber, int pluginNumber); + void copySlot(int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber); + void moveSlot(int fromChainNumber, int fromSlotNumber, int toChainNumber, int toSlotNumber); + + /** + * Returns true if it's a plugin in this slot, otherwise false. + */ + bool isPluginSlot(int chainNumber, int slotNumber); + + void setGainStageGain(int chainNumber, int slotNumber, float gain); + std::tuple getGainStageGainAndPan(int chainNumber, int slotNumber); + void setGainStagePan(int chainNumber, int slotNumber, float pan); + int getNumMainChannels() const; + float getGainStageOutputAmplitude(int chainNumber, int slotNumber, int channelNumber) const; + + void closeGuestPluginWindows(); + +private: + SyndicateAudioProcessor& _processor; + std::unique_ptr _pluginSelectorWindow; + std::vector> _guestPluginWindows; + std::unique_ptr _errorPopover; + + int _pluginNumber; + int _chainNumber; + + void _onPluginSelected(std::unique_ptr plugin, const juce::String& error, bool shouldClose); + void _onPluginWindowClose(std::shared_ptr plugin); +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotComponent.cpp new file mode 100644 index 00000000..58599131 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotComponent.cpp @@ -0,0 +1,174 @@ +#include "PluginSlotComponent.h" +#include "UIUtils.h" + +namespace { + constexpr int CIRCLE_MARGIN {5}; + constexpr int CIRCLE_DIAMETER {UIUtils::PLUGIN_SLOT_HEIGHT - CIRCLE_MARGIN * 2}; +} + +PluginSlotComponent::PluginSlotComponent(PluginSelectionInterface& pluginSelectionInterface, + PluginModulationInterface& pluginModulationInterface, + int chainNumber, + int pluginNumber) : + BaseSlotComponent(chainNumber, pluginNumber), + _pluginSelectionInterface(pluginSelectionInterface), + _pluginModulationInterface(pluginModulationInterface), + _isHover(false) { + + _bypassButton.reset(new UIUtils::BypassButton("Bypass Button")); + addAndMakeVisible(_bypassButton.get()); + _bypassButton->setTooltip(TRANS("Enable/disable this plugin slot")); + _bypassButton->setColour(juce::TextButton::buttonOnColourId, UIUtils::highlightColour); + _bypassButton->setColour(juce::TextButton::textColourOnId, UIUtils::slotBackgroundColour); + _bypassButton->setColour(juce::TextButton::textColourOffId, UIUtils::highlightColour); + _bypassButton->addMouseListener(this, false); + _bypassButton->addListener(this); + + _openButton.reset(new UIUtils::PluginOpenButton("Plugin Open Button")); + addAndMakeVisible(_openButton.get()); + _openButton->setTooltip(TRANS("Open the editor window for this plugin slot")); + _openButton->setColour(juce::TextButton::buttonOnColourId, UIUtils::highlightColour); + _openButton->addMouseListener(this, false); + _openButton->addListener(this); + + _replaceButton.reset(new UIUtils::PluginReplaceButton("Plugin Replace Button")); + addAndMakeVisible(_replaceButton.get()); + _replaceButton->setTooltip(TRANS("Replace the plugin in this slot")); + _replaceButton->setColour(juce::TextButton::buttonOnColourId, UIUtils::highlightColour); + _replaceButton->addMouseListener(this, false); + _replaceButton->addListener(this); + + _deleteButton.reset(new UIUtils::CrossButton("Plugin delete Button")); + addAndMakeVisible(_deleteButton.get()); + _deleteButton->setTooltip(TRANS("Remove this plugin slot")); + _deleteButton->setColour(UIUtils::CrossButton::enabledColour, UIUtils::highlightColour); + _deleteButton->setColour(UIUtils::CrossButton::disabledColour, UIUtils::deactivatedColour); + _deleteButton->addMouseListener(this, false); + _deleteButton->addListener(this); + + _modulationButton.reset(new UIUtils::ModulationButton("Modulation Button")); + addAndMakeVisible(_modulationButton.get()); + _modulationButton->setTooltip(TRANS("Open the modulation tray for this plugin slot")); + _modulationButton->setColour(juce::TextButton::buttonOnColourId, UIUtils::PLUGIN_SLOT_MODULATION_ON_COLOUR); + _modulationButton->setColour(juce::TextButton::textColourOnId, UIUtils::slotBackgroundColour); + _modulationButton->setColour(juce::TextButton::textColourOffId, UIUtils::PLUGIN_SLOT_MODULATION_ON_COLOUR); + _modulationButton->addMouseListener(this, false); + _modulationButton->addListener(this); + + _descriptionLabel.reset(new juce::Label("Description Label", _pluginSelectionInterface.getPluginName(chainNumber, pluginNumber))); + addAndMakeVisible(_descriptionLabel.get()); + _descriptionLabel->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle("Regular")); + _descriptionLabel->setJustificationType(juce::Justification::centred); + _descriptionLabel->setEditable(false, false, false); + _descriptionLabel->setColour(juce::Label::textColourId, UIUtils::highlightColour); + _descriptionLabel->addMouseListener(this, false); + + PluginModulationConfig modulationConfig = + _pluginModulationInterface.getPluginModulationConfig(_chainNumber, _slotNumber); + + if (modulationConfig.isActive) { + _modulationTray.reset(new PluginSlotModulationTray(_pluginModulationInterface, _chainNumber, _slotNumber)); + addAndMakeVisible(_modulationTray.get()); + } + + // Set UI state + _bypassButton->setToggleState(!_pluginSelectionInterface.getPluginBypass(_chainNumber, _slotNumber), juce::dontSendNotification); + _modulationButton->setToggleState(modulationConfig.isActive, juce::dontSendNotification); + + _openButton->setVisible(false); + _replaceButton->setVisible(false); + _deleteButton->setVisible(false); + _descriptionLabel->setVisible(true); +} + +PluginSlotComponent::~PluginSlotComponent() { + _bypassButton = nullptr; + _openButton = nullptr; + _replaceButton = nullptr; + _deleteButton = nullptr; + _modulationButton = nullptr; + _modulationTray = nullptr; +} + +void PluginSlotComponent::resized() { + BaseSlotComponent::resized(); + + if (_modulationTray != nullptr) { + _modulationTray->setBounds(0, UIUtils::PLUGIN_SLOT_HEIGHT, getWidth(), UIUtils::PLUGIN_SLOT_MOD_TRAY_HEIGHT); + } + + // Figure out the areas for each button + juce::Rectangle availableArea = getAvailableSlotArea(); + availableArea.removeFromLeft(CIRCLE_MARGIN); + _bypassButton->setBounds(availableArea.removeFromLeft(CIRCLE_DIAMETER).withTrimmedTop(CIRCLE_MARGIN).withTrimmedBottom(CIRCLE_MARGIN)); + + availableArea.removeFromLeft(CIRCLE_MARGIN); + availableArea.removeFromRight(CIRCLE_MARGIN); + + _modulationButton->setBounds(availableArea.removeFromRight(CIRCLE_DIAMETER).withTrimmedTop(CIRCLE_MARGIN).withTrimmedBottom(CIRCLE_MARGIN)); + availableArea.removeFromRight(CIRCLE_MARGIN); + + _descriptionLabel->setBounds(availableArea); + + _openButton->setBounds(availableArea.removeFromLeft(availableArea.getWidth() / 3)); + _replaceButton->setBounds(availableArea.removeFromLeft(availableArea.getWidth() / 2)); + _deleteButton->setBounds(availableArea); +} + +void PluginSlotComponent::mouseMove(const juce::MouseEvent& event) { + + const juce::Point mousePoint {event.getEventRelativeTo(this).position.toInt()}; + + // Show the buttons if the mouse is over anywhere other than the drag handle + if (getAvailableSlotArea().contains(mousePoint)) { + _openButton->setVisible(true); + _replaceButton->setVisible(true); + _deleteButton->setVisible(true); + _descriptionLabel->setVisible(false); + + _isHover = true; + } else if (_isHover) { + _openButton->setVisible(false); + _replaceButton->setVisible(false); + _deleteButton->setVisible(false); + _descriptionLabel->setVisible(true); + + _isHover = false; + } +} + +void PluginSlotComponent::mouseExit(const juce::MouseEvent& /*event*/) { + _openButton->setVisible(false); + _replaceButton->setVisible(false); + _deleteButton->setVisible(false); + _descriptionLabel->setVisible(true); + + _isHover = false; +} + +void PluginSlotComponent::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == _bypassButton.get()) { + _pluginSelectionInterface.togglePluginBypass(_chainNumber, _slotNumber); + _bypassButton->setToggleState(!_pluginSelectionInterface.getPluginBypass(_chainNumber, _slotNumber), juce::dontSendNotification); + } else if (buttonThatWasClicked == _openButton.get()) { + _pluginSelectionInterface.openPluginEditor(_chainNumber, _slotNumber); + } else if (buttonThatWasClicked == _replaceButton.get()) { + _pluginSelectionInterface.selectNewPlugin(_chainNumber, _slotNumber); + } else if (buttonThatWasClicked == _deleteButton.get()) { + _pluginSelectionInterface.removePlugin(_chainNumber, _slotNumber); + } else if (buttonThatWasClicked == _modulationButton.get()) { + // Don't update the button - togglePluginModulationActive() will cause the graph to redraw + _pluginModulationInterface.togglePluginModulationActive(_chainNumber, _slotNumber); + } +} + +void PluginSlotComponent::paint(juce::Graphics& g) { + // Fill the space between the plugin slot and the modulation tray + if (_modulationTray != nullptr) { + g.setColour(UIUtils::modulationTrayBackgroundColour); + g.fillRect(MARGIN, UIUtils::PLUGIN_SLOT_HEIGHT / 2, getWidth() - MARGIN * 2, UIUtils::PLUGIN_SLOT_HEIGHT / 2); + } + + // Draw the slot background + BaseSlotComponent::paint(g); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotComponent.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotComponent.h new file mode 100644 index 00000000..a6549dd8 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotComponent.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include "BaseSlotComponent.h" +#include "PluginSelectionInterface.h" +#include "PluginSlotModulationTray.h" + +class PluginSlotComponent : public BaseSlotComponent, + juce::Button::Listener { +public: + PluginSlotComponent(PluginSelectionInterface& pluginSelectionInterface, + PluginModulationInterface& pluginModulationInterface, + int chainNumber, + int pluginNumber); + ~PluginSlotComponent() override; + + void resized() override; + void mouseMove(const juce::MouseEvent& event) override; + void mouseExit(const juce::MouseEvent& event) override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + void paint(juce::Graphics& g) override; + +private: + PluginSelectionInterface& _pluginSelectionInterface; + PluginModulationInterface& _pluginModulationInterface; + bool _isHover; + std::unique_ptr _modulationTray; + + std::unique_ptr _bypassButton; + std::unique_ptr _openButton; + std::unique_ptr _replaceButton; + std::unique_ptr _deleteButton; + std::unique_ptr _modulationButton; + std::unique_ptr _descriptionLabel; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotModulationTray.cpp b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotModulationTray.cpp new file mode 100644 index 00000000..41705b32 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotModulationTray.cpp @@ -0,0 +1,72 @@ +#include "PluginSlotModulationTray.h" +#include "UIUtils.h" + +PluginSlotModulationTray::PluginSlotModulationTray(PluginModulationInterface& pluginModulationInterface, + int chainNumber, + int pluginNumber) { + auto addModulationTarget = [&](int targetNumber) { + std::unique_ptr newTarget = + std::make_unique(pluginModulationInterface, chainNumber, pluginNumber, targetNumber); + + _targetsView->getViewedComponent()->addAndMakeVisible(newTarget.get()); + _modulationTargets.push_back(std::move(newTarget)); + }; + + _targetsView.reset(new juce::Viewport()); + _targetsView->setViewedComponent(new juce::Component()); + _targetsView->setScrollBarsShown(false, true); + _targetsView->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + _targetsView->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, UIUtils::neutralColour.withAlpha(0.5f)); + _targetsView->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + addAndMakeVisible(_targetsView.get()); + + // Restore existing targets + const PluginModulationConfig& modulationConfig = + pluginModulationInterface.getPluginModulationConfig(chainNumber, pluginNumber); + + for (int index {0}; index < modulationConfig.parameterConfigs.size(); index++) { + addModulationTarget(index); + } + + // Add an empty target + const size_t emptyTargetNumber {modulationConfig.parameterConfigs.size()}; + addModulationTarget(emptyTargetNumber); +} + +void PluginSlotModulationTray::resized() { + juce::Rectangle viewableArea = getLocalBounds().reduced(5); + + _targetsView->setBounds(viewableArea); + + const int targetWidth {static_cast(viewableArea.getHeight() * 0.75)}; + + juce::Rectangle scrollableArea(std::max(targetWidth * static_cast(_modulationTargets.size()), viewableArea.getWidth()), + viewableArea.getHeight()); + _targetsView->getViewedComponent()->setBounds(scrollableArea); + scrollableArea.removeFromBottom(_targetsView->getScrollBarThickness()); + + juce::FlexBox flexBox; + flexBox.flexWrap = juce::FlexBox::Wrap::wrap; + flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + flexBox.alignContent = juce::FlexBox::AlignContent::center; + + for (std::unique_ptr& target : _modulationTargets) { + flexBox.items.add(juce::FlexItem(*target.get()).withMinWidth(targetWidth).withMinHeight(scrollableArea.getHeight())); + } + + flexBox.performLayout(scrollableArea.toFloat()); +} + +void PluginSlotModulationTray::paint(juce::Graphics& g) { + + const juce::Rectangle fillArea = getLocalBounds().reduced(1, 1).toFloat(); + + g.setColour(UIUtils::modulationTrayBackgroundColour); + g.fillRoundedRectangle(fillArea, UIUtils::PLUGIN_SLOT_CORNER_RADIUS); + g.fillRect( + static_cast(fillArea.getX()), + 0, + static_cast(fillArea.getWidth()), + UIUtils::PLUGIN_SLOT_CORNER_RADIUS + ); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotModulationTray.h b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotModulationTray.h new file mode 100644 index 00000000..044bd7ec --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/PluginGraph/PluginSlotModulationTray.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "PluginModulationTarget.h" +#include "PluginModulationInterface.h" + +class PluginSlotModulationTray : public juce::Component { +public: + PluginSlotModulationTray(PluginModulationInterface& pluginModulationInterface, + int chainNumber, + int pluginNumber); + + ~PluginSlotModulationTray() = default; + + void resized() override; + void paint(juce::Graphics& g) override; + +private: + std::vector> _modulationTargets; + std::unique_ptr _targetsView; +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/SplitterButtonsComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/SplitterButtonsComponent.cpp new file mode 100644 index 00000000..0367b1db --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/SplitterButtonsComponent.cpp @@ -0,0 +1,144 @@ +#include "ParameterData.h" + +#include "SplitterButtonsComponent.h" +#include "PluginConfigurator.hpp" + +SplitterButtonsComponent::SplitterButtonsComponent(SyndicateAudioProcessor& processor) + : _processor(processor) { + _buttonLookAndFeel.reset(new UIUtils::ToggleButtonLookAndFeel()); + + seriesBtn.reset(new juce::TextButton("Series Button")); + addAndMakeVisible(seriesBtn.get()); + seriesBtn->setButtonText(TRANS("Series")); + seriesBtn->addListener(this); + seriesBtn->setLookAndFeel(_buttonLookAndFeel.get()); + seriesBtn->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + seriesBtn->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + seriesBtn->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + seriesBtn->setTooltip("A single chain of plugins in series"); + + parallelBtn.reset(new juce::TextButton("Parallel Button")); + addAndMakeVisible(parallelBtn.get()); + parallelBtn->setButtonText(TRANS("Parallel")); + parallelBtn->addListener(this); + parallelBtn->setLookAndFeel(_buttonLookAndFeel.get()); + parallelBtn->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + parallelBtn->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + parallelBtn->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + parallelBtn->setTooltip("Multiple chains of plugins in parallel"); + + multibandBtn.reset(new juce::TextButton("Multiband Button")); + addAndMakeVisible(multibandBtn.get()); + multibandBtn->setButtonText(TRANS("Multiband")); + multibandBtn->addListener(this); + multibandBtn->setLookAndFeel(_buttonLookAndFeel.get()); + multibandBtn->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + multibandBtn->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + multibandBtn->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + multibandBtn->setTooltip("Multiple chains of plugins processing different frequency bands"); + + leftrightBtn.reset(new juce::TextButton("Left/Right Button")); + addAndMakeVisible(leftrightBtn.get()); + leftrightBtn->setButtonText(TRANS("Left/Right")); + leftrightBtn->addListener(this); + leftrightBtn->setLookAndFeel(_buttonLookAndFeel.get()); + leftrightBtn->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + leftrightBtn->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + leftrightBtn->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + leftrightBtn->setTooltip("Two chains of plugins to process the left and right channels separately"); + + midsideBtn.reset(new juce::TextButton("Mid/Side Button")); + addAndMakeVisible(midsideBtn.get()); + midsideBtn->setButtonText(TRANS("Mid/Side")); + midsideBtn->addListener(this); + midsideBtn->setLookAndFeel(_buttonLookAndFeel.get()); + midsideBtn->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + midsideBtn->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + midsideBtn->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + midsideBtn->setTooltip("Two chains of plugins to process the mid and side channels separately"); + + leftrightBtn->setEnabled(canDoStereoSplitTypes(_processor.getBusesLayout())); + midsideBtn->setEnabled(canDoStereoSplitTypes(_processor.getBusesLayout())); +} + +SplitterButtonsComponent::~SplitterButtonsComponent() { + seriesBtn->setLookAndFeel(nullptr); + parallelBtn->setLookAndFeel(nullptr); + multibandBtn->setLookAndFeel(nullptr); + leftrightBtn->setLookAndFeel(nullptr); + midsideBtn->setLookAndFeel(nullptr); + + seriesBtn = nullptr; + parallelBtn = nullptr; + multibandBtn = nullptr; + leftrightBtn = nullptr; + midsideBtn = nullptr; +} + +void SplitterButtonsComponent::resized() { + juce::Rectangle availableArea = getLocalBounds(); + const int margin {availableArea.getHeight() / 5}; + availableArea.removeFromTop(margin); + availableArea.removeFromBottom(margin); + availableArea.removeFromLeft(margin); + availableArea.removeFromRight(margin); + + juce::FlexBox flexBox; + flexBox.flexWrap = juce::FlexBox::Wrap::wrap; + flexBox.justifyContent = juce::FlexBox::JustifyContent::spaceAround; + flexBox.alignContent = juce::FlexBox::AlignContent::center; + flexBox.items.add(juce::FlexItem(*seriesBtn.get()).withMinWidth(88).withMinHeight(availableArea.getHeight())); + flexBox.items.add(juce::FlexItem(*parallelBtn.get()).withMinWidth(88).withMinHeight(availableArea.getHeight())); + flexBox.items.add(juce::FlexItem(*multibandBtn.get()).withMinWidth(88).withMinHeight(availableArea.getHeight())); + flexBox.items.add(juce::FlexItem(*leftrightBtn.get()).withMinWidth(88).withMinHeight(availableArea.getHeight())); + flexBox.items.add(juce::FlexItem(*midsideBtn.get()).withMinWidth(88).withMinHeight(availableArea.getHeight())); + flexBox.performLayout(availableArea.toFloat()); +} + +void SplitterButtonsComponent::buttonClicked(juce::Button* buttonThatWasClicked) { + if (buttonThatWasClicked == seriesBtn.get()) { + _processor.setSplitType(SPLIT_TYPE::SERIES); + } else if (buttonThatWasClicked == parallelBtn.get()) { + _processor.setSplitType(SPLIT_TYPE::PARALLEL); + } else if (buttonThatWasClicked == multibandBtn.get()) { + _processor.setSplitType(SPLIT_TYPE::MULTIBAND); + } else if (buttonThatWasClicked == leftrightBtn.get()) { + _processor.setSplitType(SPLIT_TYPE::LEFTRIGHT); + } else if (buttonThatWasClicked == midsideBtn.get()) { + _processor.setSplitType(SPLIT_TYPE::MIDSIDE); + } +} + +void SplitterButtonsComponent::onParameterUpdate() { + if (_processor.getSplitType() == SPLIT_TYPE::SERIES) { + seriesBtn->setToggleState(true, juce::NotificationType::dontSendNotification); + parallelBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + multibandBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + leftrightBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + midsideBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + } else if (_processor.getSplitType() == SPLIT_TYPE::PARALLEL) { + seriesBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + parallelBtn->setToggleState(true, juce::NotificationType::dontSendNotification); + multibandBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + leftrightBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + midsideBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + } else if (_processor.getSplitType() == SPLIT_TYPE::MULTIBAND) { + seriesBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + parallelBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + multibandBtn->setToggleState(true, juce::NotificationType::dontSendNotification); + leftrightBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + midsideBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + } else if (_processor.getSplitType() == SPLIT_TYPE::LEFTRIGHT) { + seriesBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + parallelBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + multibandBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + leftrightBtn->setToggleState(true, juce::NotificationType::dontSendNotification); + midsideBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + } else if (_processor.getSplitType() == SPLIT_TYPE::MIDSIDE) { + seriesBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + parallelBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + multibandBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + leftrightBtn->setToggleState(false, juce::NotificationType::dontSendNotification); + midsideBtn->setToggleState(true, juce::NotificationType::dontSendNotification); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/SplitterButtonsComponent.h b/ports-juce7/syndicate/Syndicate/UI/SplitterButtonsComponent.h new file mode 100644 index 00000000..39d6f093 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/SplitterButtonsComponent.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "PluginProcessor.h" +#include "UIUtils.h" + +class SplitterButtonsComponent : public juce::Component, public juce::Button::Listener { +public: + SplitterButtonsComponent(SyndicateAudioProcessor& processor); + ~SplitterButtonsComponent() override; + + void resized() override; + void buttonClicked(juce::Button* buttonThatWasClicked) override; + + void onParameterUpdate(); + +private: + SyndicateAudioProcessor& _processor; + std::unique_ptr _buttonLookAndFeel; + + std::unique_ptr seriesBtn; + std::unique_ptr parallelBtn; + std::unique_ptr multibandBtn; + std::unique_ptr leftrightBtn; + std::unique_ptr midsideBtn; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SplitterButtonsComponent) +}; diff --git a/ports-juce7/syndicate/Syndicate/UI/UIUtils.cpp b/ports-juce7/syndicate/Syndicate/UI/UIUtils.cpp new file mode 100644 index 00000000..3dcd3e55 --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/UIUtils.cpp @@ -0,0 +1,761 @@ +#include "UIUtils.h" + +namespace UIUtils { + + int getChainXPos(int chainIndex, int numChains, int graphViewWidth) { + const int graphMidpoint {graphViewWidth / 2}; + const double offsetCoefficent {-0.5 * numChains + 1 * chainIndex}; + return static_cast(graphMidpoint + CHAIN_WIDTH * offsetCoefficent); + } + + void ToggleButtonLookAndFeel::drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool isMouseOverButton, + bool isButtonDown) { + const int width {button.getWidth()}; + const int height {button.getHeight()}; + + constexpr float indent {2.0f}; + const int cornerSize {juce::jmin(juce::roundToInt(width * 0.4f), + juce::roundToInt(height * 0.4f))}; + + if (button.getToggleState()) { + g.setColour(button.findColour(UIUtils::ToggleButtonLookAndFeel::highlightColour)); + } else { + g.setColour(button.findColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour)); + } + + g.fillRoundedRectangle( + indent, + indent, + width - 2 * indent, + height - 2 * indent, + static_cast(cornerSize)); + + if (button.isConnectedOnLeft()) { + g.fillRect(0.0f, indent, cornerSize + indent, height - 2 * indent); + } + + if (button.isConnectedOnRight()) { + g.fillRect(width - cornerSize - indent, indent, cornerSize + indent, height - 2 * indent); + } + } + + void ToggleButtonLookAndFeel::drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool isMouseOverButton, + bool isButtonDown) { + constexpr int MARGIN {0}; + + juce::Font font; + font.setTypefaceName(WECore::JUCEPlugin::CoreLookAndFeel::getTypefaceForFont(font)->getName()); + g.setFont(font); + + if (!textButton.isEnabled()) { + g.setColour(textButton.findColour(UIUtils::ToggleButtonLookAndFeel::disabledColour)); + } else if (textButton.getToggleState()) { + g.setColour(textButton.findColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour)); + } else { + g.setColour(textButton.findColour(UIUtils::ToggleButtonLookAndFeel::highlightColour)); + } + + g.drawFittedText(textButton.getButtonText(), + MARGIN, + 0, + textButton.getWidth() - 2 * MARGIN, + textButton.getHeight(), + juce::Justification::centred, + 0); + } + + + void StaticButtonLookAndFeel::drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool isMouseOverButton, + bool isButtonDown) { + const int width {button.getWidth()}; + const int height {button.getHeight()}; + + constexpr float indent {2.0f}; + const int cornerSize {_getCornerSize(width, height)}; + + g.setColour(button.findColour(UIUtils::StaticButtonLookAndFeel::backgroundColour)); + g.fillRoundedRectangle( + indent, + indent, + width - 2 * indent, + height - 2 * indent, + static_cast(cornerSize)); + } + + void StaticButtonLookAndFeel::drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool isMouseOverButton, + bool isButtonDown) { + + const int cornerSize {_getCornerSize(textButton.getWidth(), textButton.getHeight())}; + + juce::Font font; + font.setTypefaceName(WECore::JUCEPlugin::CoreLookAndFeel::getTypefaceForFont(font)->getName()); + g.setFont(font); + + if (textButton.isEnabled()) { + g.setColour(textButton.findColour(UIUtils::StaticButtonLookAndFeel::highlightColour)); + } else { + g.setColour(textButton.findColour(UIUtils::StaticButtonLookAndFeel::disabledColour)); + } + + g.drawFittedText(textButton.getButtonText(), + cornerSize, + 0, + textButton.getWidth() - 2 * cornerSize, + textButton.getHeight(), + juce::Justification::centred, + 0); + } + + int StaticButtonLookAndFeel::_getCornerSize(int width, int height) const { + return juce::jmin(juce::roundToInt(width * 0.4f), juce::roundToInt(height * 0.4f)); + } + + void AddButtonLookAndFeel::drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool isMouseOverButton, + bool isButtonDown) { + juce::Font font; + font.setTypefaceName(WECore::JUCEPlugin::CoreLookAndFeel::getTypefaceForFont(font)->getName()); + g.setFont(font); + + if (textButton.isEnabled()) { + g.setColour(textButton.findColour(UIUtils::StaticButtonLookAndFeel::highlightColour)); + } else { + g.setColour(textButton.findColour(UIUtils::StaticButtonLookAndFeel::disabledColour)); + } + + constexpr int lineSpacing {4}; + const float fontHeight {font.getHeight()}; + + const int firstLineY {static_cast(textButton.getHeight() / 2 - lineSpacing - fontHeight)}; + const int secondLineY {textButton.getHeight() / 2 + lineSpacing}; + + const int textX {_getCornerSize(textButton.getWidth(), textButton.getHeight())}; + const int textWidth {textButton.getWidth() - 2 * textX}; + + g.drawFittedText("+", + textX, + firstLineY, + textWidth, + fontHeight, + juce::Justification::centred, + 0); + + g.drawFittedText(textButton.getButtonText(), + textX, + secondLineY, + textWidth, + fontHeight, + juce::Justification::centred, + 0); + } + + int AddButtonLookAndFeel::_getCornerSize(int width, int height) const { + return juce::jmin(juce::roundToInt(width * 0.2f), juce::roundToInt(height * 0.2f)); + } + + void SearchBarLookAndFeel::drawTextEditorOutline(juce::Graphics& g, + int width, + int height, + juce::TextEditor& textEditor) { + const juce::Rectangle area = + juce::Rectangle(textEditor.getWidth(), textEditor.getHeight()).reduced(1, 1); + + g.setColour(textEditor.findColour(juce::TextEditor::outlineColourId)); + g.drawRoundedRectangle(area, 2, 1); + } + + + const juce::Colour& getColourForModulationType(MODULATION_TYPE type) { + static const juce::Colour macroColour(147, 252, 116); + static const juce::Colour lfoColour(252, 116, 147); + static const juce::Colour envelopeColour(116, 147, 252); + static const juce::Colour randomColour(247, 147, 252); + + switch (type) { + case MODULATION_TYPE::MACRO: + return macroColour; + case MODULATION_TYPE::LFO: + return lfoColour; + case MODULATION_TYPE::ENVELOPE: + return envelopeColour; + default: + return randomColour; + } + } + + void setDefaultLabelStyle(std::unique_ptr& label) { + label->setFont(juce::Font(15.00f, juce::Font::plain).withTypefaceStyle("Regular")); + label->setJustificationType(juce::Justification::centred); + label->setEditable(false, false, false); + label->setColour(juce::Label::textColourId, neutralColour); + label->setColour(juce::TextEditor::textColourId, juce::Colours::black); + label->setColour(juce::TextEditor::backgroundColourId, juce::Colour(0x00000000)); + } + + void StandardComboBoxLookAndFeel::drawComboBox(juce::Graphics& g, + int width, + int height, + bool isButtonDown, + int buttonX, + int buttonY, + int buttonW, + int buttonH, + juce::ComboBox& box) { + + // Draw background + g.setColour(slotBackgroundColour); + g.fillRoundedRectangle(0, 0, width, height, height / 2); + + // Draw arrows + // TODO using buttonX - 6 works for now, but it's not ideal at really small sizes + WECore::LookAndFeelMixins::ComboBoxV2::drawComboBox( + g, width, height, isButtonDown, buttonX - 10, buttonY, buttonW, buttonH, box); + } + + void StandardComboBoxLookAndFeel::positionComboBoxText(juce::ComboBox& box, juce::Label& label) { + label.setBounds(10, 1, box.getWidth() - 6 - box.getHeight(), box.getHeight() - 2); + label.setFont(getComboBoxFont(box)); + } + + void StandardComboBoxLookAndFeel::drawPopupMenuBackground(juce::Graphics& g, int width, int height) { + g.fillAll(findColour(juce::PopupMenu::backgroundColourId)); + } + + void StandardComboBoxLookAndFeel::drawPopupMenuItem(juce::Graphics& g, + const juce::Rectangle& area, + bool isSeparator, + bool isActive, + bool isHighlighted, + bool isTicked, + bool hasSubMenu, + const juce::String& text, + const juce::String& shortcutKeyText, + const juce::Drawable* /*icon*/, + const juce::Colour* /*textColour*/) { + + juce::Rectangle availableArea = area.reduced(1); + + juce::Rectangle leftMarginArea = availableArea.removeFromLeft(availableArea.getWidth() / 8); + + if (isHighlighted) { + g.setColour(findColour(juce::PopupMenu::highlightedBackgroundColourId)); + g.fillRect(availableArea); + g.fillRect(leftMarginArea); + g.setColour(findColour(juce::PopupMenu::highlightedTextColourId)); + } else { + if (isTicked) { + g.setColour(findColour(juce::PopupMenu::highlightedBackgroundColourId)); + g.fillRect(leftMarginArea.removeFromLeft(leftMarginArea.getWidth() / 2)); + } + + g.setColour(findColour(juce::PopupMenu::textColourId)); + } + + juce::Font font(getPopupMenuFont()); + + const float maxFontHeight = static_cast(area.getHeight() / 1.3f); + + if (font.getHeight() > maxFontHeight) { + font.setHeight(maxFontHeight); + } + + g.setFont(font); + g.drawFittedText(text, availableArea, juce::Justification::centredLeft, 1); + } + + void TableHeaderLookAndFeel::drawTableHeaderBackground(juce::Graphics& g, + juce::TableHeaderComponent& header) { + // Main fill + g.fillAll(header.findColour(juce::TableHeaderComponent::backgroundColourId)); + + // Bottom line + g.setColour(header.findColour(juce::TableHeaderComponent::outlineColourId)); + g.fillRect(header.getLocalBounds().removeFromBottom(1)); + } + + void TableHeaderLookAndFeel::drawTableHeaderColumn(juce::Graphics& g, + juce::TableHeaderComponent& header, + const juce::String& columnName, + int columnId, + int width, + int height, + bool isMouseOver, + bool isMouseDown, + int columnFlags) { + // Hover/click highlight + const juce::Colour highlightColour = header.findColour(juce::TableHeaderComponent::highlightColourId); + + if (isMouseOver) { + g.fillAll(highlightColour.withMultipliedAlpha(0.1f)); + } + + // Sort arrow + g.setColour(header.findColour(juce::TableHeaderComponent::textColourId)); + juce::Rectangle area(width, height); + area.reduce(4, 0); + + if ((columnFlags & (juce::TableHeaderComponent::sortedForwards | juce::TableHeaderComponent::sortedBackwards)) != 0) { + juce::Path sortArrow; + sortArrow.addTriangle(0.0f, + 0.0f, + 0.5f, + (columnFlags & juce::TableHeaderComponent::sortedForwards) != 0 ? -0.8f : 0.8f, + 1.0f, + 0.0f); + + g.fillPath(sortArrow, sortArrow.getTransformToScaleToFit(area.removeFromRight(height / 2).reduced(2).toFloat(), true)); + } + + // Header title + g.setFont(juce::Font(height * 0.5f, juce::Font::bold)); + g.drawFittedText(columnName, area, juce::Justification::centredLeft, 1); + } + + PopoverComponent::PopoverComponent(juce::String title, + juce::String content, + std::function onCloseCallback) : + _onCloseCallback(onCloseCallback) { + _titleLabel.reset(new juce::Label("Title Label", title)); + addAndMakeVisible(_titleLabel.get()); + _titleLabel->setFont(juce::Font(20.00f, juce::Font::plain).withTypefaceStyle("Bold")); + _titleLabel->setJustificationType(juce::Justification::centred); + _titleLabel->setEditable(false, false, false); + _titleLabel->setColour(juce::Label::textColourId, highlightColour); + + const juce::Font contentFont = juce::Font(15.0f, juce::Font::plain).withTypefaceStyle("Regular"); + _contentLabel.reset(new juce::Label("Content Label", content)); + addAndMakeVisible(_contentLabel.get()); + _contentLabel->setFont(contentFont); + _contentLabel->setJustificationType(juce::Justification::centred); + _contentLabel->setEditable(false, false, false); + _contentLabel->setColour(juce::Label::textColourId, highlightColour); + + _contentSize = _getBoundsForText(content, contentFont); + + _contentView.reset(new juce::Viewport()); + _contentView->setViewedComponent(_contentLabel.get(), false); + _contentView->setScrollBarsShown(true, true); + _contentView->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + _contentView->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, neutralColour.withAlpha(0.5f)); + _contentView->getVerticalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + _contentView->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::backgroundColourId, juce::Colour(0x00000000)); + _contentView->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::thumbColourId, neutralColour.withAlpha(0.5f)); + _contentView->getHorizontalScrollBar().setColour(juce::ScrollBar::ColourIds::trackColourId, juce::Colour(0x00000000)); + addAndMakeVisible(_contentView.get()); + + _button.reset(new juce::TextButton("OK button")); + addAndMakeVisible(_button.get()); + _button->setButtonText(TRANS("OK")); + _button->setLookAndFeel(&_buttonLookAndFeel); + _button->setColour(StaticButtonLookAndFeel::backgroundColour, slotBackgroundColour); + _button->setColour(StaticButtonLookAndFeel::highlightColour, highlightColour); + _button->setColour(StaticButtonLookAndFeel::disabledColour, deactivatedColour); + _button->onClick = [this] { _onCloseCallback(); }; + } + + void PopoverComponent::resized() { + juce::Rectangle availableArea = getLocalBounds().reduced(20); + + _titleLabel->setBounds(availableArea.removeFromTop(availableArea.getHeight() / 5)); + + juce::Rectangle buttonArea = availableArea.removeFromBottom(availableArea.getHeight() / 4); + _button->setBounds(buttonArea.withSizeKeepingCentre(60, 40)); + _contentView->setBounds(availableArea); + + const int scrollBarWidth {10}; + const int contentWidth {std::max(_contentSize.getWidth(), availableArea.getWidth() - scrollBarWidth)}; + _contentLabel->setBounds(_contentSize.withWidth(contentWidth)); + } + + void PopoverComponent::paint(juce::Graphics& g) { + g.fillAll(juce::Colours::black.withAlpha(0.8f)); + } + + juce::Rectangle PopoverComponent::_getBoundsForText(const juce::String& content, const juce::Font& font) const { + const juce::StringArray lines = juce::StringArray::fromLines(content); + + int maxWidth {0}; + for (const juce::String& line : lines) { + const int thisWidth {font.getStringWidth(line)}; + if (thisWidth > maxWidth) { + maxWidth = thisWidth; + } + } + + constexpr int MARGIN {5}; + return juce::Rectangle(maxWidth + MARGIN, lines.size() * font.getHeight()); + } + + SafeAnimatedComponent::SafeAnimatedComponent() : _stopEvent(true) { + } + + SafeAnimatedComponent::~SafeAnimatedComponent() { + stop(); + } + + void SafeAnimatedComponent::start() { + startTimerHz(20); + } + + void SafeAnimatedComponent::stop() { + stopTimer(); + _stopEvent.wait(1000); + } + + void SafeAnimatedComponent::timerCallback() { + _onTimerCallback(); + repaint(); + } + + BypassButton::BypassButton(const juce::String& buttonName) : juce::Button(buttonName) { } + + void BypassButton::paintButton(juce::Graphics& g, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + const juce::Rectangle area = getLocalBounds().reduced(1, 1).toFloat(); + + + juce::Colour buttonColour = highlightColour; + juce::Colour iconColour = modulationTrayBackgroundColour; + if (!getToggleState()) { + buttonColour = modulationTrayBackgroundColour; + iconColour = highlightColour; + } + + // Draw the background + g.setColour(buttonColour); + g.fillEllipse(area); + + // Draw the icon + g.setColour(iconColour); + g.drawLine(getWidth() * 0.5, getHeight() * 0.25, getWidth() * 0.5, getHeight() * 0.75, 1.0f); + } + + ModulationButton::ModulationButton(const juce::String& buttonName) : juce::Button(buttonName) { } + + void ModulationButton::paintButton(juce::Graphics& g, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + const juce::Rectangle area = getLocalBounds().reduced(1, 1).toFloat(); + + juce::Colour buttonColour = PLUGIN_SLOT_MODULATION_ON_COLOUR; + juce::Colour iconColour = modulationTrayBackgroundColour; + if (!getToggleState()) { + buttonColour = modulationTrayBackgroundColour; + iconColour = PLUGIN_SLOT_MODULATION_ON_COLOUR; + } + + // Draw the background + g.setColour(buttonColour); + g.fillEllipse(area); + + // Draw the icon + g.setColour(iconColour); + g.drawText("M", getLocalBounds().reduced(2), juce::Justification::centred, false); + } + + CrossButton::CrossButton(const juce::String& buttonName) : juce::Button(buttonName) { } + + void CrossButton::paintButton(juce::Graphics& g, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + juce::Rectangle area = reduceToSquare(getLocalBounds()).toFloat(); + if (area.getWidth() > 22) { + area = area.reduced(area.getWidth() / 3.5); + } else { + area = area.reduced(area.getWidth() / 4); + } + + if (isEnabled()) { + g.setColour(findColour(enabledColour)); + } else { + g.setColour(findColour(disabledColour)); + } + g.drawLine(juce::Line(area.getTopLeft(), area.getBottomRight())); + g.drawLine(juce::Line(area.getTopRight(), area.getBottomLeft())); + } + + PluginOpenButton::PluginOpenButton(const juce::String& buttonName) : juce::Button(buttonName) { } + + void PluginOpenButton::paintButton(juce::Graphics& g, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + juce::Rectangle area = reduceToSquare(getLocalBounds()).toFloat(); + area = area.reduced(area.getWidth() / 5, area.getWidth() / 4); + + const float cornerRadius {area.getWidth() / 5}; + const float lineThickness {1.0f}; + + // Draw the outer part + g.setColour(findColour(juce::TextButton::buttonOnColourId)); + g.drawRoundedRectangle(area, cornerRadius, lineThickness); + + // Draw the inner part + const float border {area.getWidth() / 10}; + area.removeFromTop(border * 2); + + g.drawRoundedRectangle(area, cornerRadius, lineThickness); + } + + PluginReplaceButton::PluginReplaceButton(const juce::String& buttonName) : juce::Button(buttonName) { } + + void PluginReplaceButton::paintButton(juce::Graphics& g, + bool /*shouldDrawButtonAsHighlighted*/, + bool /*shouldDrawButtonAsDown*/) { + juce::Rectangle area = reduceToSquare(getLocalBounds()).toFloat(); + area = area.reduced(area.getWidth() / 4); + + + const float cornerRadius {area.getWidth() / 4}; + const float horizontalGap {area.getWidth() / 2}; + const float arrowHeadLength {area.getWidth() / 3}; + + // Draw from top left going clockwise + juce::Path p; + p.startNewSubPath(area.getX() + horizontalGap, area.getY()); + p.lineTo(area.getRight() - cornerRadius, area.getY()); + p.addCentredArc(area.getRight() - cornerRadius, + area.getY() + cornerRadius, + cornerRadius, + cornerRadius, + 0, + 0, + WECore::CoreMath::DOUBLE_PI / 2); + p.lineTo(area.getBottomRight()); + p.lineTo(area.getRight() - arrowHeadLength, area.getBottom() - arrowHeadLength); + p.startNewSubPath(area.getBottomRight()); + p.lineTo(area.getRight() + arrowHeadLength, area.getBottom() - arrowHeadLength); + + // Second arrow + p.startNewSubPath(area.getRight() - horizontalGap, area.getBottom()); + p.lineTo(area.getX() + cornerRadius, area.getBottom()); + p.addCentredArc(area.getX() + cornerRadius, + area.getBottom() - cornerRadius, + cornerRadius, + cornerRadius, + WECore::CoreMath::DOUBLE_PI, + 0, + WECore::CoreMath::DOUBLE_PI / 2); + p.lineTo(area.getTopLeft()); + p.lineTo(area.getX() + arrowHeadLength, area.getY() + arrowHeadLength); + p.startNewSubPath(area.getTopLeft()); + p.lineTo(area.getX() - arrowHeadLength, area.getY() + arrowHeadLength); + + g.setColour(findColour(juce::TextButton::buttonOnColourId)); + g.strokePath(p, juce::PathStrokeType(1)); + } + + void DragHandle::paint(juce::Graphics& g) { + juce::Rectangle area = reduceToSquare(getLocalBounds()); + if (area.getWidth() > 22) { + area = area.reduced(area.getWidth() / 5); + } else { + area = area.reduced(area.getWidth() / 10); + } + + const int arrowHeadLength {area.getWidth() / 5}; + + juce::Path p; + + // Left arrow head + p.startNewSubPath(area.getX(), area.getCentreY()); + p.lineTo(area.getX() + arrowHeadLength, area.getCentreY() - arrowHeadLength); + p.startNewSubPath(area.getX(), area.getCentreY()); + p.lineTo(area.getX() + arrowHeadLength, area.getCentreY() + arrowHeadLength); + + // Arrow going right + p.startNewSubPath(area.getX(), area.getCentreY()); + p.lineTo(area.getRight(), area.getCentreY()); + p.lineTo(area.getRight() - arrowHeadLength, area.getCentreY() - arrowHeadLength); + p.startNewSubPath(area.getRight(), area.getCentreY()); + p.lineTo(area.getRight() - arrowHeadLength, area.getCentreY() + arrowHeadLength); + + // Bottom arrow head + p.startNewSubPath(area.getCentreX(), area.getBottom()); + p.lineTo(area.getCentreX() - arrowHeadLength, area.getBottom() - arrowHeadLength); + p.startNewSubPath(area.getCentreX(), area.getBottom()); + p.lineTo(area.getCentreX() + arrowHeadLength, area.getBottom() - arrowHeadLength); + + // Arrow going up + p.startNewSubPath(area.getCentreX(), area.getBottom()); + p.lineTo(area.getCentreX(), area.getY()); + p.lineTo(area.getCentreX() - arrowHeadLength, area.getY() + arrowHeadLength); + p.startNewSubPath(area.getCentreX(), area.getY()); + p.lineTo(area.getCentreX() + arrowHeadLength, area.getY() + arrowHeadLength); + + g.setColour(findColour(ColourIds::handleColourId)); + g.strokePath(p, juce::PathStrokeType(1)); + } + + LinkedScrollView::LinkedScrollView() : _otherView(nullptr) { + } + + void LinkedScrollView::setOtherView(juce::Viewport* otherView) { + _otherView = otherView; + } + + void LinkedScrollView::removeOtherView(juce::Viewport* otherView) { + if (_otherView == otherView) { + _otherView = nullptr; + } + } + + void LinkedScrollView::scrollBarMoved(juce::ScrollBar* scrollBar, double newRangeStart) { + juce::Viewport::scrollBarMoved(scrollBar, newRangeStart); + + const int newRangeStartInt {juce::roundToInt(newRangeStart)}; + if (_otherView != nullptr) { + // Only linked horizontally + _otherView->setViewPosition(newRangeStartInt, 0); + } + } + + WaveStylusViewer::WaveStylusViewer(std::function getNextValueCallback) : + _getNextValueCallback(getNextValueCallback) { + std::fill(_envelopeValues.begin(), _envelopeValues.end(), 0); + + start(); + } + + void WaveStylusViewer::paint(juce::Graphics& g) { + const int XIncrement {static_cast(getWidth() / _envelopeValues.size())}; + + juce::Path p; + + const int height {getHeight()}; + auto envelopeValueToYPos = [&height](float value) { + return height / 2 - value * height / 2; + }; + + p.startNewSubPath(0, envelopeValueToYPos(_envelopeValues[0])); + + for (int index {1}; index < _envelopeValues.size(); index++) { + p.lineTo(index * XIncrement, envelopeValueToYPos(_envelopeValues[index])); + } + + g.fillAll(UIUtils::backgroundColour); + g.setColour(findColour(ColourIds::lineColourId)); + const juce::PathStrokeType pStroke(1); + g.strokePath(p, pStroke); + + constexpr int STYLUS_DIAMETER {4}; + g.fillEllipse(0, envelopeValueToYPos(_envelopeValues[0]) - STYLUS_DIAMETER / 2, STYLUS_DIAMETER, STYLUS_DIAMETER); + } + + void WaveStylusViewer::_onTimerCallback() { + _stopEvent.reset(); + + std::rotate(_envelopeValues.rbegin(), _envelopeValues.rbegin() + 1, _envelopeValues.rend()); + _envelopeValues[0] = _getNextValueCallback(); + + _stopEvent.signal(); + } + + UniBiModeButtons::UniBiModeButtons(std::function onUniClick, + std::function onBiClick, + std::function getUniState, + std::function getBiState, + juce::Colour buttonColour) : + _onUniClick(onUniClick), + _onBiClick(onBiClick) { + _unipolarButton.reset(new juce::TextButton("Unipolar Button")); + addAndMakeVisible(_unipolarButton.get()); + _unipolarButton->setTooltip(TRANS("Set the output mode to unipolar")); + _unipolarButton->setButtonText(TRANS("Uni")); + _unipolarButton->setLookAndFeel(&_buttonLookAndFeel); + _unipolarButton->setColour(juce::TextButton::buttonOnColourId, buttonColour); + _unipolarButton->setColour(juce::TextButton::textColourOnId, UIUtils::backgroundColour); + _unipolarButton->setColour(juce::TextButton::textColourOffId, buttonColour); + _unipolarButton->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + _unipolarButton->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, buttonColour); + _unipolarButton->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + _unipolarButton->onClick = [&]() { + if (!_unipolarButton->getToggleState()) { + _unipolarButton->setToggleState(true, juce::dontSendNotification); + _bipolarButton->setToggleState(false, juce::dontSendNotification); + _onUniClick(); + } + }; + _unipolarButton->setConnectedEdges(juce::Button::ConnectedOnRight); + + _bipolarButton.reset(new juce::TextButton("Bipolar Button")); + addAndMakeVisible(_bipolarButton.get()); + _bipolarButton->setTooltip(TRANS("Set the output mode to bipolar")); + _bipolarButton->setButtonText(TRANS("Bi")); + _bipolarButton->setLookAndFeel(&_buttonLookAndFeel); + _bipolarButton->setColour(juce::TextButton::buttonOnColourId, buttonColour); + _bipolarButton->setColour(juce::TextButton::textColourOnId, UIUtils::backgroundColour); + _bipolarButton->setColour(juce::TextButton::textColourOffId, buttonColour); + _bipolarButton->setColour(UIUtils::ToggleButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + _bipolarButton->setColour(UIUtils::ToggleButtonLookAndFeel::highlightColour, buttonColour); + _bipolarButton->setColour(UIUtils::ToggleButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + _bipolarButton->onClick = [&]() { + if (!_bipolarButton->getToggleState()) { + _bipolarButton->setToggleState(true, juce::dontSendNotification); + _unipolarButton->setToggleState(false, juce::dontSendNotification); + _onBiClick(); + } + }; + _bipolarButton->setConnectedEdges(juce::Button::ConnectedOnLeft); + + _unipolarButton->setToggleState(getUniState(), juce::dontSendNotification); + _bipolarButton->setToggleState(getBiState(), juce::dontSendNotification); + } + + UniBiModeButtons::~UniBiModeButtons() { + _unipolarButton->setLookAndFeel(nullptr); + _bipolarButton->setLookAndFeel(nullptr); + + _unipolarButton = nullptr; + _bipolarButton = nullptr; + } + + void UniBiModeButtons::resized() { + juce::Rectangle availableArea = getLocalBounds(); + + _unipolarButton->setBounds(availableArea.removeFromLeft(availableArea.getWidth() / 2)); + _bipolarButton->setBounds(availableArea); + } + + juce::String getCopyKeyName() { +#if _WIN32 + return "Alt"; +#elif __APPLE__ + return "Option"; +#elif __linux__ + return "Alt"; +#else + #error "Unknown OS" +#endif + } + + juce::String getCmdKeyName() { +#if _WIN32 + return "Ctrl"; +#elif __APPLE__ + return "Cmd"; +#elif __linux__ + return "Ctrl"; +#else + #error "Unknown OS" +#endif + } + + juce::String presetNameOrPlaceholder(const juce::String& value) { + if (value.isEmpty()) { + return "No preset saved"; + } + return value; + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/UIUtils.h b/ports-juce7/syndicate/Syndicate/UI/UIUtils.h new file mode 100644 index 00000000..a12f003b --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/UIUtils.h @@ -0,0 +1,360 @@ +#pragma once + +#include + +#include "CoreJUCEPlugin/CoreLookAndFeel.h" +#include "CoreJUCEPlugin/LookAndFeelMixins/ComboBoxV2.h" +#include "CoreJUCEPlugin/LookAndFeelMixins/LinearSliderV2.h" +#include "CoreJUCEPlugin/LookAndFeelMixins/MidAnchoredRotarySlider.h" +#include "CoreJUCEPlugin/LookAndFeelMixins/RotarySliderV2.h" +#include "ModulationSourceDefinition.hpp" + +namespace UIUtils { + // Chains/plugin slots + constexpr int CHAIN_WIDTH {200}; + constexpr int PLUGIN_SLOT_HEIGHT {30}; + constexpr int PLUGIN_SLOT_CORNER_RADIUS {PLUGIN_SLOT_HEIGHT / 2}; + constexpr int SLOT_DRAG_HANDLE_WIDTH {PLUGIN_SLOT_HEIGHT}; + + // Modulation tray + constexpr int PLUGIN_SLOT_MOD_TRAY_HEIGHT {PLUGIN_SLOT_HEIGHT * 3}; + constexpr int PLUGIN_MOD_TARGET_SLIDER_HEIGHT {static_cast(UIUtils::PLUGIN_SLOT_MOD_TRAY_HEIGHT * 0.25)}; + constexpr int PLUGIN_MOD_TARGET_SLIDER_WIDTH {PLUGIN_MOD_TARGET_SLIDER_HEIGHT}; + + int getChainXPos(int chainIndex, int numChains, int graphViewWidth); + + // Macros + constexpr int NUM_MACROS {4}; + constexpr int MACRO_WIDTH {64}; + constexpr int MACRO_HEIGHT {104}; + constexpr int MACRO_YPAD {10}; + + // Modulation sources + constexpr int MODULATION_BAR_WIDTH {572}; + constexpr int MODULATION_BAR_HEIGHT {130}; + constexpr int MODULATION_LIST_WIDTH {160}; + constexpr int MODULATION_LIST_COLUMN_WIDTH {MODULATION_LIST_WIDTH / 2}; + constexpr int MODULATION_LIST_BUTTON_HEIGHT {24}; + + // LookAndFeel + typedef WECore::LookAndFeelMixins::LinearSliderV2> StandardSliderLookAndFeel; + typedef WECore::LookAndFeelMixins::MidAnchoredRotarySlider MidAnchoredSliderLookAndFeel; + + class ToggleButtonLookAndFeel : public WECore::JUCEPlugin::CoreLookAndFeel { + public: + enum ColourIds { + backgroundColour, + highlightColour, + disabledColour + }; + + void drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool isMouseOverButton, + bool isButtonDown) override; + + void drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool isMouseOverButton, + bool isButtonDown) override; + }; + + class StaticButtonLookAndFeel : public WECore::JUCEPlugin::CoreLookAndFeel { + public: + enum ColourIds { + backgroundColour, + highlightColour, + disabledColour + }; + + virtual void drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool isMouseOverButton, + bool isButtonDown) override; + + virtual void drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool isMouseOverButton, + bool isButtonDown) override; + + protected: + virtual int _getCornerSize(int width, int height) const; + }; + + class AddButtonLookAndFeel : public StaticButtonLookAndFeel { + public: + virtual void drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool isMouseOverButton, + bool isButtonDown) override; + + protected: + virtual int _getCornerSize(int width, int height) const override; + }; + + class TextOnlyButtonLookAndFeel : public StaticButtonLookAndFeel { + public: + void drawButtonBackground(juce::Graphics& /*g*/, + juce::Button& /*button*/, + const juce::Colour& /*backgroundColour*/, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) override { /* Do nothing */ } + }; + + class SearchBarLookAndFeel : public WECore::JUCEPlugin::CoreLookAndFeel { + public: + void drawTextEditorOutline(juce::Graphics& g, + int width, + int height, + juce::TextEditor& textEditor) override; + }; + + class StandardComboBoxLookAndFeel : public WECore::LookAndFeelMixins::ComboBoxV2 { + public: + void drawComboBox(juce::Graphics& g, + int width, + int height, + bool isButtonDown, + int buttonX, + int buttonY, + int buttonW, + int buttonH, + juce::ComboBox& box) override; + + void positionComboBoxText(juce::ComboBox& box, juce::Label& label) override; + + void drawPopupMenuBackground(juce::Graphics& g, int width, int height) override; + + void drawPopupMenuItem(juce::Graphics& g, + const juce::Rectangle& area, + bool isSeparator, + bool isActive, + bool isHighlighted, + bool isTicked, + bool hasSubMenu, + const juce::String& text, + const juce::String& shortcutKeyText, + const juce::Drawable* /*icon*/, + const juce::Colour* /*textColour*/) override; + }; + + class TableHeaderLookAndFeel : public WECore::JUCEPlugin::CoreLookAndFeel { + public: + virtual void drawTableHeaderBackground(juce::Graphics& g, + juce::TableHeaderComponent& header) override; + + virtual void drawTableHeaderColumn(juce::Graphics& g, + juce::TableHeaderComponent& header, + const juce::String& columnName, + int columnId, + int width, + int height, + bool isMouseOver, + bool isMouseDown, + int columnFlags) override; + }; + + class PopoverComponent : public juce::Component { + public: + PopoverComponent(juce::String title, + juce::String content, + std::function onCloseCallback); + + void resized() override; + void paint(juce::Graphics& g) override; + + private: + std::function _onCloseCallback; + + StaticButtonLookAndFeel _buttonLookAndFeel; + + std::unique_ptr _titleLabel; + std::unique_ptr _contentLabel; + std::unique_ptr _contentView; + std::unique_ptr _button; + + juce::Rectangle _contentSize; + + juce::Rectangle _getBoundsForText(const juce::String& content, const juce::Font& font) const; + }; + + const juce::Colour& getColourForModulationType(MODULATION_TYPE type); + + const juce::Colour neutralColour = juce::Colour(226, 226, 226); + const juce::Colour highlightColour = juce::Colour(0xfffc9d74); + const juce::Colour deactivatedColour = neutralColour.withBrightness(0.5); + + const juce::Colour backgroundColour = juce::Colour(0xff272727); + const juce::Colour slotBackgroundColour = juce::Colour(0xff272727).withMultipliedLightness(1.2); + const juce::Colour modulationTrayBackgroundColour = slotBackgroundColour.withMultipliedLightness(1.2); + + const juce::Colour PLUGIN_SLOT_MODULATION_ON_COLOUR(161, 102, 221); + + const juce::Colour tooltipColour = juce::Colour(0xff929292); + + void setDefaultLabelStyle(std::unique_ptr& label); + + class SafeAnimatedComponent : public juce::Component, + protected juce::Timer, + public juce::SettableTooltipClient { + public: + SafeAnimatedComponent(); + virtual ~SafeAnimatedComponent(); + + void start(); + + /** + * We need to make sure the UI thread isn't inside paint() before deleting a gain stage, so + * this will block until the timer has stopped and paint() is finished. + */ + void stop(); + + protected: + /** + * Inheriting classes must reset this at the start of paint() and signal it at the end of + * paint() + */ + juce::WaitableEvent _stopEvent; + + /** + * Can be overidden by inheriting classes for any processing that should be done on the + * message thread. + */ + virtual void _onTimerCallback() {} + + private: + void timerCallback() override; + }; + + /** + * Returns a square that fits in the given rectangle. + */ + template + juce::Rectangle reduceToSquare(juce::Rectangle area) { + const T smallestDimension {std::min(area.getWidth(), area.getHeight())}; + return area.withSizeKeepingCentre(smallestDimension, smallestDimension); + } + + class BypassButton : public juce::Button { + public: + BypassButton(const juce::String& buttonName); + + void paintButton(juce::Graphics& g, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + }; + + class ModulationButton : public juce::Button { + public: + ModulationButton(const juce::String& buttonName); + + void paintButton(juce::Graphics& g, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + }; + + class CrossButton : public juce::Button { + public: + enum ColourIds { + enabledColour, + disabledColour + }; + + CrossButton(const juce::String& buttonName); + + void paintButton(juce::Graphics& g, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + }; + + class PluginOpenButton : public juce::Button { + public: + PluginOpenButton(const juce::String& buttonName); + + void paintButton(juce::Graphics& g, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + }; + + class PluginReplaceButton : public juce::Button { + public: + PluginReplaceButton(const juce::String& buttonName); + + void paintButton(juce::Graphics& g, + bool shouldDrawButtonAsHighlighted, + bool shouldDrawButtonAsDown) override; + }; + + class DragHandle : public juce::Component, public juce::SettableTooltipClient { + public: + DragHandle() : juce::Component("Drag handle") { } + + void paint(juce::Graphics& g) override; + + enum ColourIds + { + handleColourId = 0x1201202 + }; + }; + + class LinkedScrollView : public juce::Viewport { + public: + LinkedScrollView(); + virtual ~LinkedScrollView() = default; + + void setOtherView(juce::Viewport* otherView); + void removeOtherView(juce::Viewport* otherView); + + void scrollBarMoved(juce::ScrollBar* scrollBar, double newRangeStart) override; + + private: + juce::Viewport* _otherView; + }; + + /** + * Displays the historic values of a signal similarly to a seismograph or ECG stylus. + */ + class WaveStylusViewer : public UIUtils::SafeAnimatedComponent { + public: + enum ColourIds { + lineColourId + }; + + WaveStylusViewer(std::function getNextValueCallback); + + void paint(juce::Graphics& g) override; + + private: + std::function _getNextValueCallback; + std::array _envelopeValues; + + void _onTimerCallback() override; + }; + + class UniBiModeButtons : public juce::Component { + public: + UniBiModeButtons(std::function onUniClick, + std::function onBiClick, + std::function getUniState, + std::function getBiState, + juce::Colour buttonColour); + virtual ~UniBiModeButtons() override; + + void resized() override; + + private: + std::function _onUniClick; + std::function _onBiClick; + + UIUtils::ToggleButtonLookAndFeel _buttonLookAndFeel; + + std::unique_ptr _unipolarButton; + std::unique_ptr _bipolarButton; + }; + + juce::String getCopyKeyName(); + juce::String getCmdKeyName(); + juce::String presetNameOrPlaceholder(const juce::String& value); +} diff --git a/ports-juce7/syndicate/Syndicate/UI/UndoRedoComponent.cpp b/ports-juce7/syndicate/Syndicate/UI/UndoRedoComponent.cpp new file mode 100644 index 00000000..ec259b1e --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/UndoRedoComponent.cpp @@ -0,0 +1,72 @@ +#include "UndoRedoComponent.h" + +namespace { + const juce::String undoTooltipPrefix = "Undo the previous change - "; + const juce::String redoTooltipPrefix = "Redo the last change - "; + const juce::String defaultUndoTooltip = undoTooltipPrefix + "no changes to undo"; + const juce::String defaultRedoTooltip = redoTooltipPrefix + "no changes to redo"; +} + +UndoRedoComponent::UndoRedoComponent(SyndicateAudioProcessor& processor) + : _processor(processor) { + _undoButton.reset(new juce::TextButton("Undo Button")); + addAndMakeVisible(_undoButton.get()); + _undoButton->setButtonText(TRANS("Undo")); + _undoButton->setTooltip(defaultUndoTooltip); + _undoButton->setLookAndFeel(&_buttonLookAndFeel); + _undoButton->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + _undoButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + _undoButton->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + _undoButton->onClick = [&]() { + _processor.undo(); + }; + + _redoButton.reset(new juce::TextButton("Redo Button")); + addAndMakeVisible(_redoButton.get()); + _redoButton->setButtonText(TRANS("Redo")); + _redoButton->setTooltip(defaultRedoTooltip); + _redoButton->setLookAndFeel(&_buttonLookAndFeel); + _redoButton->setColour(UIUtils::StaticButtonLookAndFeel::backgroundColour, UIUtils::slotBackgroundColour); + _redoButton->setColour(UIUtils::StaticButtonLookAndFeel::highlightColour, UIUtils::highlightColour); + _redoButton->setColour(UIUtils::StaticButtonLookAndFeel::disabledColour, UIUtils::deactivatedColour); + _redoButton->onClick = [&]() { + processor.redo(); + }; + + refresh(); +} + +UndoRedoComponent::~UndoRedoComponent() { +} + +void UndoRedoComponent::resized() { + constexpr int BUTTON_HEIGHT {24}; + juce::Rectangle availableArea = getLocalBounds(); + availableArea.reduce(4, 0); + availableArea.removeFromTop(8); + + // Undo/Redo + _undoButton->setBounds(availableArea.removeFromTop(BUTTON_HEIGHT)); + availableArea.removeFromTop(4); + _redoButton->setBounds(availableArea.removeFromTop(BUTTON_HEIGHT)); +} + +void UndoRedoComponent::refresh() { + const std::optional undoOperation = ModelInterface::getUndoOperation(_processor.manager); + if (undoOperation.has_value()) { + _undoButton->setTooltip(undoTooltipPrefix + undoOperation.value()); + _undoButton->setEnabled(true); + } else { + _undoButton->setTooltip(defaultUndoTooltip); + _undoButton->setEnabled(false); + } + + const std::optional redoOperation = ModelInterface::getRedoOperation(_processor.manager); + if (redoOperation.has_value()) { + _redoButton->setTooltip(redoTooltipPrefix + redoOperation.value()); + _redoButton->setEnabled(true); + } else { + _redoButton->setTooltip(defaultRedoTooltip); + _redoButton->setEnabled(false); + } +} diff --git a/ports-juce7/syndicate/Syndicate/UI/UndoRedoComponent.h b/ports-juce7/syndicate/Syndicate/UI/UndoRedoComponent.h new file mode 100644 index 00000000..6feb658f --- /dev/null +++ b/ports-juce7/syndicate/Syndicate/UI/UndoRedoComponent.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include "UIUtils.h" +#include "PluginProcessor.h" +#include "MetadataEditComponent.hpp" + +class SyndicateAudioProcessorEditor; + +class UndoRedoComponent : public juce::Component { +public: + UndoRedoComponent(SyndicateAudioProcessor& processor); + ~UndoRedoComponent() override; + + void resized() override; + + void refresh(); + +private: + UIUtils::StaticButtonLookAndFeel _buttonLookAndFeel; + + std::unique_ptr _undoButton; + std::unique_ptr _redoButton; + + SyndicateAudioProcessor& _processor; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(UndoRedoComponent) +}; diff --git a/ports-juce7/syndicate/Tests/TestUtils.hpp b/ports-juce7/syndicate/Tests/TestUtils.hpp new file mode 100644 index 00000000..75242692 --- /dev/null +++ b/ports-juce7/syndicate/Tests/TestUtils.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include "PluginConfigurator.hpp" + +namespace TestUtils { + // TODO remove + inline juce::AudioProcessor::BusesLayout createLayoutWithInputChannels(juce::AudioChannelSet channelSet) { + juce::AudioProcessor::BusesLayout layout; + layout.inputBuses.add(channelSet); + return layout; + }; + + inline juce::AudioProcessor::BusesLayout createLayoutWithChannels(juce::AudioChannelSet inputSet, juce::AudioChannelSet outputSet) { + juce::AudioProcessor::BusesLayout layout; + layout.inputBuses.add(inputSet); + layout.outputBuses.add(outputSet); + return layout; + }; + + // Default implementation that tests can build on top of + class TestPluginInstance : public juce::AudioPluginInstance { + public: + TestPluginInstance() = default; + virtual ~TestPluginInstance() = default; + + TestPluginInstance(BusesProperties supportedBuses) : juce::AudioPluginInstance(supportedBuses) {} + + virtual void fillInPluginDescription(juce::PluginDescription& desc) const { desc.name = "TestPlugin"; } + virtual const juce::String getName() const { return "TestPlugin"; } + virtual void prepareToPlay(double sampleRate, int maximumExpectedSamplesPerBlock) {} + virtual void releaseResources() {} + virtual void processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages) {} + virtual double getTailLengthSeconds() const { return 0; } + virtual bool acceptsMidi() const { return false; } + virtual bool producesMidi() const { return false; } + virtual juce::AudioProcessorEditor* createEditor() { return nullptr; } + virtual bool hasEditor() const { return false; } + virtual int getNumPrograms() { return 0; } + virtual int getCurrentProgram() { return 0; } + virtual void setCurrentProgram(int index) {} + virtual const juce::String getProgramName(int index) { return ""; } + virtual void changeProgramName(int index, const juce::String& newName) {} + virtual void getStateInformation(juce::MemoryBlock& destData) { } + virtual void setStateInformation(const void* data, int sizeInBytes) { } + }; +} diff --git a/ports-juce7/syndicate/Tests/catchMain.cpp b/ports-juce7/syndicate/Tests/catchMain.cpp new file mode 100644 index 00000000..0c7c351f --- /dev/null +++ b/ports-juce7/syndicate/Tests/catchMain.cpp @@ -0,0 +1,2 @@ +#define CATCH_CONFIG_MAIN +#include "catch.hpp" diff --git a/ports-juce7/syndicate/WECore/CarveDSP/CarveDSPUnit.h b/ports-juce7/syndicate/WECore/CarveDSP/CarveDSPUnit.h new file mode 100644 index 00000000..62919920 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CarveDSP/CarveDSPUnit.h @@ -0,0 +1,262 @@ +/* + * File: CarveDSPUnit.h + * + * Version: 2.0.0 + * + * Created: 09/09/2015 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "CarveParameters.h" +#include "General/CoreMath.h" + +/** + * A class for applying waveshaping functions to samples. + * + * To use this class, use the setter and getter methods to manipulate parameters, + * and call the process method to process individual samples. + * + * Each sample is processed with no dependency on the previous samples, therefore a + * single object can be reused for multiple audio streams if so desired. + * + * Internally relies on the parameters provided in CarveParameters.h + * + * The process method must be called once for each sample you wish to process: + * @code + * CarveDSPUnit unit; + * unit.setMode(WECore::Carve::Parameters::MODE.SINE); + * ... + * set any other parameters you need + * ... + * + * for (size_t iii {0}; iii < buffer.size(); iii++) { + * buffer[iii] = unit.process(buffer[iii]); + * } + * @endcode + */ + +namespace WECore::Carve { + + template + class CarveDSPUnit { + static_assert(std::is_floating_point::value, + "Must be provided with a floating point template type"); + + public: + /** + * Sets all parameters to their default values. + */ + CarveDSPUnit() : _preGain(Parameters::PREGAIN.defaultValue), + _postGain(Parameters::POSTGAIN.defaultValue), + _tweak(Parameters::TWEAK.defaultValue), + _mode(Parameters::MODE.defaultValue) { } + + virtual ~CarveDSPUnit() {} + + /** @name Setter Methods */ + /** @{ */ + + /** + * Sets the wave shape which will be applied to the signal. + * + * @param[in] val Value the mode should be set to + * + * @see ModeParameter for valid values + */ + void setMode(int val) { _mode = Parameters::MODE.BoundsCheck(val); } + + /** + * Sets the gain to be applied to the signal before processing. + * More pre-gain = more distortion. + * + * @param[in] val Pre-gain value that should be used + * + * @see PREGAIN for valid values + */ + void setPreGain(double val) { _preGain = Parameters::PREGAIN.BoundsCheck(val); } + + /** + * Sets the gain to be applied to the signal after processing. + * More post-gain = more volume. + * + * @param[in] val Post-gain value that should be used + * + * @see POSTGAIN for valid values + */ + void setPostGain(double val) { _postGain = Parameters::POSTGAIN.BoundsCheck(val); } + + /** + * Sets the tweak value to be applied to the signal during processing. + * This behaves differently for each mode, and modifies the shape of + * the wave applied to the signal + * + * @param[in] val Tweak value that should be used + * + * @see TWEAK for valid values + */ + void setTweak(double val) { _tweak = Parameters::TWEAK.BoundsCheck(val); } + + /** @} */ + + /** @name Getter Methods */ + /** @{ */ + + /** + * @see setMode + */ + int getMode() { return _mode; } + + /** + * @see setPreGain + */ + double getPreGain() { return _preGain; } + + /** + * @see setPostGain + */ + double getPostGain() { return _postGain; } + + /** + * @see setTweak + */ + double getTweak() { return _tweak; } + + /** @} */ + + /** + * Performs the processing on the sample, by calling the appropriate + * private processing methods. + * + * @param[in] inSample The sample to be processed + * + * @return The value of inSample after processing + */ + T process(T inSample) const; + + private: + double _preGain, + _postGain, + _tweak; + + int _mode; + + T _processSine(T inSample) const; + + T _processParabolicSoft(T inSample) const; + + T _processParabolicHard(T inSample) const; + + T _processAsymmetricSine(T inSample) const; + + T _processExponent(T inSample) const; + + T _processClipper(T inSample) const; + }; + + template + T CarveDSPUnit::process(T inSample) const { + switch (_mode) { + case Parameters::ModeParameter::OFF: + return 0; + + case Parameters::ModeParameter::SINE: + return _processSine(inSample); + + case Parameters::ModeParameter::PARABOLIC_SOFT: + return _processParabolicSoft(inSample); + + case Parameters::ModeParameter::PARABOLIC_HARD: + return _processParabolicHard(inSample); + + case Parameters::ModeParameter::ASYMMETRIC_SINE: + return _processAsymmetricSine(inSample); + + case Parameters::ModeParameter::EXPONENT: + return _processExponent(inSample); + + case Parameters::ModeParameter::CLIPPER: + return _processClipper(inSample); + + default: + return _processSine(inSample); + } + } + + template + T CarveDSPUnit::_processSine(T inSample) const { + return ( + (((1 - std::abs(_tweak/2)) * sin(CoreMath::DOUBLE_PI * inSample * _preGain))) + + ((_tweak/2) * sin(4 * CoreMath::DOUBLE_PI * inSample * _preGain)) + ) + * _postGain; + } + + template + T CarveDSPUnit::_processParabolicSoft(T inSample) const { + return ( + CoreMath::DOUBLE_PI * inSample * _preGain * ((4 * _tweak) + - sqrt(4 * pow(inSample * CoreMath::DOUBLE_PI * _preGain, 2))) * 0.5 + ) + * _postGain; + } + + template + T CarveDSPUnit::_processParabolicHard(T inSample) const { + return ( + ((1 - std::abs(_tweak/10)) * (atan(_preGain * 4 * CoreMath::DOUBLE_PI * inSample) / 1.5)) + + ((_tweak/5) * sin(CoreMath::DOUBLE_PI * inSample * _preGain)) + ) + * _postGain; + } + + template + T CarveDSPUnit::_processAsymmetricSine(T inSample) const { + return ( + cos(CoreMath::DOUBLE_PI * inSample * (_tweak + 1)) + * atan(4 * CoreMath::DOUBLE_PI * inSample * _preGain) + ) + * _postGain; + } + + template + T CarveDSPUnit::_processExponent(T inSample) const { + return ( + sin(-0.25 * pow(2 * CoreMath::DOUBLE_E, (inSample * _preGain + 1.5))) + ) + * _postGain; + } + + template + T CarveDSPUnit::_processClipper(T inSample) const { + inSample *= CoreMath::DOUBLE_PI * _preGain; + + const T tweakInverted {1 - _tweak}; + + return ( + sin(inSample) + + 0.3 * sin(3 * inSample) * tweakInverted + + 0.15 * sin(5 * inSample) * tweakInverted + + 0.075 * sin(7 * inSample) * tweakInverted + + 0.0375 * sin(9 * inSample) * tweakInverted + + 0.01875 * sin(11 * inSample) * tweakInverted + ) + * _postGain / 1.5 * -1; + } +} diff --git a/ports-juce7/syndicate/WECore/CarveDSP/CarveNoiseFilter.h b/ports-juce7/syndicate/WECore/CarveDSP/CarveNoiseFilter.h new file mode 100644 index 00000000..990674b8 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CarveDSP/CarveNoiseFilter.h @@ -0,0 +1,169 @@ +/* + * File: CarveNoiseFilter.h + * + * Version: 2.0.0 + * + * Created: 02/06/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "WEFilters/TPTSVFilter.h" + +/** + * A simple bandpass filter which can process mono or stereo signals. + * Was initially created to remove frequencies at the extremes of the human + * hearing range to clean up audio but can fulfil any typical bandpass + * filter purpose. + * + * The cutoff frequencies cannot be changed once the object is constructed. + * + * Has methods for processing either a mono or stereo buffer of samples. + * + * @see setSampleRate - recommended to call before performing any processing + */ +namespace WECore::Carve { + + template + class NoiseFilter { + public: + + /** + * Defaults the sample rate. It is recommended to call setSampleRate manually + * before attempting any processing. + * + * @param lowCutHz Everything below this frequency will be cut + * @param highCutHz Everything above this frequency will be cut + */ + NoiseFilter(double lowCutHz, double highCutHz); + + virtual ~NoiseFilter() {} + + /** + * Configures the filters for the correct sample rate. Ensure this is + * called before attempting to process audio. + * + * @param sampleRate The sample rate the filter should be configured for + */ + inline void setSampleRate(double sampleRate); + + /** + * Resets all filters. + * Call this whenever the audio stream is interrupted (ie. the playhead is moved) + */ + inline void reset(); + + /** + * Applies the filtering to a mono buffer of samples. + * Expect seg faults or other memory issues if arguements passed are incorrect. + * + * @param inSample Pointer to the first sample of the buffer + * @param numSamples Number of samples in the buffer + */ + inline void Process1in1out(T* inSample, size_t numSamples); + + /** + * Applies the filtering to a stereo buffer of samples. + * Expect seg faults or other memory issues if arguements passed are incorrect. + * + * @param inLeftSample Pointer to the first sample of the left channel's buffer + * @param inRightSample Pointer to the first sample of the right channel's buffer + * @param numSamples Number of samples in the buffer. The left and right buffers + * must be the same size. + */ + inline void Process2in2out(T *inLeftSample, T *inRightSample, size_t numSamples); + + private: + WECore::TPTSVF::TPTSVFilter _monoLowCutFilter; + WECore::TPTSVF::TPTSVFilter _leftLowCutFilter; + WECore::TPTSVF::TPTSVFilter _rightLowCutFilter; + + WECore::TPTSVF::TPTSVFilter _monoHighCutFilter; + WECore::TPTSVF::TPTSVFilter _leftHighCutFilter; + WECore::TPTSVF::TPTSVFilter _rightHighCutFilter; + + double _lowCutHz, + _highCutHz; + }; + + template + NoiseFilter::NoiseFilter(double lowCutHz, double highCutHz) : _lowCutHz(lowCutHz), + _highCutHz(highCutHz) { + setSampleRate(44100); + + auto setupLowCutFilter = [lowCutHz](TPTSVF::TPTSVFilter& filter) { + filter.setMode(WECore::TPTSVF::Parameters::ModeParameter::HIGHPASS); + filter.setCutoff(lowCutHz); + filter.setQ(1); + filter.setGain(1); + }; + + auto setupHighCutFilter = [highCutHz](TPTSVF::TPTSVFilter& filter) { + filter.setMode(WECore::TPTSVF::Parameters::ModeParameter::LOWPASS); + filter.setCutoff(highCutHz); + filter.setQ(1); + filter.setGain(1); + }; + + setupLowCutFilter(_monoLowCutFilter); + setupLowCutFilter(_leftLowCutFilter); + setupLowCutFilter(_rightLowCutFilter); + + setupHighCutFilter(_monoHighCutFilter); + setupHighCutFilter(_leftHighCutFilter); + setupHighCutFilter(_rightHighCutFilter); + } + + template + void NoiseFilter::setSampleRate(double sampleRate) { + _monoLowCutFilter.setSampleRate(sampleRate); + _leftLowCutFilter.setSampleRate(sampleRate); + _rightLowCutFilter.setSampleRate(sampleRate); + + _monoHighCutFilter.setSampleRate(sampleRate); + _leftHighCutFilter.setSampleRate(sampleRate); + _rightHighCutFilter.setSampleRate(sampleRate); + } + + template + void NoiseFilter::reset() { + _monoLowCutFilter.reset(); + _leftLowCutFilter.reset(); + _rightLowCutFilter.reset(); + + _monoHighCutFilter.reset(); + _leftHighCutFilter.reset(); + _rightHighCutFilter.reset(); + } + + template + void NoiseFilter::Process1in1out(T* inSample, size_t numSamples) { + _monoLowCutFilter.processBlock(inSample, numSamples); + _monoHighCutFilter.processBlock(inSample, numSamples); + } + + template + void NoiseFilter::Process2in2out(T *inLeftSample, T *inRightSample, size_t numSamples) { + _leftLowCutFilter.processBlock(inLeftSample, numSamples); + _leftHighCutFilter.processBlock(inLeftSample, numSamples); + + _rightLowCutFilter.processBlock(inRightSample, numSamples); + _rightHighCutFilter.processBlock(inRightSample, numSamples); + } +} diff --git a/ports-juce7/syndicate/WECore/CarveDSP/CarveParameters.h b/ports-juce7/syndicate/WECore/CarveDSP/CarveParameters.h new file mode 100644 index 00000000..6b7b6196 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CarveDSP/CarveParameters.h @@ -0,0 +1,57 @@ +/* + * File: CarveParameters.h + * + * Created: 25/09/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/ParameterDefinition.h" + +namespace WECore::Carve::Parameters { + + class ModeParameter : public ParameterDefinition::BaseParameter { + public: + + ModeParameter() : ParameterDefinition::BaseParameter::BaseParameter( + OFF, CLIPPER, SINE) { } + + static constexpr int OFF = 1, + SINE = 2, + PARABOLIC_SOFT = 3, + PARABOLIC_HARD = 4, + ASYMMETRIC_SINE = 5, + EXPONENT = 6, + CLIPPER = 7; + }; + + const ModeParameter MODE; + + //@{ + /** + * A parameter which can take any float value between the ranges defined. + * The values passed on construction are in the following order: + * minimum value, + * maximum value, + * default value + */ + const ParameterDefinition::RangedParameter PREGAIN(0, 2, 1), + POSTGAIN(0, 2, 0.5), + TWEAK(0, 1, 0); + //@} +} diff --git a/ports-juce7/syndicate/WECore/CarveDSP/Tests/CarveNoiseFilterTests.cpp b/ports-juce7/syndicate/WECore/CarveDSP/Tests/CarveNoiseFilterTests.cpp new file mode 100644 index 00000000..afd4dea8 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CarveDSP/Tests/CarveNoiseFilterTests.cpp @@ -0,0 +1,44 @@ +/* + * File: CarveNoiseFilterTests.cpp + * + * Created: 20/05/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "CarveDSP/CarveNoiseFilter.h" + +SCENARIO("CarveNoiseFilter: Silence in = silence out") { + GIVEN("A CarveNoiseFilter and a buffer of silent samples") { + std::vector buffer(1024); + WECore::Carve::NoiseFilter mFilter(20, 20000); + mFilter.setSampleRate(48000); + + WHEN("The silence samples are processed") { + // fill the buffer + std::fill(buffer.begin(), buffer.end(), 0); + + mFilter.Process1in1out(&buffer[0], buffer.size()); + + THEN("The output is silence") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(0.0f).margin(0.00001)); + } + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/CarveDSP/Tests/DSPUnitParameterTests.cpp b/ports-juce7/syndicate/WECore/CarveDSP/Tests/DSPUnitParameterTests.cpp new file mode 100644 index 00000000..5e2e37e5 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CarveDSP/Tests/DSPUnitParameterTests.cpp @@ -0,0 +1,154 @@ +/* + * File: CarveDSPUnitTests.cpp + * + * Created: 26/12/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "CarveDSP/CarveDSPUnit.h" + +SCENARIO("CarveDSPUnit: Parameters can be set and retrieved correctly") { + GIVEN("A new CarveDSPUnit object") { + WECore::Carve::CarveDSPUnit mCarve; + + WHEN("Nothing is changed") { + THEN("Parameters have their default values") { + CHECK(mCarve.getMode() == 2); + CHECK(mCarve.getPreGain() == Approx(1.0)); + CHECK(mCarve.getPostGain() == Approx(0.5)); + CHECK(mCarve.getTweak() == Approx(0.0)); + } + } + + WHEN("All parameters are changed to unique values") { + mCarve.setMode(2); + mCarve.setPreGain(0.02); + mCarve.setPostGain(0.03); + mCarve.setTweak(0.04); + + THEN("They all get their correct unique values") { + CHECK(mCarve.getMode() == 2); + CHECK(mCarve.getPreGain() == Approx(0.02)); + CHECK(mCarve.getPostGain() == Approx(0.03)); + CHECK(mCarve.getTweak() == Approx(0.04)); + } + } + } +} + +SCENARIO("CarveDSPUnit: Parameters enforce their bounds correctly") { + GIVEN("A new CarveDSPUnit object") { + WECore::Carve::CarveDSPUnit mCarve; + + WHEN("All parameter values are too low") { + mCarve.setMode(-5); + mCarve.setPreGain(-5); + mCarve.setPostGain(-5); + mCarve.setTweak(-5); + + THEN("Parameters enforce their lower bounds") { + CHECK(mCarve.getMode() == 1); + CHECK(mCarve.getPreGain() == Approx(0.0)); + CHECK(mCarve.getPostGain() == Approx(0.0)); + CHECK(mCarve.getTweak() == Approx(0.0)); + } + } + + WHEN("All parameter values are too high") { + mCarve.setMode(10); + mCarve.setPreGain(5); + mCarve.setPostGain(5); + mCarve.setTweak(5); + + THEN("Parameters enforce their upper bounds") { + CHECK(mCarve.getMode() == 7); + CHECK(mCarve.getPreGain() == Approx(2.0)); + CHECK(mCarve.getPostGain() == Approx(2.0)); + CHECK(mCarve.getTweak() == Approx(1.0)); + } + } + } +} + +SCENARIO("CarveDSPUnit: Parameter combinations that should result in silence output for any input") { + GIVEN("A new CarveDSPUnit object and a buffer of 0.5fs") { + std::vector buffer(1024); + WECore::Carve::CarveDSPUnit mCarve; + + WHEN("The unit is turned off") { + // fill the buffer + std::fill(buffer.begin(), buffer.end(), 0.5); + + // turn the unit off + mCarve.setMode(1); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The output is silence") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(0.0)); + } + } + } + + WHEN("Unit is on but has 0 pregain") { + // fill the buffer + std::fill(buffer.begin(), buffer.end(), 0.5); + + // turn the unit on, set pregain + mCarve.setMode(2); + mCarve.setPreGain(0); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The output is silence") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(0.0)); + } + } + } + + WHEN("Unit is on but has 0 postgain") { + // fill the buffer + std::fill(buffer.begin(), buffer.end(), 0.5); + + // turn the unit on, set pregain and postgain + mCarve.setMode(2); + mCarve.setPreGain(1); + mCarve.setPostGain(0); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The output is silence") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(0.0)); + } + } + } + } +} + diff --git a/ports-juce7/syndicate/WECore/CarveDSP/Tests/DSPUnitProcessingTests.cpp b/ports-juce7/syndicate/WECore/CarveDSP/Tests/DSPUnitProcessingTests.cpp new file mode 100644 index 00000000..403a7118 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CarveDSP/Tests/DSPUnitProcessingTests.cpp @@ -0,0 +1,266 @@ +/* + * File: DSPUnitProcessingTests.cpp + * + * Created: 08/09/2018 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "CarveDSP/CarveDSPUnit.h" +#include "CarveDSP/Tests/TestData.h" + +SCENARIO("CarveDSPUnit: Silence in = silence out") { + GIVEN("A CarveDSPUnit and a buffer of silent samples") { + std::vector buffer(1024); + WECore::Carve::CarveDSPUnit mCarve; + + WHEN("The silence samples are processed") { + // fill the buffer + std::fill(buffer.begin(), buffer.end(), 0); + + // turn the unit on + mCarve.setMode(2); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The output is silence") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(0.0)); + } + } + } + } +} + +SCENARIO("CarveDSPUnit: Sine Default") { + GIVEN("A CarveDSPUnit and a buffer of sine samples") { + std::vector buffer(1024); + const std::vector& expectedOutput = + TestData::Carve::Data.at(Catch::getResultCapture().getCurrentTestName()); + + WECore::Carve::CarveDSPUnit mCarve; + + // Set some parameters for the input signal + constexpr size_t SAMPLE_RATE {44100}; + constexpr size_t SINE_FREQ {1000}; + constexpr double SAMPLES_PER_CYCLE {SAMPLE_RATE / SINE_FREQ}; + + WHEN("The silence samples are processed") { + // fill the buffer + std::generate(buffer.begin(), + buffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE));} ); + + // turn the unit on + mCarve.setMode(2); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The expected output is produced") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(expectedOutput[iii]).margin(0.00001)); + } + } + } + } +} + +SCENARIO("CarveDSPUnit: Parabolic Soft Default") { + GIVEN("A CarveDSPUnit and a buffer of sine samples") { + std::vector buffer(1024); + const std::vector& expectedOutput = + TestData::Carve::Data.at(Catch::getResultCapture().getCurrentTestName()); + + WECore::Carve::CarveDSPUnit mCarve; + + // Set some parameters for the input signal + constexpr size_t SAMPLE_RATE {44100}; + constexpr size_t SINE_FREQ {1000}; + constexpr double SAMPLES_PER_CYCLE {SAMPLE_RATE / SINE_FREQ}; + + WHEN("The silence samples are processed") { + // fill the buffer + std::generate(buffer.begin(), + buffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE));} ); + + // turn the unit on + mCarve.setMode(3); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The expected output is produced") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(expectedOutput[iii]).margin(0.00001)); + } + } + } + } +} + +SCENARIO("CarveDSPUnit: Parabolic Hard Default") { + GIVEN("A CarveDSPUnit and a buffer of sine samples") { + std::vector buffer(1024); + const std::vector& expectedOutput = + TestData::Carve::Data.at(Catch::getResultCapture().getCurrentTestName()); + + WECore::Carve::CarveDSPUnit mCarve; + + // Set some parameters for the input signal + constexpr size_t SAMPLE_RATE {44100}; + constexpr size_t SINE_FREQ {1000}; + constexpr double SAMPLES_PER_CYCLE {SAMPLE_RATE / SINE_FREQ}; + + WHEN("The silence samples are processed") { + // fill the buffer + std::generate(buffer.begin(), + buffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE));} ); + + // turn the unit on + mCarve.setMode(4); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The expected output is produced") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(expectedOutput[iii]).margin(0.00001)); + } + } + } + } +} + +SCENARIO("CarveDSPUnit: Asymetric Sine Default") { + GIVEN("A CarveDSPUnit and a buffer of sine samples") { + std::vector buffer(1024); + const std::vector& expectedOutput = + TestData::Carve::Data.at(Catch::getResultCapture().getCurrentTestName()); + + WECore::Carve::CarveDSPUnit mCarve; + + // Set some parameters for the input signal + constexpr size_t SAMPLE_RATE {44100}; + constexpr size_t SINE_FREQ {1000}; + constexpr double SAMPLES_PER_CYCLE {SAMPLE_RATE / SINE_FREQ}; + + WHEN("The silence samples are processed") { + // fill the buffer + std::generate(buffer.begin(), + buffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE));} ); + + // turn the unit on + mCarve.setMode(5); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The expected output is produced") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(expectedOutput[iii]).margin(0.00001)); + } + } + } + } +} + +SCENARIO("CarveDSPUnit: Exponent Default") { + GIVEN("A CarveDSPUnit and a buffer of sine samples") { + std::vector buffer(1024); + const std::vector& expectedOutput = + TestData::Carve::Data.at(Catch::getResultCapture().getCurrentTestName()); + + WECore::Carve::CarveDSPUnit mCarve; + + // Set some parameters for the input signal + constexpr size_t SAMPLE_RATE {44100}; + constexpr size_t SINE_FREQ {1000}; + constexpr double SAMPLES_PER_CYCLE {SAMPLE_RATE / SINE_FREQ}; + + WHEN("The silence samples are processed") { + // fill the buffer + std::generate(buffer.begin(), + buffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE));} ); + + // turn the unit on + mCarve.setMode(6); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The expected output is produced") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(expectedOutput[iii]).margin(0.00001)); + } + } + } + } +} + +SCENARIO("CarveDSPUnit: Clipper Default") { + GIVEN("A CarveDSPUnit and a buffer of sine samples") { + std::vector buffer(1024); + const std::vector& expectedOutput = + TestData::Carve::Data.at(Catch::getResultCapture().getCurrentTestName()); + + WECore::Carve::CarveDSPUnit mCarve; + + // Set some parameters for the input signal + constexpr size_t SAMPLE_RATE {44100}; + constexpr size_t SINE_FREQ {1000}; + constexpr double SAMPLES_PER_CYCLE {SAMPLE_RATE / SINE_FREQ}; + + WHEN("The silence samples are processed") { + // fill the buffer + std::generate(buffer.begin(), + buffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE));} ); + + // turn the unit on + mCarve.setMode(7); + + // do processing + for (size_t iii {0}; iii < buffer.size(); iii++) { + buffer[iii] = mCarve.process(buffer[iii]); + } + + THEN("The expected output is produced") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(expectedOutput[iii]).margin(0.00001)); + } + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/CarveDSP/Tests/TestData.h b/ports-juce7/syndicate/WECore/CarveDSP/Tests/TestData.h new file mode 100644 index 00000000..0faca919 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CarveDSP/Tests/TestData.h @@ -0,0 +1,55 @@ +/* + * File: TestData.h + * + * Created: 08/09/2018 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include + +namespace TestData::Carve { + const std::unordered_map> Data = + { + { + "Scenario: CarveDSPUnit: Sine Default", + {0, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 1.92361e-16,-0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -3.84721e-16, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 5.77082e-16, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -7.69443e-16, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 9.61804e-16, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -1.15416e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 1.34653e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711,-0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -1.53889e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 1.73125e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -1.92361e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051,0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 2.11597e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567,-0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -2.30833e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 2.50069e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -2.69305e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17,0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 2.88541e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -3.07777e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 3.27013e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -3.46249e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 3.65485e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -3.84721e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 4.03958e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -4.23194e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985,0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 4.4243e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -4.61666e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 4.80902e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -5.00138e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 5.19374e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -5.3861e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 5.57846e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174,-5.77082e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 5.96318e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -6.15554e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 6.3479e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -6.54026e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593,0.48245, 0.386986, 0.216174, 6.73263e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -6.92499e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 7.11735e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149,-0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -7.30971e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 7.50207e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -7.69443e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567,0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 7.88679e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -8.07915e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 8.27151e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -8.46387e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17, 0.0159857, 0.0634567, 0.140051, 0.239149, 0.34711, 0.441985, 0.49593, 0.48245, 0.386986, 0.216174, 8.65623e-15, -0.216174, -0.386986, -0.48245, -0.49593, -0.441985, -0.34711, -0.239149, -0.140051, -0.0634567, -0.0159857, -6.12323e-17, -0.0159857, -0.0634567, -0.140051, -0.239149, -0.34711, -0.441985, -0.49593, -0.48245, -0.386986, -0.216174, -8.84859e-15, 0.216174, 0.386986, 0.48245, 0.49593, 0.441985, 0.34711, 0.239149, 0.140051, 0.0634567, 0.0159857, 6.12323e-17} + }, + { + "Scenario: CarveDSPUnit: Parabolic Soft Default", + {0, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -7.40053e-32, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 2.96021e-31, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -6.66048e-31, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.18408e-30, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -1.85013e-30, 0.0999471, 0.391691, 0.851597,1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 2.66419e-30, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -3.62626e-30, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625,2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 4.73634e-30, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -5.99443e-30, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 7.40053e-30, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -8.95464e-30, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.06568e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -1.25069e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.4505e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -1.66512e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.89454e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321,-3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -2.13875e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 2.39777e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -2.67159e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 2.96021e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -3.26363e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 3.58186e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -3.91488e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471,4.2627e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471,-4.62533e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 5.00276e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -5.39499e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 5.80201e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -6.22384e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 6.66048e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -7.11191e-29, 0.0999471, 0.391691, 0.851597, 1.44241,2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 7.57814e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -8.05918e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855,3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 8.55501e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -9.06565e-29, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 9.59109e-29, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -1.01313e-28, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.06864e-28, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -1.12562e-28, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.18408e-28, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311,-4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -1.24403e-28, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.30545e-28, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -1.36836e-28, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.43274e-28, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348, -4.83486, -4.54311, -4.08321, -3.4924, -2.81855, -2.11625, -1.44241, -0.851597, -0.391691, -0.0999471, -1.49861e-28, 0.0999471, 0.391691, 0.851597, 1.44241, 2.11625, 2.81855, 3.4924, 4.08321, 4.54311, 4.83486, 4.9348, 4.83486, 4.54311, 4.08321, 3.4924, 2.81855, 2.11625, 1.44241, 0.851597, 0.391691, 0.0999471, 1.56595e-28, -0.0999471, -0.391691, -0.851597, -1.44241, -2.11625, -2.81855, -3.4924, -4.08321, -4.54311, -4.83486, -4.9348} + }, + { + "Scenario: CarveDSPUnit: Parabolic Hard Default", + {0, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 5.12962e-16, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.02592e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.53889e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -2.05185e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 2.56481e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -3.07777e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 3.59073e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -4.1037e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 4.61666e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -5.12962e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 5.64258e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -6.15554e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 6.6685e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -7.18147e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 7.69443e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -8.20739e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 8.72035e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -9.23331e-15, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 9.74628e-15, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016,-0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.02592e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.07722e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.12852e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.17981e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.23111e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.2824e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.3337e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.385e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.43629e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.48759e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.53889e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.59018e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.64148e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.69277e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161,-0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.74407e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.79537e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.84666e-14, 0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 1.89796e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -1.94926e-14, 0.353648, 0.431837, 0.460509, 0.474885,0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 2.00055e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -2.05185e-14, 0.353648, 0.431837, 0.460509,0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 2.10314e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -2.15444e-14, 0.353648, 0.431837,0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 2.20574e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -2.25703e-14, 0.353648,0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129, 0.496858, 0.496016, 0.494512, 0.492161, 0.488629, 0.48329, 0.474885, 0.460509, 0.431837, 0.353648, 2.30833e-14, -0.353648, -0.431837, -0.460509, -0.474885, -0.48329, -0.488629, -0.492161, -0.494512, -0.496016, -0.496858, -0.497129, -0.496858, -0.496016, -0.494512, -0.492161, -0.488629, -0.48329, -0.474885, -0.460509, -0.431837, -0.353648, -2.35962e-14,0.353648, 0.431837, 0.460509, 0.474885, 0.48329, 0.488629, 0.492161, 0.494512, 0.496016, 0.496858, 0.497129} + }, + { + "Scenario: CarveDSPUnit: Asymetric Sine Default", + {0, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 7.69443e-16, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -1.53889e-15, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 2.30833e-15, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -3.07777e-15, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008,0.181405, 0.410172, 0.47833, 3.84721e-15, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -4.61666e-15, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 5.3861e-15, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -6.15554e-15, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 6.92499e-15, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -7.69443e-15, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 8.46387e-15, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -9.23331e-15, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 1.00028e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -1.07722e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 1.15416e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -1.23111e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 1.30805e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -1.385e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 1.46194e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -1.53889e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 1.61583e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -1.69277e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 1.76972e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -1.84666e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 1.92361e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -2.00055e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 2.0775e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -2.15444e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 2.23138e-14, -0.47833, -0.410172,-0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -2.30833e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 2.38527e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -2.46222e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 2.53916e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -2.61611e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 2.69305e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172,-0.47833, -2.76999e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 2.84694e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -2.92388e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 3.00083e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -3.07777e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 3.15472e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -3.23166e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 3.3086e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -3.38555e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693, -0.744906, -0.738008, -0.712075, -0.648322, -0.527546, -0.338939, -0.0907008, 0.181405, 0.410172, 0.47833, 3.46249e-14, -0.47833, -0.410172, -0.181405, 0.0907008, 0.338939, 0.527546, 0.648322, 0.712075, 0.738008, 0.744906, 0.745693, 0.744906, 0.738008, 0.712075, 0.648322, 0.527546, 0.338939, 0.0907008, -0.181405, -0.410172, -0.47833, -3.53944e-14, 0.47833, 0.410172, 0.181405, -0.0907008, -0.338939, -0.527546, -0.648322, -0.712075, -0.738008, -0.744906, -0.745693} + }, + { + "Scenario: CarveDSPUnit: Exponent Default", + {0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098,0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372,0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367,-0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303,-0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372, 0.470625, 0.184847, -0.398901, -0.283009, 0.46098, 0.0892376, -0.499057, -0.059823, 0.461741, 0.388821, 0.0137183, -0.303045, -0.461303, -0.499999, -0.477367, -0.432628, -0.385836, -0.345424, -0.314118, -0.292262, -0.279444, -0.275228, -0.279444, -0.292262, -0.314118, -0.345424, -0.385836, -0.432628, -0.477367, -0.499999, -0.461303, -0.303045, 0.0137183, 0.388821, 0.461741, -0.059823, -0.499057, 0.0892376, 0.46098, -0.283009, -0.398901, 0.184847, 0.470625, 0.499372} + }, + { + "Scenario: CarveDSPUnit: Clipper Default", + {0, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -4.76894e-16, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 9.53789e-16, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095,-0.265377, -1.43068e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.90758e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -2.38447e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 2.86137e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -3.33826e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 3.81515e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -4.29205e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 4.76894e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -5.24584e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 5.72273e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -6.19963e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 6.67652e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -7.15341e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 7.63031e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -8.1072e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552,0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 8.5841e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -9.06099e-15, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 9.53789e-15, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.00148e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.04917e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.09686e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.14455e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.19224e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.23993e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.28761e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.3353e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.38299e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.43068e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.47837e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.52606e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.57375e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.62144e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.66913e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.71682e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914,-0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.76451e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.8122e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.85989e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 1.90758e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -1.95527e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 2.00296e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -2.05065e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 2.09833e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934,-0.146255, -0.0394464, -1.7401e-16, -0.0394464, -0.146255, -0.247934, -0.264116, -0.266552, -0.261914, -0.266438, -0.267839, -0.265095, -0.265377, -2.14602e-14, 0.265377, 0.265095, 0.267839, 0.266438, 0.261914, 0.266552, 0.264116, 0.247934, 0.146255, 0.0394464, 1.7401e-16, 0.0394464, 0.146255, 0.247934, 0.264116, 0.266552, 0.261914, 0.266438, 0.267839, 0.265095, 0.265377, 2.19371e-14, -0.265377, -0.265095, -0.267839, -0.266438, -0.261914, -0.266552, -0.264116, -0.247934, -0.146255, -0.0394464, -1.7401e-16} + } + }; +} + diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreAudioProcessor.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreAudioProcessor.h new file mode 100644 index 00000000..16387e5a --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreAudioProcessor.h @@ -0,0 +1,484 @@ +/* + * File: CoreAudioProcessor.h + * + * Version: 1.0.0 + * + * Created: 10/06/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" +#include "General/ParameterDefinition.h" +#include "ParameterUpdateHandler.h" +#include "CustomParameter.h" + +namespace WECore::JUCEPlugin { + + /** + * This class provides basic functionality that is commonly used by an AudioProcessor in a + * White Elephant plugin. + * + * Classes inheriting from this should: + * - Call registerParameter to declare parameters + */ + class CoreAudioProcessor : public juce::AudioProcessor, + public ParameterUpdateHandler { + public: + inline CoreAudioProcessor(); + inline CoreAudioProcessor(const BusesProperties& ioLayouts); + inline virtual ~CoreAudioProcessor(); + + /** + * Sets a given parameter using a value in the internal (non-normalised) range. + */ + /** @{ */ + inline void setParameterValueInternal(juce::AudioParameterFloat* param, float value); + inline void setParameterValueInternal(juce::AudioParameterInt* param, int value); + inline void setParameterValueInternal(juce::AudioParameterBool* param, bool value); + /** @} */ + + /** + * Adds a listener that will be notified whenever a parameter is changed. + */ + void addParameterChangeListener(juce::ChangeListener* listener) { + _parameterBroadcaster.addChangeListener(listener); + } + + /** + * Removes a previously added listener. + */ + void removeParameterChangeListener(juce::ChangeListener* listener) { + _parameterBroadcaster.removeChangeListener(listener); + } + + /** + * Calls writeToXml and stores the XML in the given memory block. + */ + inline virtual void getStateInformation(juce::MemoryBlock& destData) override; + + /** + * Collects the registered parameter values and writes them to XML. + * + * Float parameters are written in their normalised 0 to 1 range. + * + * Int parameters are written as a float representation of their real (not normalised) value. + * + * Bool parameters are written as a float representation of true or false. + */ + inline std::unique_ptr writeToXml(); + + /** + * Reads the given memory into an XmlElement and calls restoreFromXml. + */ + inline virtual void setStateInformation(const void* data, int sizeInBytes) override; + + /** + * Restores parameter values from previously written XML. + */ + inline void restoreFromXml(std::unique_ptr element); + + protected: + /** + * Used to register public parameters that are visible to the host. + * + * The parameter will be deleted when the processor class is deallocated. + * + * Float parameters are created with their real (not normalised) ranges. + * + * Int parameters are created with their real (not normalised) ranges. + * + * (Bool parameters don't have a meaningful range.) + */ + /** @{ */ + inline void registerParameter(juce::AudioParameterFloat*& param, + const juce::String& name, + const ParameterDefinition::RangedParameter* range, + float defaultValue, + float precision); + + inline void registerParameter(juce::AudioParameterFloat*& param, + const juce::String& name, + const ParameterDefinition::RangedParameter* range, + float defaultValue, + float precision); + + inline void registerParameter(juce::AudioParameterInt*& param, + const juce::String& name, + const ParameterDefinition::BaseParameter* range, + int defaultValue); + + inline void registerParameter(juce::AudioParameterBool*& param, + const juce::String& name, + bool defaultValue); + /** @} */ + + /** + * Used to register private parameters that are not visible to the host. Used for parameters + * that cause other parameter or state changes so make automation impractical, but should + * still be saved/restored and trigger updates through the ParameterBroadcaster as usual. + * + * These parameters typically need a custom setter when being restored to trigger the extra + * parameter/state changes. + * + * The parameter will be deleted when the processor class is deallocated. + * + * Float parameters are created with their real (not normalised) ranges. + * + * Int parameters are created with their real (not normalised) ranges. + * + * Custom parameters don't have a range and can only ever be private. + * + * (Bool parameters don't have a meaningful range.) + * + */ + /** @{ */ + inline void registerPrivateParameter(juce::AudioParameterFloat*& param, + const juce::String& name, + const ParameterDefinition::RangedParameter* range, + float defaultValue, + float precision, + std::function setter); + + inline void registerPrivateParameter(juce::AudioParameterInt*& param, + const juce::String& name, + const ParameterDefinition::BaseParameter* range, + int defaultValue, + std::function setter); + + template + inline void registerPrivateParameter(PARAM_TYPE*& param, const juce::String& name); + /** @} */ + + /** + * Override this and return a vector of parameter names corresponding to the order that + * parameters were stored in using the legacy schema. + */ + virtual std::vector _provideParamNamesForMigration() = 0; + + /** + * Override this to migrate saved parameter values from normalised to internal. + */ + virtual void _migrateParamValues(std::vector& paramValues) = 0; + + private: + + // Increment this after changing how parameter states are stored + static constexpr int PARAMS_SCHEMA_VERSION {1}; + + // We need to store pointers to all the private parameters that are registered so that they + // can be deallocated by the destructor + std::vector _privateParameters; + std::vector _customParameters; + + /** + * Stores a setter and getter for a parameter. Used when persisting parameter values to XML + * and restoring values from XML. + */ + struct ParameterInterface { + juce::String name; + std::function getter; + std::function setter; + }; + + /** + * Stores a setter and getter for a custom parameter. Used when persisting parameter values + * to XML and restoring values from XML. Access to the params XML element is provided since + * custom parameters typically need to store complex data. + */ + struct CustomParameterInterface { + juce::String name; + std::function writeToXml; + std::function restoreFromXml; + }; + + /** + * Listens for parameter changes and triggers the broadcaster so the changes can be handled + * by another thread. + */ + class ParameterBroadcaster : public juce::AudioProcessorParameter::Listener, + public juce::ChangeBroadcaster { + public: + ParameterBroadcaster() = default; + virtual ~ParameterBroadcaster() = default; + + virtual void parameterValueChanged(int /*parameterIndex*/, float /*newValue*/) override { + this->sendChangeMessage(); + } + + virtual void parameterGestureChanged(int /*parameterIndex*/, bool /*gestureIsStarting*/) override {} + }; + + ParameterBroadcaster _parameterBroadcaster; + + /** + * List of parameters which will trigger updates and are stored in XML. + */ + std::vector _paramsList; + std::vector _customParamsList; + + inline std::vector _stringToFloatVector(const juce::String sFloatCSV) const; + + inline std::unique_ptr _migrateParameters( + std::unique_ptr rootElement); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CoreAudioProcessor) + }; + + CoreAudioProcessor::CoreAudioProcessor() { + addParameterChangeListener(&_parameterListener); + } + + CoreAudioProcessor::CoreAudioProcessor(const BusesProperties& ioLayouts) : juce::AudioProcessor(ioLayouts) { + addParameterChangeListener(&_parameterListener); + } + + CoreAudioProcessor::~CoreAudioProcessor() { + for (juce::AudioProcessorParameter* parameter : _privateParameters) { + delete parameter; + } + + for (CustomParameter* parameter : _customParameters) { + delete parameter; + } + } + + void CoreAudioProcessor::setParameterValueInternal(juce::AudioParameterFloat* param, float value) { + param->setValueNotifyingHost(param->getNormalisableRange().convertTo0to1(value)); + } + + void CoreAudioProcessor::setParameterValueInternal(juce::AudioParameterInt* param, int value) { + param->setValueNotifyingHost(param->getNormalisableRange().convertTo0to1(value)); + } + + void CoreAudioProcessor::setParameterValueInternal(juce::AudioParameterBool* param, bool value) { + param->setValueNotifyingHost(value); + } + + void CoreAudioProcessor::getStateInformation(juce::MemoryBlock& destData) { + std::unique_ptr element = writeToXml(); + copyXmlToBinary(*element.get(), destData); + } + + std::unique_ptr CoreAudioProcessor::writeToXml() { + auto rootElement = std::make_unique("Root"); + + // Set the XML params version + rootElement->setAttribute("SchemaVersion", PARAMS_SCHEMA_VERSION); + + // Store the parameters + juce::XmlElement* paramsElement = rootElement->createNewChildElement("Params"); + for (const ParameterInterface& param : _paramsList) { + paramsElement->setAttribute(param.name, param.getter()); + } + + for (const CustomParameterInterface& param : _customParamsList) { + juce::XmlElement* thisParameterElement = paramsElement->createNewChildElement(param.name); + param.writeToXml(thisParameterElement); + } + + return rootElement; + } + + void CoreAudioProcessor::setStateInformation(const void* data, int sizeInBytes) { + std::unique_ptr element(getXmlFromBinary(data, sizeInBytes)); + restoreFromXml(std::move(element)); + } + + void CoreAudioProcessor::restoreFromXml(std::unique_ptr element) { + // Parse the XML + if (element != nullptr) { + + // If state was saved using an old plugin we need to migrate the XML data + if (element->getIntAttribute("SchemaVersion", 0) < PARAMS_SCHEMA_VERSION) { + element = _migrateParameters(std::move(element)); + } + + // Iterate through our list of parameters, restoring them from the XML attributes + juce::XmlElement* paramsElement = element->getChildByName("Params"); + if (paramsElement != nullptr) { + for (const ParameterInterface& param : _paramsList) { + if (paramsElement->hasAttribute(param.name)) { + param.setter(static_cast(paramsElement->getDoubleAttribute(param.name))); + } + } + + for (const CustomParameterInterface& param : _customParamsList) { + juce::XmlElement* thisParameterElement = paramsElement->getChildByName(param.name); + + if (thisParameterElement != nullptr) { + param.restoreFromXml(thisParameterElement); + } + } + } + } + } + + void CoreAudioProcessor::registerParameter(juce::AudioParameterFloat*& param, + const juce::String& name, + const ParameterDefinition::RangedParameter* range, + float defaultValue, + float precision) { + param = new juce::AudioParameterFloat(name, name, {static_cast(range->minValue), static_cast(range->maxValue), precision}, defaultValue); + + ParameterInterface interface = {name, + [¶m]() { return param->get(); }, + [&](float val) { setParameterValueInternal(param, val); }}; + _paramsList.push_back(interface); + + param->addListener(&_parameterBroadcaster); + addParameter(param); + } + + void CoreAudioProcessor::registerParameter(juce::AudioParameterFloat*& param, + const juce::String& name, + const ParameterDefinition::RangedParameter* range, + float defaultValue, + float precision) { + param = new juce::AudioParameterFloat(name, name, {static_cast(range->minValue), static_cast(range->maxValue), precision}, defaultValue); + + ParameterInterface interface = {name, + [¶m]() { return param->get(); }, + [&](float val) { setParameterValueInternal(param, val); }}; + _paramsList.push_back(interface); + + param->addListener(&_parameterBroadcaster); + addParameter(param); + } + + void CoreAudioProcessor::registerParameter(juce::AudioParameterInt*& param, + const juce::String& name, + const ParameterDefinition::BaseParameter* range, + int defaultValue) { + param = new juce::AudioParameterInt(name, name, range->minValue, range->maxValue, defaultValue); + + ParameterInterface interface = {name, + [¶m]() { return param->get(); }, + [&](float val) { setParameterValueInternal(param, static_cast(val)); }}; + _paramsList.push_back(interface); + + param->addListener(&_parameterBroadcaster); + addParameter(param); + } + + void CoreAudioProcessor::registerParameter(juce::AudioParameterBool*& param, + const juce::String& name, + bool defaultValue) { + param = new juce::AudioParameterBool(name, name, defaultValue); + + ParameterInterface interface = {name, + [¶m]() { return param->get(); }, + [&](float val) { setParameterValueInternal(param, static_cast(val)); }}; + _paramsList.push_back(interface); + + param->addListener(&_parameterBroadcaster); + addParameter(param); + } + + void CoreAudioProcessor::registerPrivateParameter(juce::AudioParameterFloat*& param, + const juce::String& name, + const ParameterDefinition::RangedParameter* range, + float defaultValue, + float precision, + std::function setter) { + param = new juce::AudioParameterFloat(name, name, {static_cast(range->minValue), static_cast(range->maxValue), precision}, defaultValue); + + ParameterInterface interface = {name, + [¶m]() { return param->get(); }, + [setter](float val) { setter(val); }}; + _paramsList.push_back(interface); + + param->addListener(&_parameterBroadcaster); + _privateParameters.push_back(param); + } + + void CoreAudioProcessor::registerPrivateParameter(juce::AudioParameterInt*& param, + const juce::String& name, + const ParameterDefinition::BaseParameter* range, + int defaultValue, + std::function setter) { + param = new juce::AudioParameterInt(name, name, range->minValue, range->maxValue, defaultValue); + + ParameterInterface interface = {name, + [¶m]() { return param->get(); }, + [setter](float val) { setter(static_cast(val)); }}; + _paramsList.push_back(interface); + + param->addListener(&_parameterBroadcaster); + _privateParameters.push_back(param); + } + + template + void CoreAudioProcessor::registerPrivateParameter(PARAM_TYPE*& param, const juce::String& name) { + param = new PARAM_TYPE(); + + CustomParameterInterface interface = {name, + [¶m](juce::XmlElement* element) { param->writeToXml(element); }, + [¶m](juce::XmlElement* element) { param->restoreFromXml(element); }}; + _customParamsList.push_back(interface); + + param->setListener(&_parameterBroadcaster); + _customParameters.push_back(param); + } + + std::vector CoreAudioProcessor::_stringToFloatVector(const juce::String sFloatCSV) const { + juce::StringArray tokenizer; + tokenizer.addTokens(sFloatCSV, ",",""); + + std::vector values; + + for (int iii {0}; iii < tokenizer.size(); iii++) { + values.push_back(tokenizer[iii].getFloatValue()); + } + + return values; + } + + std::unique_ptr CoreAudioProcessor::_migrateParameters(std::unique_ptr rootElement) { + const int schemaVersion {rootElement->getIntAttribute("SchemaVersion", 0)}; + + std::unique_ptr retVal = std::make_unique("Root"); + + if (schemaVersion == 0) { + // This is an original parameter schema - parameters are normalised values in a single string + + forEachXmlChildElement((*rootElement), childElement) { + if (childElement->hasTagName("AllUserParam")) { + + // Read the values into a float array + juce::String sFloatCSV = childElement->getAllSubText(); + std::vector readParamValues = _stringToFloatVector(sFloatCSV); + _migrateParamValues(readParamValues); + + std::vector paramNames = _provideParamNamesForMigration(); + + juce::XmlElement* paramsElement = retVal->createNewChildElement("Params"); + + for (size_t idx {0}; idx < paramNames.size(); idx++) { + if (idx < readParamValues.size()) { + paramsElement->setAttribute(paramNames[idx], readParamValues[idx]); + } + } + } + } + } + + return retVal; + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreLookAndFeel.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreLookAndFeel.h new file mode 100644 index 00000000..a5438edc --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreLookAndFeel.h @@ -0,0 +1,339 @@ +/* + * File: CoreLookAndFeel.h + * + * Version: 2.0.0 + * + * Created: 17/09/2015 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" +#include "General/CoreMath.h" + +namespace WECore::JUCEPlugin { + + /** + * A class which contains most of the basic design elements which the white elephant audio plugins + * have in common. + * + * Not all drawing methods are defined, and so methods inherited from LookAndFeel_V2 may be used. + * + * By default the three colours which are used are dark grey, light grey, and neon blue. These can + * be changed using the provided setter methods. + * + * The colour members (highlightColour, etc) are being phased out as it doesn't fit well with + * how colours are managed in JUCE. + */ + class CoreLookAndFeel : public juce::LookAndFeel_V2 { + public: + CoreLookAndFeel() { + setHighlightColour(juce::Colour(34, 252, 255)); + setLightColour(juce::Colour(200, 200, 200)); + setDarkColour(juce::Colour(107, 107, 107)); + } + + virtual ~CoreLookAndFeel() = default; + + CoreLookAndFeel operator=(CoreLookAndFeel&) = delete; + CoreLookAndFeel(CoreLookAndFeel&) = delete; + + virtual inline void drawLinearSliderThumb(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float /*minSliderPos*/, + float /*maxSliderPos*/, + const juce::Slider::SliderStyle style, + juce::Slider& slider) override; + + virtual inline void drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& /*backgroundColour*/, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) override; + + virtual inline void drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) override; + + virtual inline void drawComboBox(juce::Graphics& g, + int /*width*/, + int /*height*/, + const bool /*isButtonDown*/, + int buttonX, + int buttonY, + int buttonW, + int buttonH, + juce::ComboBox& box) override; + + virtual inline void drawLinearSlider(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + const juce::Slider::SliderStyle style, + juce::Slider& slider) override; + + virtual inline void drawLinearSliderBackground(juce::Graphics& g, + int x, + int y, + int width, + int height, + float /*sliderPos*/, + float /*minSliderPos*/, + float /*maxSliderPos*/, + const juce::Slider::SliderStyle /*style*/, + juce::Slider& slider) override; + + virtual inline void drawTooltip(juce::Graphics& g, + const juce::String& text, + int width, + int height) override; + + virtual void setHighlightColour(juce::Colour newColour) { + setColour(juce::ComboBox::arrowColourId, newColour); + setColour(juce::GroupComponent::textColourId, newColour); + setColour(juce::Slider::rotarySliderFillColourId, newColour); + setColour(juce::Slider::thumbColourId, newColour); + setColour(juce::Slider::trackColourId, newColour); + setColour(juce::TextButton::buttonOnColourId, newColour); + setColour(juce::TextButton::textColourOnId, newColour); + + highlightColour = newColour; + } + + virtual void setLightColour(juce::Colour newColour) { + setColour(juce::PopupMenu::backgroundColourId, newColour); + setColour(juce::Slider::backgroundColourId, newColour); + setColour(juce::Slider::rotarySliderOutlineColourId, newColour); + setColour(juce::TextButton::buttonColourId, newColour); + setColour(juce::TextButton::textColourOffId, newColour); + + lightColour = newColour; + } + + virtual void setDarkColour(juce::Colour newColour) { + setColour(juce::PopupMenu::highlightedBackgroundColourId, newColour); + + darkColour = newColour; + } + + protected: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CoreLookAndFeel) + + juce::Colour lightColour, + darkColour, + highlightColour; + }; + + void CoreLookAndFeel::drawLinearSliderThumb(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float /*minSliderPos*/, + float /*maxSliderPos*/, + const juce::Slider::SliderStyle style, + juce::Slider& slider) { + + const float sliderRadius = static_cast(getSliderThumbRadius(slider) - 2); + + juce::Colour* ring; + + if (slider.isEnabled()) { + ring = &highlightColour; + } else { + ring = &lightColour; + } + + if (style == juce::Slider::LinearHorizontal || style == juce::Slider::LinearVertical) + { + float kx, ky; + + if (style == juce::Slider::LinearVertical) + { + kx = x + width * 0.5f; + ky = sliderPos; + } + else + { + kx = sliderPos; + ky = y + height * 0.5f; + } + + juce::Path p; + p.addEllipse(kx - sliderRadius, ky - sliderRadius, sliderRadius * 2, sliderRadius * 2); + + g.setColour(darkColour); + g.fillPath(p); + + g.setColour(*ring); + g.strokePath(p, juce::PathStrokeType(2.0f)); + } + + } + + void CoreLookAndFeel::drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& /*backgroundColour*/, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) { + const int width {button.getWidth()}; + const int height {button.getHeight()}; + + const float indent {2.0f}; + const int cornerSize {juce::jmin(juce::roundToInt(width * 0.4f), + juce::roundToInt(height * 0.4f))}; + + juce::Path p; + juce::PathStrokeType pStroke(1); + juce::Colour* bc {nullptr}; + + + + + if (button.isEnabled()) { + if (button.getToggleState()) { + bc = &highlightColour; + } else { + bc = &lightColour; + } + } else { + bc = &darkColour; + } + + p.addRoundedRectangle(indent, indent, width - 2 * indent, height - 2 * indent, static_cast(cornerSize)); + + + g.setColour(*bc); + g.strokePath(p, pStroke); + } + + void CoreLookAndFeel::drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) { + + juce::Colour* textColour {nullptr}; + + if (textButton.isEnabled()) { + if (textButton.getToggleState() || textButton.getWidth() < 24) { + textColour = &highlightColour; + } else { + textColour = &lightColour; + } + } else { + textColour = &darkColour; + } + + g.setColour(*textColour); + int margin {0}; + + // differentiates between the small button on the tempo sync ratio and larger buttons + if (textButton.getWidth() > 24) { + margin = 5; + } + + g.drawFittedText(textButton.getButtonText(), margin, 0, textButton.getWidth() - 2 * margin, textButton.getHeight(), juce::Justification::centred, 0); + } + + void CoreLookAndFeel::drawComboBox(juce::Graphics& g, + int /*width*/, + int /*height*/, + const bool /*isButtonDown*/, + int buttonX, + int buttonY, + int buttonW, + int buttonH, + juce::ComboBox& box) { + + g.fillAll(lightColour); + g.setColour(darkColour); + g.fillRect(buttonX, buttonY, buttonW, buttonH); + + const float arrowX {0.2f}; + const float arrowH {0.3f}; + + if (box.isEnabled()) { + juce::Path p; + p.addTriangle(buttonX + buttonW * 0.5f, buttonY + buttonH * (0.45f - arrowH), + buttonX + buttonW * (1.0f - arrowX), buttonY + buttonH * 0.45f, + buttonX + buttonW * arrowX, buttonY + buttonH * 0.45f); + + p.addTriangle(buttonX + buttonW * 0.5f, buttonY + buttonH * (0.55f + arrowH), + buttonX + buttonW * (1.0f - arrowX), buttonY + buttonH * 0.55f, + buttonX + buttonW * arrowX, buttonY + buttonH * 0.55f); + + g.setColour(box.isPopupActive() ? highlightColour : lightColour); + + g.fillPath(p); + } + } + + void CoreLookAndFeel::drawLinearSlider(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + const juce::Slider::SliderStyle style, + juce::Slider& slider) { + // Draw background first + drawLinearSliderBackground(g, x, y, width, height, sliderPos, minSliderPos, maxSliderPos, style, slider); + drawLinearSliderThumb(g, x, y, width, height, sliderPos, minSliderPos, maxSliderPos, style, slider); + } + + void CoreLookAndFeel::drawLinearSliderBackground(juce::Graphics& g, + int x, + int y, + int width, + int height, + float /*sliderPos*/, + float /*minSliderPos*/, + float /*maxSliderPos*/, + const juce::Slider::SliderStyle /*style*/, + juce::Slider& slider) { + g.setColour(lightColour); + + if (slider.isHorizontal()) { + g.fillRect(x, y + height / 2, width, 2); + } + } + + void CoreLookAndFeel::drawTooltip(juce::Graphics& g, + const juce::String& text, + int width, + int height) { + g.setColour(lightColour); + g.fillRect(0, 0, width, height); + + g.setColour(darkColour); + g.drawFittedText(text, 0, 0, width, height, juce::Justification::centred, 3); + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreProcessorEditor.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreProcessorEditor.h new file mode 100644 index 00000000..3b01a130 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CoreProcessorEditor.h @@ -0,0 +1,81 @@ +/* + * File: CoreProcessorEditor.h + * + * Version: 1.0.0 + * + * Created: 18/03/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" +#include "CoreAudioProcessor.h" + +namespace WECore::JUCEPlugin { + + /** + * This class provides basic functionality that is commonly used by an AudioProcessorEditor in a + * White Elephant plugin. + * + * Classes inheriting from this should: + * - Override _onParameterUpdate and call it in the constructor + */ +class CoreProcessorEditor : public juce::AudioProcessorEditor, + public ParameterUpdateHandler { + public: + ~CoreProcessorEditor() { + dynamic_cast(processor). + removeParameterChangeListener(&_parameterListener); + } + + protected: + CoreProcessorEditor(CoreAudioProcessor& ownerFilter) + : AudioProcessorEditor(ownerFilter) { + dynamic_cast(processor). + addParameterChangeListener(&_parameterListener); + } + + /** + * Sets the look and feel for all child components. + * + * Previously LookAndFeel::setDefaultLookAndFeel(&defaultLookAndFeel); was used but this + * resulted in a bug where upon opening two instances of the same plugin simultaneously, when + * one is closed the other will stop applying defaultLookAndFeel. + */ + void _assignLookAndFeelToAllChildren(juce::LookAndFeel& defaultLookAndFeel) { + for (int iii {0}; iii < getNumChildComponents(); iii++) { + getChildComponent(iii)->setLookAndFeel(&defaultLookAndFeel); + } + } + + /** + * Sets the LookAndFeel for all child components to nullptr. + * + * Must be called before the LookAndFeel is deallocated, normally this is in the derived + * editor's destructor. + */ + void _removeLookAndFeelFromAllChildren() { + for (int iii {0}; iii < getNumChildComponents(); iii++) { + getChildComponent(iii)->setLookAndFeel(nullptr); + } + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CoreProcessorEditor) + }; +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CustomParameter.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CustomParameter.h new file mode 100644 index 00000000..2ff6aee8 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/CustomParameter.h @@ -0,0 +1,78 @@ +/* + * File: CustomParameter.h + * + * Version: 1.0.0 + * + * Created: 18/08/2021 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" + +namespace WECore::JUCEPlugin { + /** + * Used to store state that shouldn't be exposed to the host, but would benefit from using the + * save/restore and update mechanism used by conventional parameters. + * + * Derive from this class and add your own setter/getter methods as needed, just call + * _updateListener() to trigger an update. + * + * Provide an implementation for restoreFromXml and writeToXml to enable saving and restoring + * parameter state. + */ + class CustomParameter { + public: + inline CustomParameter(); + virtual ~CustomParameter() = default; + + inline void setListener(juce::AudioProcessorParameter::Listener* listener); + inline void removeListener(); + + virtual void restoreFromXml(juce::XmlElement* element) = 0; + virtual void writeToXml(juce::XmlElement* element) = 0; + + protected: + inline void _updateListener(); + + private: + std::mutex _listenerMutex; + juce::AudioProcessorParameter::Listener* _listener; + }; + + CustomParameter::CustomParameter() : _listener(nullptr) { + } + + void CustomParameter::setListener(juce::AudioProcessorParameter::Listener* listener) { + std::scoped_lock lock(_listenerMutex); + _listener = listener; + } + + void CustomParameter::removeListener() { + std::scoped_lock lock(_listenerMutex); + _listener = nullptr; + } + + void CustomParameter::_updateListener() { + std::scoped_lock lock(_listenerMutex); + if (_listener != nullptr) { + _listener->parameterValueChanged(0, 0); + } + } +} \ No newline at end of file diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LabelReadoutSlider.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LabelReadoutSlider.h new file mode 100644 index 00000000..6c33410b --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LabelReadoutSlider.h @@ -0,0 +1,171 @@ +/* + * File: LabelReadoutSlider.h + * + * Version: 1.0.0 + * + * Created: 01/07/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" +#include "General/ParameterDefinition.h" + +namespace WECore::JUCEPlugin { + + /** + * Handles mouse events that may indicate that the Slider value has changed. + */ + class SliderLabelUpdater : public juce::Slider { + public: + explicit SliderLabelUpdater(const juce::String& componentName) : Slider(componentName) {} + virtual ~SliderLabelUpdater() = default; + + /** @name Mouse event handlers */ + /** @{ */ + virtual void mouseEnter(const juce::MouseEvent& event) override { + Slider::mouseEnter(event); + _updateLabel(event); + } + + virtual void mouseMove(const juce::MouseEvent& event) override { + Slider::mouseMove(event); + _updateLabel(event); + } + + virtual void mouseExit(const juce::MouseEvent& event) override { + Slider::mouseExit(event); + _resetLabel(); + } + + virtual void mouseDoubleClick(const juce::MouseEvent& event) override { + Slider::mouseDoubleClick(event); + _updateLabel(event); + } + + virtual void mouseDrag(const juce::MouseEvent& event) override { + Slider::mouseDrag(event); + _updateLabel(event); + } + + virtual void mouseWheelMove(const juce::MouseEvent& event, + const juce::MouseWheelDetails& wheel) override { + Slider::mouseWheelMove(event, wheel); + _updateLabel(event); + } + /** @} */ + + private: + /** + * Called when the Slider value may have changed and the Label(s) should be updated. + */ + virtual void _updateLabel(const juce::MouseEvent& event) = 0; + + /** + * Called when the mouse is no longer over the Slider, so the Label(s) can be reset. + */ + virtual void _resetLabel() = 0; + }; + + /** + * Outputs the value of the Slider to a label while hovering over the slider. + */ + template + class LabelReadoutSlider : public SliderLabelUpdater { + public: + explicit LabelReadoutSlider(const juce::String& componentName) : SliderLabelUpdater(componentName), + _isRunning(false), + _valueToString([](T value) { return juce::String(value, 2); }) {} + + virtual ~LabelReadoutSlider() = default; + + /** + * Tells the slider to start writing to the given component on mouse enter events. + * + * Doesn't take ownership of the component. + */ + /** @{ */ + inline void start(juce::Label* targetLabel, juce::String defaultText); + inline void start(juce::TextButton* targetButton, juce::String defaultText); + /** @} */ + + /** + * Tells the slider to stop writing to the label. + * + * Call this in your destructor if the Label or RangedParameter might which we depend on + * might be deallocated before LabelReadoutSlider. + */ + inline void stop(); + + void setValueToString(std::function valueToString) { _valueToString = valueToString; } + + protected: + std::function _targetCallback; + bool _isRunning; + std::function _valueToString; + + private: + juce::String _defaultText; + + static std::function _labelToCallback(juce::Label* label) { + return [label](const juce::String& text) { label->setText(text, juce::dontSendNotification); }; + } + + static std::function _textButtonToCallback(juce::TextButton* button) { + return [button](const juce::String& text) { button->setButtonText(text); }; + } + + inline virtual void _updateLabel(const juce::MouseEvent& event) override; + + inline virtual void _resetLabel() override; + }; + + template + void LabelReadoutSlider::start(juce::Label* targetLabel, juce::String defaultText) { + _targetCallback = _labelToCallback(targetLabel); + _defaultText = defaultText; + _isRunning = true; + } + + template + void LabelReadoutSlider::start(juce::TextButton* targetButton, juce::String defaultText) { + _targetCallback = _textButtonToCallback(targetButton); + _defaultText = defaultText; + _isRunning = true; + } + + template + void LabelReadoutSlider::stop() { + _isRunning = false; + } + + template + void LabelReadoutSlider::_updateLabel(const juce::MouseEvent& /*event*/) { + if (_isRunning) { + _targetCallback(_valueToString(getValue())); + } + } + + template + void LabelReadoutSlider::_resetLabel() { + if (_isRunning) { + _targetCallback(_defaultText); + } + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/ButtonV2.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/ButtonV2.h new file mode 100644 index 00000000..8e0e4ccc --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/ButtonV2.h @@ -0,0 +1,128 @@ +/* + * File: ButtonV2.h + * + * Version: 1.0.0 + * + * Created: 19/03/2019 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" + +namespace WECore::LookAndFeelMixins { + + /** + * V2 (December 2018) style lookandfeel button mixin. + * + * Uses the following colours: + * -# ** TextButton::buttonOnColourId ** + * -# ** TextButton::buttonColourId ** + * -# ** TextButton::textColourOnId ** + * -# ** TextButton::textColourOffId ** + */ + template + class ButtonV2 : public BASE { + + public: + ButtonV2() = default; + virtual ~ButtonV2() = default; + + /** @{ LookAndFeel overrides */ + inline virtual void drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& backgroundColour, + bool isMouseOverButton, + bool isButtonDown) override; + + inline virtual void drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool isMouseOverButton, + bool isButtonDown) override; + /** @} */ + + private: + static constexpr float _disabledDarker {0.7f}; + }; + + template + void ButtonV2::drawButtonBackground(juce::Graphics& g, + juce::Button& button, + const juce::Colour& /*backgroundColour*/, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) { + const int width {button.getWidth()}; + const int height {button.getHeight()}; + + constexpr float indent {2.0f}; + const int cornerSize {juce::jmin(juce::roundToInt(width * 0.4f), + juce::roundToInt(height * 0.4f))}; + + juce::Path p; + juce::PathStrokeType pStroke(1); + + if (button.isEnabled()) { + if (button.getToggleState()) { + g.setColour(button.findColour(juce::TextButton::buttonOnColourId)); + } else { + g.setColour(button.findColour(juce::TextButton::buttonColourId)); + } + } else { + g.setColour(button.findColour(juce::TextButton::buttonColourId).darker(_disabledDarker)); + } + + p.addRoundedRectangle(indent, + indent, + width - 2 * indent, + height - 2 * indent, + static_cast(cornerSize)); + + g.strokePath(p, pStroke); + } + + template + void ButtonV2::drawButtonText(juce::Graphics& g, + juce::TextButton& textButton, + bool /*isMouseOverButton*/, + bool /*isButtonDown*/) { + if (textButton.isEnabled()) { + if (textButton.getToggleState()) { + g.setColour(textButton.findColour(juce::TextButton::textColourOnId)); + } else { + g.setColour(textButton.findColour(juce::TextButton::textColourOffId)); + } + } else { + g.setColour(textButton.findColour(juce::TextButton::textColourOffId).darker(_disabledDarker)); + } + + constexpr int MARGIN {0}; + + juce::Font font; + font.setTypefaceName(BASE::getTypefaceForFont(font)->getName()); + g.setFont(font); + + g.drawFittedText(textButton.getButtonText(), + MARGIN, + 0, + textButton.getWidth() - 2 * MARGIN, + textButton.getHeight(), + juce::Justification::centred, + 0); + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/ComboBoxV2.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/ComboBoxV2.h new file mode 100644 index 00000000..6f99895d --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/ComboBoxV2.h @@ -0,0 +1,93 @@ +/* + * File: ComboBoxV2.h + * + * Version: 1.0.0 + * + * Created: 19/03/2019 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" + +namespace WECore::LookAndFeelMixins { + + /** + * V2 (December 2018) style lookandfeel combo box mixin. + * + * Uses the following colours: + * -# ** ComboBox::arrowColourId ** + */ + template + class ComboBoxV2 : public BASE { + + public: + ComboBoxV2() = default; + virtual ~ComboBoxV2() = default; + + /** @{ LookAndFeel overrides */ + inline virtual void drawComboBox(juce::Graphics& g, + int width, + int height, + const bool isButtonDown, + int buttonX, + int buttonY, + int buttonW, + int buttonH, + juce::ComboBox& box) override; + /** @} */ + }; + + template + void ComboBoxV2::drawComboBox(juce::Graphics& g, + int /*width*/, + int /*height*/, + const bool /*isButtonDown*/, + int buttonX, + int buttonY, + int buttonW, + int buttonH, + juce::ComboBox& box) { + + g.setColour(box.findColour(juce::ComboBox::arrowColourId)); + + juce::Path p; + constexpr float LINE_WIDTH {0.5}; + const int arrowMarginX {buttonY / 4}; + const int arrowMarginY {buttonH / 3}; + const int arrowTipX {buttonX + (buttonW / 2)}; + const int arrowTipY {buttonY + buttonH - arrowMarginY}; + + // Left side of arrow + p.addLineSegment(juce::Line(buttonX + arrowMarginX, + buttonY + arrowMarginY, + arrowTipX, + arrowTipY), + LINE_WIDTH); + + // Right side of arrow + p.addLineSegment(juce::Line(buttonX + buttonW - arrowMarginX, + buttonY + arrowMarginY, + arrowTipX, + arrowTipY), + LINE_WIDTH); + + g.strokePath(p, juce::PathStrokeType(1)); + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/GroupComponentV2.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/GroupComponentV2.h new file mode 100644 index 00000000..6b0b369c --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/GroupComponentV2.h @@ -0,0 +1,85 @@ +/* + * File: GroupComponentV2.h + * + * Version: 1.0.0 + * + * Created: 19/03/2019 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" + +namespace WECore::LookAndFeelMixins { + + /** + * V2 (December 2018) style lookandfeel group mixin. + * + * Uses the following colours: + * -# ** GroupComponent::textColourId ** + */ + template + class GroupComponentV2 : public BASE { + + public: + GroupComponentV2() : _groupFontName("Courier New") {} + virtual ~GroupComponentV2() = default; + + /** @{ LookAndFeel overrides */ + inline virtual void drawGroupComponentOutline(juce::Graphics& g, + int width, + int height, + const juce::String& text, + const juce::Justification& justification, + juce::GroupComponent& group) override; + + /** @} */ + + void setGroupFontName(const char* fontName) { _groupFontName = fontName; } + + private: + const char* _groupFontName; + }; + + template + void GroupComponentV2::drawGroupComponentOutline(juce::Graphics& g, + int width, + int height, + const juce::String& text, + const juce::Justification& /*justification*/, + juce::GroupComponent& group) { + + constexpr int MARGIN {2}; + + g.setColour(group.findColour(juce::GroupComponent::textColourId)); + + juce::Font font; + font.setTypefaceName(_groupFontName); + g.setFont(font); + + g.drawText(text, + MARGIN, + MARGIN, + width, + height, + juce::Justification::topLeft, + true); + } + +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/LinearSliderV2.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/LinearSliderV2.h new file mode 100644 index 00000000..617c0bb6 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/LinearSliderV2.h @@ -0,0 +1,168 @@ +/* + * File: LinearSliderV2.h + * + * Version: 1.0.0 + * + * Created: 04/07/2021 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" + +namespace WECore::LookAndFeelMixins { + + /** + * V2 (December 2018) style lookandfeel button mixin. + * + * Uses the following colours: + * -# ** Slider::backgroundColourId ** + * -# ** Slider::thumbColourId ** + * -# ** Slider::trackColourId ** + */ + template + class LinearSliderV2 : public BASE { + public: + LinearSliderV2() = default; + virtual ~LinearSliderV2() = default; + + /** @{ LookAndFeel overrides */ + inline virtual void drawLinearSlider(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + const juce::Slider::SliderStyle style, + juce::Slider& slider) override; + + inline virtual void drawLinearSliderThumb(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + const juce::Slider::SliderStyle style, + juce::Slider& slider) override; + + inline virtual void drawLinearSliderBackground(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + const juce::Slider::SliderStyle style, + juce::Slider& slider) override; + /** @} */ + }; + + template + void LinearSliderV2::drawLinearSlider(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float minSliderPos, + float maxSliderPos, + const juce::Slider::SliderStyle style, + juce::Slider& slider) { + // Draw background first + drawLinearSliderBackground(g, x, y, width, height, sliderPos, minSliderPos, maxSliderPos, style, slider); + drawLinearSliderThumb(g, x, y, width, height, sliderPos, minSliderPos, maxSliderPos, style, slider); + } + + template + void LinearSliderV2::drawLinearSliderThumb(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPos, + float minSliderPos, + float /*maxSliderPos*/, + const juce::Slider::SliderStyle style, + juce::Slider& slider) { + + constexpr float MARGIN {2}; + + if (slider.isEnabled()) { + g.setColour(slider.findColour(juce::Slider::thumbColourId)); + } else { + g.setColour(slider.findColour(juce::Slider::backgroundColourId)); + } + + if (style == juce::Slider::LinearHorizontal) { + // Horizontal rectangle + g.fillRect(juce::Rectangle(minSliderPos, + y + MARGIN, + sliderPos - minSliderPos, + height - 3 * MARGIN)); + } else { + // Vertical rectangle + g.fillRect(juce::Rectangle(x + MARGIN, + y + height, + width - 3 * MARGIN, + -(y + height - sliderPos))); + } + } + + template + void LinearSliderV2::drawLinearSliderBackground(juce::Graphics& g, + int x, + int y, + int width, + int height, + float /*sliderPos*/, + float /*minSliderPos*/, + float /*maxSliderPos*/, + const juce::Slider::SliderStyle style, + juce::Slider& slider) { + + constexpr int MARGIN {2}; + constexpr int LINE_WIDTH {1}; + + if (slider.isEnabled()) { + g.setColour(slider.findColour(juce::Slider::trackColourId)); + } else { + g.setColour(slider.findColour(juce::Slider::backgroundColourId)); + } + + if (style == juce::Slider::LinearHorizontal) { + // Horizontal line + g.fillRect(juce::Rectangle(x, + y + height - MARGIN - (LINE_WIDTH / 2), + width, + LINE_WIDTH)); + } else { + // Vertical line + g.fillRect(juce::Rectangle(x + width - MARGIN - (LINE_WIDTH / 2), + y, + LINE_WIDTH, + height)); + + } + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h new file mode 100644 index 00000000..d37e86bd --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h @@ -0,0 +1,107 @@ +/* + * File: LookAndFeelMixinsV2.h + * + * Version: 1.0.0 + * + * Created: 19/03/2019 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "ButtonV2.h" +#include "ComboBoxV2.h" +#include "GroupComponentV2.h" +#include "LinearSliderV2.h" +#include "MidAnchoredRotarySlider.h" +#include "PopupMenuV2.h" +#include "RotarySliderV2.h" +#include "CoreJUCEPlugin/CoreLookAndFeel.h" + +/** + * Mixin LookAndFeel classes that can be used to augment any existing class derived from + * juce::LookAndFeel. + * + * Mixin classes have been provided rather than a single LookAndFeel as it allows different + * LookAndFeels for different components to be mixed and overriden separately. Each mixin class + * handles all the drawing for a particular component. + * + * @section Example Usage + * + * To create a LookAndFeel class which uses juce::LookAndFeel_V4 but with WECore's buttons: + * + * @code + * #include "CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h" + * + * using WECore::LookAndFeelMixins; + * typedef ButtonV2 WECoreButtonsLookAndFeel; + * + * WECoreButtonsLookAndFeel _myLookAndFeelInstance; + * @endcode + * + * To create a LookAndFeel class which uses juce::LookAndFeel_V4 but with WECore's buttons *and* + * combo boxes: + * + * @code + * #include "CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h" + * + * using WECore::LookAndFeelMixins; + * typedef ButtonV2> WECoreComboLookAndFeel; + * + * WECoreComboLookAndFeel _myLookAndFeelInstance; + * @endcode + * + * You can then futher customise these typedef'd LookAndFeel classes in the same way you would + * customise a normal JUCE LookAndFeelClass: + * + * @code + * #include "CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h" + * + * using WECore::LookAndFeelMixins; + * typedef ButtonV2> WECoreComboLookAndFeel; + * + * class MyCustomLookAndFeel : public WECoreComboLookAndFeel { + * ... custom overrides ... + * } + * @endcode + * + * or, if you already have a custom LookAndFeel which you'd like to augment with the mixin classes, + * you can do that too: + * + * @code + * #include "CoreJUCEPlugin/LookAndFeelMixins/LookAndFeelMixins.h" + * + * using WECore::LookAndFeelMixins; + * typedef ButtonV2> NewCustomLookAndFeel; + * + * NewCustomLookAndFeel _myLookAndFeelInstance; + * @endcode + */ +namespace WECore::LookAndFeelMixins { + /** + * typedef'd all V2 mixins over the CoreLookAndFeel for convenience. + */ + typedef ButtonV2< + ComboBoxV2< + GroupComponentV2< + LinearSliderV2< + PopupMenuV2< + RotarySliderV2< + WECore::JUCEPlugin::CoreLookAndFeel + >>>>>> WEV2LookAndFeel; +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/MidAnchoredRotarySlider.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/MidAnchoredRotarySlider.h new file mode 100644 index 00000000..ad8bdd21 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/MidAnchoredRotarySlider.h @@ -0,0 +1,126 @@ +/* + * File: MidAnchoredRotarySlider.h + * + * Version: 1.0.0 + * + * Created: 21/11/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" +#include "General/CoreMath.h" + +namespace WECore::LookAndFeelMixins { + + /** + * Based on RotarySliderV2, but fills the right side of the slider when above half of the + * slider's value, and fills the left half when below. + * + * Uses the following colours: + * -# ** Slider::rotarySliderFillColourId ** + * -# ** Slider::rotarySliderOutlineColourId ** + */ + template + class MidAnchoredRotarySlider : public BASE { + + public: + MidAnchoredRotarySlider() = default; + virtual ~MidAnchoredRotarySlider() = default; + + /** @{ LookAndFeel overrides */ + inline virtual void drawRotarySlider(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPosProportional, + float rotaryStartAngle, + float rotaryEndAngle, + juce::Slider &slider) override; + /** @} */ + }; + + template + void MidAnchoredRotarySlider::drawRotarySlider(juce::Graphics& g, + int /*x*/, + int /*y*/, + int width, + int height, + float /*sliderPosProportional*/, + float /*rotaryStartAngle*/, + float /*rotaryEndAngle*/, + juce::Slider &slider) { + + // Calculate useful constants + constexpr double arcGap {CoreMath::DOUBLE_TAU / 4}; + constexpr double rangeOfMotion {CoreMath::DOUBLE_TAU - arcGap}; + + const double sliderNormalisedValue {(slider.getValue() - slider.getMinimum()) / + (slider.getMaximum() - slider.getMinimum())}; + + double arcStartPoint {0}; + double arcEndPoint {0}; + if (sliderNormalisedValue > 0.5) { + arcStartPoint = CoreMath::DOUBLE_PI; + arcEndPoint = CoreMath::DOUBLE_PI + (sliderNormalisedValue - 0.5) * rangeOfMotion; + } else { + arcStartPoint = CoreMath::DOUBLE_PI + (sliderNormalisedValue - 0.5) * rangeOfMotion; + arcEndPoint = CoreMath::DOUBLE_PI; + } + + constexpr int margin {2}; + juce::Rectangle area = slider.getBounds(); + area.reduce(margin, margin); + const int diameter {std::min(area.getWidth(), area.getHeight())}; + + if (slider.isEnabled()) { + g.setColour(slider.findColour(juce::Slider::rotarySliderFillColourId)); + } else { + g.setColour(slider.findColour(juce::Slider::rotarySliderOutlineColourId)); + } + + juce::Path p; + + // Draw inner ring + constexpr int arcSpacing {3}; + p.addCentredArc(width / 2, + height / 2, + diameter / 2 - arcSpacing, + diameter / 2 - arcSpacing, + CoreMath::DOUBLE_PI, + arcGap / 2, + CoreMath::DOUBLE_TAU - (arcGap / 2), + true); + + g.strokePath(p, juce::PathStrokeType(0.7f)); + + // Draw outer ring + p.clear(); + p.addCentredArc(width / 2, + height / 2, + diameter / 2, + diameter / 2, + CoreMath::DOUBLE_PI, + arcStartPoint, + arcEndPoint, + true); + g.strokePath(p, juce::PathStrokeType(3.0f)); + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/PopupMenuV2.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/PopupMenuV2.h new file mode 100644 index 00000000..a0cf90ad --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/PopupMenuV2.h @@ -0,0 +1,117 @@ +/* + * File: PopupMenuV2.h + * + * Version: 1.0.0 + * + * Created: 19/03/2019 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" + +namespace WECore::LookAndFeelMixins { + + /** + * V2 (December 2018) style lookandfeel popup ment mixin. + * + * Uses the following colours: + * -# ** PopupMenu::highlightedBackgroundColourId ** + * -# ** PopupMenu::highlightedTextColourId ** + * -# ** PopupMenu::textColourId ** + */ + template + class PopupMenuV2 : public BASE { + + public: + PopupMenuV2() : _popupMenuFontName("Courier New") {} + virtual ~PopupMenuV2() = default; + + /** @{ LookAndFeel overrides */ + inline virtual void drawPopupMenuItem(juce::Graphics& g, + const juce::Rectangle& area, + bool isSeparator, + bool isActive, + bool isHighlighted, + bool isTicked, + bool hasSubMenu, + const juce::String& text, + const juce::String& shortcutKeyText, + const juce::Drawable* icon, + const juce::Colour* textColour) override; + + inline virtual juce::Font getPopupMenuFont() override; + /** @} */ + + void setPopupMenuFontName(const char* fontName) { _popupMenuFontName = fontName; } + + private: + const char* _popupMenuFontName; + }; + + template + void PopupMenuV2::drawPopupMenuItem(juce::Graphics& g, + const juce::Rectangle& area, + bool /*isSeparator*/, + bool /*isActive*/, + bool isHighlighted, + bool isTicked, + bool /*hasSubMenu*/, + const juce::String& text, + const juce::String& /*shortcutKeyText*/, + const juce::Drawable* /*icon*/, + const juce::Colour* /*textColour*/) { + + juce::Rectangle r = area.reduced(1); + + if (isHighlighted) { + g.setColour(BASE::findColour(juce::PopupMenu::highlightedBackgroundColourId)); + g.fillRect(r); + + g.setColour(BASE::findColour(juce::PopupMenu::highlightedTextColourId)); + } else if (isTicked) { + g.setColour(BASE::findColour(juce::PopupMenu::highlightedBackgroundColourId).withAlpha(0.2f)); + g.fillRect(r); + + g.setColour(BASE::findColour(juce::PopupMenu::textColourId)); + } else { + g.setColour(BASE::findColour(juce::PopupMenu::textColourId)); + } + + juce::Font font(getPopupMenuFont()); + + const float maxFontHeight {area.getHeight() / 1.3f}; + + if (font.getHeight() > maxFontHeight) { + font.setHeight(maxFontHeight); + } + + g.setFont(font); + + r.removeFromLeft(3); + g.drawFittedText(text, r, juce::Justification::centredLeft, 1); + } + + template + juce::Font PopupMenuV2::getPopupMenuFont() { + juce::Font comboFont; + comboFont.setTypefaceName(_popupMenuFontName); + return comboFont; + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/RotarySliderV2.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/RotarySliderV2.h new file mode 100644 index 00000000..38e2e8b5 --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/LookAndFeelMixins/RotarySliderV2.h @@ -0,0 +1,114 @@ +/* + * File: RotarySliderV2.h + * + * Version: 1.0.0 + * + * Created: 19/03/2019 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" + +namespace WECore::LookAndFeelMixins { + + /** + * V2 (December 2018) style lookandfeel button mixin. + * + * Uses the following colours: + * -# ** Slider::rotarySliderFillColourId ** + * -# ** Slider::rotarySliderOutlineColourId ** + */ + template + class RotarySliderV2 : public BASE { + + public: + RotarySliderV2() = default; + virtual ~RotarySliderV2() = default; + + /** @{ LookAndFeel overrides */ + inline virtual void drawRotarySlider(juce::Graphics& g, + int x, + int y, + int width, + int height, + float sliderPosProportional, + float rotaryStartAngle, + float rotaryEndAngle, + juce::Slider &slider) override; + /** @} */ + }; + + template + void RotarySliderV2::drawRotarySlider(juce::Graphics& g, + int /*x*/, + int /*y*/, + int width, + int height, + float /*sliderPosProportional*/, + float /*rotaryStartAngle*/, + float /*rotaryEndAngle*/, + juce::Slider &slider) { + + // Calculate useful constants + constexpr double arcGap {CoreMath::DOUBLE_TAU / 4}; + constexpr double rangeOfMotion {CoreMath::DOUBLE_TAU - arcGap}; + + const double sliderNormalisedValue {slider.valueToProportionOfLength(slider.getValue())}; + const double arcEndPoint {sliderNormalisedValue * rangeOfMotion + arcGap / 2}; + + constexpr int margin {2}; + juce::Rectangle area = slider.getBounds(); + area.reduce(margin, margin); + const int diameter {std::min(area.getWidth(), area.getHeight())}; + + if (slider.isEnabled()) { + g.setColour(slider.findColour(juce::Slider::rotarySliderFillColourId)); + } else { + g.setColour(slider.findColour(juce::Slider::rotarySliderOutlineColourId)); + } + + juce::Path p; + + // Draw inner ring + constexpr int arcSpacing {3}; + p.addCentredArc(width / 2, + height / 2, + diameter / 2 - arcSpacing, + diameter / 2 - arcSpacing, + CoreMath::DOUBLE_PI, + arcGap / 2, + CoreMath::DOUBLE_TAU - (arcGap / 2), + true); + + g.strokePath(p, juce::PathStrokeType(0.7f)); + + // Draw outer ring + p.clear(); + p.addCentredArc(width / 2, + height / 2, + diameter / 2, + diameter / 2, + CoreMath::DOUBLE_PI, + arcGap / 2, + arcEndPoint, + true); + g.strokePath(p, juce::PathStrokeType(3.0f)); + } +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/ParameterUpdateHandler.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/ParameterUpdateHandler.h new file mode 100644 index 00000000..b16461ba --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/ParameterUpdateHandler.h @@ -0,0 +1,61 @@ +/* + * File: CoreProcessorEditor.h + * + * Created: 24/03/2021 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" +#include "CoreAudioProcessor.h" + +namespace WECore::JUCEPlugin { + + class ParameterUpdateHandler { + + public: + ParameterUpdateHandler() : _parameterListener(this) { } + virtual ~ParameterUpdateHandler() = default; + + protected: + /** + * Is notified when a parameter has changed and calls _onParameterUpdate. + */ + class ParameterUpdateListener : public juce::ChangeListener { + public: + ParameterUpdateListener(ParameterUpdateHandler* parent) : _parent(parent) {}; + virtual ~ParameterUpdateListener() = default; + + virtual void changeListenerCallback(juce::ChangeBroadcaster* /*source*/) { + _parent->_onParameterUpdate(); + } + + private: + ParameterUpdateHandler* _parent; + }; + + ParameterUpdateListener _parameterListener; + + /** + * This will be called whenever a parameter has been updated. + */ + virtual void _onParameterUpdate() = 0; + + }; +} diff --git a/ports-juce7/syndicate/WECore/CoreJUCEPlugin/TooltipLabelUpdater.h b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/TooltipLabelUpdater.h new file mode 100644 index 00000000..c6f8a83c --- /dev/null +++ b/ports-juce7/syndicate/WECore/CoreJUCEPlugin/TooltipLabelUpdater.h @@ -0,0 +1,147 @@ +/* + * File: TooltipLabelUpdater.h + * + * Version: 1.0.0 + * + * Created: 06/06/2021 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + * + */ + +#pragma once + +#include "JuceHeader.h" + +namespace WECore::JUCEPlugin { + + /** + * Acts as a MouseListener for multiple components, setting the given Label to display their + * tooltip. + */ + class TooltipLabelUpdater : public juce::MouseListener { + public: + inline TooltipLabelUpdater(); + ~TooltipLabelUpdater() = default; + + /** + * Starts updating the label as necessary displaying an empty string when not showing a + * tooltip. + */ + inline void start(juce::Label* targetLabel); + + /** + * Starts updating the label as necessary, displaying build information when not showing a + * tooltip. + */ + inline void start(juce::Label* targetLabel, juce::AudioProcessor::WrapperType pluginFormat, bool isDemo = false); + + /** + * Must be called before the given label is destructed. + */ + void stop() { _targetLabel = nullptr; } + + inline virtual void mouseEnter(const juce::MouseEvent& event) override; + inline virtual void mouseExit(const juce::MouseEvent& event) override; + + inline void refreshTooltip(juce::Component* component); + + private: + juce::Label* _targetLabel; + juce::String _defaultString; + }; + + TooltipLabelUpdater::TooltipLabelUpdater() : _targetLabel(nullptr) { + } + + void TooltipLabelUpdater::start(juce::Label* targetLabel) { + _targetLabel = targetLabel; + _defaultString = ""; + } + + void TooltipLabelUpdater::start(juce::Label* targetLabel, juce::AudioProcessor::WrapperType pluginFormat, bool isDemo) { + _targetLabel = targetLabel; + + _defaultString = JucePlugin_Name; + _defaultString += " "; + _defaultString += JucePlugin_VersionString; + + // Format + _defaultString += " "; + _defaultString += juce::AudioProcessor::getWrapperTypeDescription(pluginFormat); + + // OS + _defaultString += " "; +#if _WIN32 + _defaultString += "Win"; +#elif __APPLE__ + #if TARGET_OS_IPHONE + _defaultString += "iOS"; + #else + _defaultString += "macOS"; + #endif +#elif __linux__ + _defaultString += "Linux"; +#else + #error "Unknown OS" +#endif + + // Arch + _defaultString += " "; +#if defined(__x86_64__) || defined(_M_AMD64) + _defaultString += "x86_64"; +#elif defined(__aarch64__) || defined(_M_ARM64) + _defaultString += "arm64"; +#else + #error "Unknown arch" +#endif + + // Demo + if (isDemo) { + _defaultString += " (DEMO)"; + } + + _targetLabel->setText(_defaultString, juce::dontSendNotification); + } + + void TooltipLabelUpdater::mouseEnter(const juce::MouseEvent& event) { + if (_targetLabel != nullptr) { + juce::TooltipClient* tooltipClient = dynamic_cast(event.eventComponent); + + if (tooltipClient != nullptr) { + const juce::String displayString = tooltipClient->getTooltip().isEmpty() ? _defaultString : tooltipClient->getTooltip(); + _targetLabel->setText(displayString, juce::dontSendNotification); + } + } + } + + void TooltipLabelUpdater::mouseExit(const juce::MouseEvent& /*event*/) { + if (_targetLabel != nullptr) { + _targetLabel->setText(_defaultString, juce::dontSendNotification); + } + } + + void TooltipLabelUpdater::refreshTooltip(juce::Component* component) { + if (_targetLabel != nullptr) { + juce::TooltipClient* tooltipClient = dynamic_cast(component); + + if (tooltipClient != nullptr) { + const juce::String displayString = tooltipClient->getTooltip().isEmpty() ? _defaultString : tooltipClient->getTooltip(); + _targetLabel->setText(displayString, juce::dontSendNotification); + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/General/AudioSpinMutex.h b/ports-juce7/syndicate/WECore/General/AudioSpinMutex.h new file mode 100644 index 00000000..6bf56c1b --- /dev/null +++ b/ports-juce7/syndicate/WECore/General/AudioSpinMutex.h @@ -0,0 +1,165 @@ +/* + * File: AudioSpinMutex.h + * + * Created: 22/10/2021 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include +#include +#include + +// Some useful links about these instructions in notes/splnlock-instructions.txt +#if defined(__x86_64__) || defined(_M_AMD64) + #include + #define CPU_PAUSE _mm_pause(); +#elif defined(__aarch64__) || defined(_M_ARM64) + #define CPU_PAUSE __asm__ __volatile__("yield" ::: "memory"); +#else + #error Unsupported architecture +#endif + +namespace WECore { + + /** + * Provides mutex that the audio thread can try-lock in a realtime safe way. + * + * Based on the implementation described here: + * https://timur.audio/using-locks-in-real-time-audio-processing-safely + */ + class AudioSpinMutex { + public: + AudioSpinMutex() = default; + ~AudioSpinMutex() = default; + + /** + * Call from the non-realtime thread. + * + * Will block until the mutex is locked. + */ + void lock() { + constexpr std::array iterations = {5, 10, 3000}; + + for (int i = 0; i < iterations[0]; ++i) { + if (tryLock()) { + return; + } + } + + for (int i = 0; i < iterations[1]; ++i) { + if (tryLock()) { + return; + } + + CPU_PAUSE + } + + while (true) { + for (int i = 0; i < iterations[2]; ++i) { + if (tryLock()) { + return; + } + + CPU_PAUSE + CPU_PAUSE + CPU_PAUSE + CPU_PAUSE + CPU_PAUSE + CPU_PAUSE + CPU_PAUSE + CPU_PAUSE + CPU_PAUSE + CPU_PAUSE + } + + // Waiting longer than we should, let's give other threads + // a chance to recover + std::this_thread::yield(); + } + } + + /** + * Returns true if the lock was successful. + * + * Is safe to call from the audio thread. + */ + bool tryLock() { + return !flag.test_and_set(std::memory_order_acquire); + } + + /** + * Call to unlock the mutex. + */ + void unlock() { + flag.clear(std::memory_order_release); + } + + private: + std::atomic_flag flag = ATOMIC_FLAG_INIT; + }; + + class AudioSpinLockBase { + public: + AudioSpinLockBase(AudioSpinMutex& mutex) : _mutex(mutex), _isLocked(false) { } + + ~AudioSpinLockBase() { + if (_isLocked) { + _mutex.unlock(); + } + } + + void unlock() { + if (_isLocked) { + _mutex.unlock(); + _isLocked = false; + } + } + + bool isLocked() { return _isLocked; } + + protected: + AudioSpinMutex& _mutex; + + // Keeps track of whether this lock is still holding the mutex. Must always check this + // internally before calling unlock on the mutex, otherwise we might unlock it, then the lock + // is taken by someone else, then we unlock it again. + bool _isLocked; + }; + + /** + * Locks the given AudioSpinMutex on constuction, unlocks it on destruction. + */ + class AudioSpinLock : public AudioSpinLockBase { + public: + explicit AudioSpinLock(AudioSpinMutex& mutex) : AudioSpinLockBase(mutex) { + _mutex.lock(); + _isLocked = true; + } + }; + + /** + * Will try lock the given AudioSpinMutex on construction and unlock on desctruction if needed. + */ + class AudioSpinTryLock : public AudioSpinLockBase { + public: + explicit AudioSpinTryLock(AudioSpinMutex& mutex) : AudioSpinLockBase(mutex) { + _isLocked = _mutex.tryLock(); + } + }; +} diff --git a/ports-juce7/syndicate/WECore/General/CoreMath.h b/ports-juce7/syndicate/WECore/General/CoreMath.h new file mode 100644 index 00000000..3db0e097 --- /dev/null +++ b/ports-juce7/syndicate/WECore/General/CoreMath.h @@ -0,0 +1,54 @@ +/* + * File: CoreMath.h + * + * Created: 18/03/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#ifndef CoreMath_h +#define CoreMath_h + +#include +#include + +/** + * Contains generic math related consts and functions. + */ +namespace WECore::CoreMath { + /** + * Used for portability since Visual Studio has a different implementation of math.h + */ + constexpr long double LONG_PI {3.14159265358979323846264338327950288}; + constexpr double DOUBLE_PI {static_cast(LONG_PI)}; + + constexpr long double LONG_TAU {2 * LONG_PI}; + constexpr double DOUBLE_TAU {static_cast(LONG_TAU)}; + + constexpr long double LONG_E {2.71828182845904523536028747135266250}; + constexpr double DOUBLE_E {static_cast(LONG_E)}; + + template + bool compareFloatsEqual(T x, T y, T tolerance = std::numeric_limits::epsilon()) { + return std::abs(x - y) < tolerance; + } + + constexpr double MINUS_INF_DB {-200}; + inline double linearTodB(double val) { return val > 0 ? std::fmax(20 * std::log10(val), MINUS_INF_DB) : MINUS_INF_DB; } + inline double dBToLinear(double val) { return std::pow(10.0, val / 20.0); } +} + +#endif /* CoreMath_h */ diff --git a/ports-juce7/syndicate/WECore/General/ParameterDefinition.h b/ports-juce7/syndicate/WECore/General/ParameterDefinition.h new file mode 100644 index 00000000..16fa2a06 --- /dev/null +++ b/ports-juce7/syndicate/WECore/General/ParameterDefinition.h @@ -0,0 +1,131 @@ +/* + * File: ParameterDefinition.h + * + * Created: 22/09/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include +#include + +/** + * Contains classes that are used for defining parameters. Note that these are not + * intended to define individual parameters (and as such they will not store the + * current value of a parameter), but are intended to define some characteristics + * of a given type of parameter, such as the values that are valid for it and + * provide some methods for performing calculations relating to those characteristics. + */ +namespace ParameterDefinition { + /** + * If the given value is between the minimum and maximum values for this parameter, + * then the value is returned unchanged. If the given value is outside the minimum + * and maximum values for this parameter, the given value is clipped to this range + * and then returned. + * + * @param val Value to clip to minumum and maximum values + * + * @return Clipped value + */ + template + T BoundsCheck(T val, T min, T max) { + if (val < min) val = min; + if (val > max) val = max; + + return val; + } + + class BooleanParameter { + public: + BooleanParameter(bool newDefaultValue) : defaultValue(newDefaultValue) {} + + bool defaultValue; + }; + + /** + * Provides basic functionality that may be useful for building other parameters from. + */ + template + class BaseParameter { + public: + + const T minValue; + const T maxValue; + const T defaultValue; + + BaseParameter() = delete; + virtual ~BaseParameter() = default; + + BaseParameter(T newMinValue, + T newMaxValue, + T newDefaultValue) : minValue(newMinValue), + maxValue(newMaxValue), + defaultValue(newDefaultValue) {} + + /** + * If the given value is between the minimum and maximum values for this parameter, + * then the value is returned unchanged. If the given value is outside the minimum + * and maximum values for this parameter, the given value is clipped to this range + * and then returned. + * + * @param val Value to clip to minumum and maximum values + * + * @return Clipped value + */ + T BoundsCheck(T val) const { + return ParameterDefinition::BoundsCheck(val, minValue, maxValue); + } + }; + + /** + * Provides storage for minimum, maximum and default values for a parameter + * which can contain a continuous value (such as a slider), as well as methods to convert + * between the normalised and internal ranges, and clip a value to the appropriate range. + */ + template + class RangedParameter: public BaseParameter { + public: + + // Inherit the constructor + using BaseParameter::BaseParameter; + + /** + * Translates parameter values from the normalised (0 to 1) range as required + * by VSTs to the range used internally for that parameter + * + * @param val Normalised value of the parameter + * + * @return The value of the parameter in the internal range for that parameter + */ + T NormalisedToInternal(T val) const { + return val * (this->maxValue - this->minValue) + this->minValue; + } + + /** + * Translates parameter values from the range used internally for that + * parameter, to the normalised range (0 to 1) as required by VSTs. + * + * @param val Value of the parameter in the internal range + * + * @return The normalised value of the parameter + */ + T InternalToNormalised(T val) const { + return (val - this->minValue) / (this->maxValue - this->minValue); + } + }; +} diff --git a/ports-juce7/syndicate/WECore/General/UpdateChecker.h b/ports-juce7/syndicate/WECore/General/UpdateChecker.h new file mode 100644 index 00000000..c8551270 --- /dev/null +++ b/ports-juce7/syndicate/WECore/General/UpdateChecker.h @@ -0,0 +1,69 @@ +/* + * File: UpdateChecker.h + * + * Created: 05/05/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#ifndef UpdateChecker_h +#define UpdateChecker_h + +#include +#include + +class UpdateChecker { +public: + UpdateChecker() = default; + + bool checkIsLatestVersion(const char* productName, + const char* productVersion) { + + // cURL setup + CURL *curl; + CURLcode result; + curl = curl_easy_init(); + + // build the URL we'll be sending + std::string requestURL {TARGET_URL}; + requestURL.append("?product="); + requestURL.append(productName); + + std::string response; + + // setup the request + curl_easy_setopt(curl, CURLOPT_URL, requestURL.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, _writeToString); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response); + + // send the request and clean up + result = curl_easy_perform(curl); + curl_easy_cleanup(curl); + + return (productVersion == response); + } + +private: + const std::string TARGET_URL {"https://whiteelephantaudio.com/versionChecker.php"}; + + static size_t _writeToString(char* ptr, size_t size, size_t nmemb, std::string* stream) { + *stream = std::string(ptr); + + return size * nmemb; + } +}; + +#endif /* UpdateChecker_h */ diff --git a/ports-juce7/syndicate/WECore/MONSTRFilters/MONSTRParameters.h b/ports-juce7/syndicate/WECore/MONSTRFilters/MONSTRParameters.h new file mode 100644 index 00000000..c177cb25 --- /dev/null +++ b/ports-juce7/syndicate/WECore/MONSTRFilters/MONSTRParameters.h @@ -0,0 +1,54 @@ +/* + * File: MONSTRParameters.h + * + * Created: 08/11/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/ParameterDefinition.h" + +namespace WECore::MONSTR::Parameters { + constexpr bool BANDSWITCH_OFF {false}, + BANDSWITCH_ON {true}, + BANDSWITCH_DEFAULT {BANDSWITCH_ON}, + + BANDMUTED_OFF {false}, + BANDMUTED_ON {true}, + BANDMUTED_DEFAULT {BANDMUTED_OFF}, + + BANDSOLO_OFF {false}, + BANDSOLO_ON {true}, + BANDSOLO_DEFAULT {BANDSOLO_OFF}; + + // constexpr as it initialises some internal memebers + constexpr int _MAX_NUM_BANDS {6}; + constexpr int _DEFAULT_NUM_BANDS {2}; + + //@{ + /** + * A parameter which can take any float value between the ranges defined. + * The values passed on construction are in the following order: + * minimum value, + * maximum value, + * default value + */ + const ParameterDefinition::RangedParameter CROSSOVER_FREQUENCY(40, 19500, 1000); + const ParameterDefinition::RangedParameter NUM_BANDS(2, _MAX_NUM_BANDS, _DEFAULT_NUM_BANDS); + //@} +} diff --git a/ports-juce7/syndicate/WECore/RichterLFO/RichterLFO.h b/ports-juce7/syndicate/WECore/RichterLFO/RichterLFO.h new file mode 100644 index 00000000..326330eb --- /dev/null +++ b/ports-juce7/syndicate/WECore/RichterLFO/RichterLFO.h @@ -0,0 +1,417 @@ +/* + * File: RichterLFO.h + * + * Created: 18/01/2015 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "RichterParameters.h" +#include "RichterWavetables.h" +#include "WEFilters/ModulationSource.h" + +namespace WECore::Richter { + + class RichterLFOPair; + + /** + * Provides and LFO with: depth, rate, tempo sync, phase, wave shape, and phase sync + * controls, plus additional functionality to allow the depth and rate controls to be + * modulated by an external source, with internal controls of the for the depth of this + * modulation. + * + * This LFO oscillates between -1 and 1. + * + * To use, you simply need to call reset, prepareForNextBuffer, and getNextOutput + * as necessary (see their descriptions for details), and use the provided getter + * and setter methods to manipulate parameters. + */ + class RichterLFO : public ModulationSource { + + public: + + /** + * Generates the wave tables on initialsation, while running gain values + * are simply looked up from these wave tables. + * + * Also initialises parameters (that are not part of RichterLFOBase) to default values. + */ + inline RichterLFO(); + + virtual ~RichterLFO() override = default; + + friend class RichterLFOPair; + + /** @name Getter Methods */ + /** @{ */ + bool getBypassSwitch() const { return _bypassSwitch; } + bool getPhaseSyncSwitch() const { return _phaseSyncSwitch; } + bool getTempoSyncSwitch() const { return _tempoSyncSwitch; } + bool getInvertSwitch() const { return _invertSwitch; } + int getWave() const { return _wave; } + int getOutputMode() { return _outputMode; } + double getTempoNumer() const { return _tempoNumer; } + double getTempoDenom() const { return _tempoDenom; } + double getFreq() { return _rawFreq; } + double getModulatedFreqValue() const { return _modulatedFreqValue; } + double getDepth() { return _rawDepth; } + double getModulatedDepthValue() const { return _modulatedDepthValue; } + double getManualPhase() const { return _manualPhase; } + double getModulatedPhaseValue() const { return _modulatedPhaseValue; } + /** @} */ + + /** @name Setter Methods */ + /** @{ */ + void setBypassSwitch(bool val) { _bypassSwitch = val; } + void setPhaseSyncSwitch(bool val) { _phaseSyncSwitch = val; } + void setTempoSyncSwitch(bool val) { _tempoSyncSwitch = val; } + void setInvertSwitch(bool val) { _invertSwitch = val; } + inline void setWave(int val); + void setOutputMode(int val) { _outputMode = Parameters::OUTPUTMODE.BoundsCheck(val); } + void setTempoNumer(int val) { _tempoNumer = Parameters::TEMPONUMER.BoundsCheck(val); } + void setTempoDenom (int val) { _tempoDenom = Parameters::TEMPODENOM.BoundsCheck(val); } + void setFreq(double val) { _rawFreq = Parameters::FREQ.BoundsCheck(val); } + void setDepth(double val) { _rawDepth = Parameters::DEPTH.BoundsCheck(val); } + void setManualPhase(double val) { _manualPhase = Parameters::PHASE.BoundsCheck(val); } + void setSampleRate(double val) { _sampleRate = val; } + /** @} */ + + inline bool addFreqModulationSource(std::shared_ptr source); + inline bool removeFreqModulationSource(std::shared_ptr source); + inline bool setFreqModulationAmount(size_t index, double amount); + std::vector> getFreqModulationSources() const { return _freqModulationSources; } + + inline bool addDepthModulationSource(std::shared_ptr source); + inline bool removeDepthModulationSource(std::shared_ptr source); + inline bool setDepthModulationAmount(size_t index, double amount); + std::vector> getDepthModulationSources() const { return _depthModulationSources; } + + inline bool addPhaseModulationSource(std::shared_ptr source); + inline bool removePhaseModulationSource(std::shared_ptr source); + inline bool setPhaseModulationAmount(size_t index, double amount); + std::vector> getPhaseModulationSources() const { return _phaseModulationSources; } + + /** + * Prepares for processing the next buffer of samples. For example if using JUCE, you + * would call this in your processBlock() method before doing any processing. + * + * @param bpm Current bpm of the host + * @param timeInSeconds Position of the host DAW's playhead at the start of + * playback. + */ + inline void prepareForNextBuffer(double bpm, double timeInSeconds); + + RichterLFO operator= (RichterLFO& other) = delete; + RichterLFO(RichterLFO&) = delete; + + protected: + int _wave, + _indexOffset, + _outputMode; + + bool _bypassSwitch, + _tempoSyncSwitch, + _phaseSyncSwitch, + _invertSwitch, + _needsSeekOffsetCalc; + + double _tempoNumer, + _tempoDenom, + _rawFreq, + _modulatedFreqValue, + _rawDepth, + _modulatedDepthValue, + _manualPhase, + _modulatedPhaseValue, + _sampleRate, + _bpm, + _wavetablePosition; + + const double* _waveArrayPointer; + + std::vector> _freqModulationSources, + _depthModulationSources, + _phaseModulationSources; + + /** + * Calculates the phase offset to be applied to the oscillator, including any + * offset required by the phase sync and any offset applied by the user. + * + * @param timeInSeconds Position of the host DAW's playhead at the start of + * playback. + */ + inline void _calcPhaseOffset(double timeInSeconds); + + /** + * Calculates the frequency of the oscillator. Will use either the frequency + * or tempoNumer/tempoDenom depending on whether tempo sync is enabled. + * + * @param modAmount The gain output from the modulation oscillator. + */ + inline double _calcFreq(double modAmount); + + /** + * Calculates the current index of the oscillator in its wavetable and increments the number + * of samples processed. + * + * @param freq The absolute frequency of the LFO, including tempo sync or modulation. + * + * @return The value in the wavetable at the current index. + * + */ + inline double _calcLFOValue(double freq, double phaseModValue); + + /** + * Returns the next output of the LFO. + * + * Note: Calling this method will advance the oscillators internal counters by one + * sample. Calling this method will return a different value each time. + * + * @return The value of the LFO's output at this moment, a value between -1 and 1. + */ + inline double _getNextOutputImpl(double inSample) override; + + /** + * Resets internal counters including indexOffset and currentScale. + */ + virtual inline void _resetImpl() override; + }; + + RichterLFO::RichterLFO() : _wave(Parameters::WAVE.defaultValue), + _indexOffset(0), + _outputMode(Parameters::OUTPUTMODE.defaultValue), + _bypassSwitch(Parameters::LFOSWITCH_DEFAULT), + _tempoSyncSwitch(Parameters::TEMPOSYNC_DEFAULT), + _phaseSyncSwitch(Parameters::PHASESYNC_DEFAULT), + _invertSwitch(Parameters::INVERT_DEFAULT), + _needsSeekOffsetCalc(true), + _tempoNumer(Parameters::TEMPONUMER.defaultValue), + _tempoDenom(Parameters::TEMPODENOM.defaultValue), + _rawFreq(Parameters::FREQ.defaultValue), + _modulatedFreqValue(0), + _rawDepth(Parameters::DEPTH.defaultValue), + _modulatedDepthValue(0), + _manualPhase(Parameters::PHASE.defaultValue), + _modulatedPhaseValue(0), + _sampleRate(44100), + _bpm(0), + _wavetablePosition(0), + _waveArrayPointer(Wavetables::getInstance()->getSine()) { + } + + void RichterLFO::setWave(int val) { + _wave = Parameters::WAVE.BoundsCheck(val); + + if (_wave == Parameters::WAVE.SINE) { + _waveArrayPointer = Wavetables::getInstance()->getSine(); + } else if (_wave == Parameters::WAVE.SQUARE) { + _waveArrayPointer = Wavetables::getInstance()->getSquare(); + } else if (_wave == Parameters::WAVE.SAW) { + _waveArrayPointer = Wavetables::getInstance()->getSaw(); + } else if (_wave == Parameters::WAVE.SIDECHAIN) { + _waveArrayPointer = Wavetables::getInstance()->getSidechain(); + } + } + + bool RichterLFO::addFreqModulationSource(std::shared_ptr source) { + // Check if the source is already in the list + for (const ModulationSourceWrapper& existingSource : _freqModulationSources) { + if (existingSource.source == source) { + return false; + } + } + + _freqModulationSources.push_back({source, 0}); + return true; + } + + bool RichterLFO::removeFreqModulationSource(std::shared_ptr source) { + for (auto it = _freqModulationSources.begin(); it != _freqModulationSources.end(); it++) { + if ((*it).source == source) { + _freqModulationSources.erase(it); + return true; + } + } + + return false; + } + + bool RichterLFO::setFreqModulationAmount(size_t index, double amount) { + if (index >= _freqModulationSources.size()) { + return false; + } + + _freqModulationSources[index].amount = amount; + return true; + } + + bool RichterLFO::addDepthModulationSource(std::shared_ptr source) { + // Check if the source is already in the list + for (const ModulationSourceWrapper& existingSource : _depthModulationSources) { + if (existingSource.source == source) { + return false; + } + } + + _depthModulationSources.push_back({source, 0}); + return true; + } + + bool RichterLFO::removeDepthModulationSource(std::shared_ptr source) { + for (auto it = _depthModulationSources.begin(); it != _depthModulationSources.end(); it++) { + if ((*it).source == source) { + _depthModulationSources.erase(it); + return true; + } + } + + return false; + } + + bool RichterLFO::setDepthModulationAmount(size_t index, double amount) { + if (index >= _depthModulationSources.size()) { + return false; + } + + _depthModulationSources[index].amount = amount; + return true; + } + + bool RichterLFO::addPhaseModulationSource(std::shared_ptr source) { + // Check if the source is already in the list + for (const ModulationSourceWrapper& existingSource : _phaseModulationSources) { + if (existingSource.source == source) { + return false; + } + } + + _phaseModulationSources.push_back({source, 0}); + return true; + } + + bool RichterLFO::removePhaseModulationSource(std::shared_ptr source) { + for (auto it = _phaseModulationSources.begin(); it != _phaseModulationSources.end(); it++) { + if ((*it).source == source) { + _phaseModulationSources.erase(it); + return true; + } + } + + return false; + } + + bool RichterLFO::setPhaseModulationAmount(size_t index, double amount) { + if (index >= _phaseModulationSources.size()) { + return false; + } + + _phaseModulationSources[index].amount = amount; + return true; + } + + void RichterLFO::prepareForNextBuffer(double bpm, + double timeInSeconds) { + _bpm = bpm; + _calcPhaseOffset(timeInSeconds); + } + + void RichterLFO::_resetImpl() { + _needsSeekOffsetCalc = true; + _indexOffset = 0; + _wavetablePosition = 0; + } + + void RichterLFO::_calcPhaseOffset(double timeInSeconds) { + + // The phase offset applied by the playhead's start position needs to be calculated only + // when playback initially starts, and not for any subsequent buffers until playback stops + // and starts again + static int seekIndexOffset {0}; + if (_needsSeekOffsetCalc) { + const double waveLength {1 / _calcFreq(0)}; + const double waveTimePosition {std::fmod(timeInSeconds, waveLength)}; + + seekIndexOffset = static_cast((waveTimePosition / waveLength) * Wavetables::SIZE); + _needsSeekOffsetCalc = false; + } + + if (_phaseSyncSwitch) { + _indexOffset = seekIndexOffset; + } else { + _indexOffset = 0; + } + } + + double RichterLFO::_calcFreq(double modAmount) { + // Calculate the frequency based on whether tempo sync is active + double freq {0}; + + if (_tempoSyncSwitch) { + freq = (_bpm / 60) * (_tempoDenom / _tempoNumer); + } else { + freq = _rawFreq + ((Parameters::FREQ.maxValue / 2) * modAmount); + } + + freq = Parameters::FREQ.BoundsCheck(freq); + _modulatedFreqValue = freq; + + return freq; + } + + double RichterLFO::_calcLFOValue(double freq, double phaseModValue) { + const double samplesPerTremoloCycle {_sampleRate / freq}; + const double scale {Wavetables::SIZE / samplesPerTremoloCycle}; + + // Calculate the current position within the wave table + _wavetablePosition = std::fmod(_wavetablePosition + scale, Wavetables::SIZE); + + _modulatedPhaseValue = _manualPhase + (phaseModValue * Parameters::PHASE.maxValue); + + const int phaseIndexOffset { + static_cast((_modulatedPhaseValue / Parameters::PHASE.maxValue) * Wavetables::SIZE) + }; + + const int index {static_cast(_wavetablePosition)}; + + const double wavetableValue {_waveArrayPointer[(index + _indexOffset + phaseIndexOffset) % Wavetables::SIZE]}; + + return _outputMode == Parameters::OUTPUTMODE.BIPOLAR ? wavetableValue : (wavetableValue + 1); + } + + double RichterLFO::_getNextOutputImpl(double /*inSample*/) { + // Get the mod amount to use, divide by 2 to reduce range to -0.5:0.5 + const double freqModValue {calcModValue(_freqModulationSources) / 2}; + const double depthModValue {calcModValue(_depthModulationSources) / 2}; + const double phaseModValue {calcModValue(_phaseModulationSources) / 2}; + + const double lfoValue {_calcLFOValue(_calcFreq(freqModValue), phaseModValue)}; + + // Calculate the depth value after modulation + const double depth {Parameters::DEPTH.BoundsCheck( + _rawDepth + (Parameters::DEPTH.maxValue * depthModValue) + )}; + _modulatedDepthValue = depth; + + if (_bypassSwitch) { + // Produce a value in the range -1:1 (or 0:2), invert if needed + return (lfoValue * depth) * (_invertSwitch ? -1 : 1); + } else { + return 0; + } + } +} diff --git a/ports-juce7/syndicate/WECore/RichterLFO/RichterLFOPair.h b/ports-juce7/syndicate/WECore/RichterLFO/RichterLFOPair.h new file mode 100644 index 00000000..0150be7e --- /dev/null +++ b/ports-juce7/syndicate/WECore/RichterLFO/RichterLFOPair.h @@ -0,0 +1,145 @@ +/* + * File: RichterLFOPair.h + * + * Created: 18/05/2015 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "RichterLFO.h" +#include "WEFilters/ModulationSource.h" + +namespace WECore::Richter { + /** + * + * A convenience class that allows a simple implementation of an LFO that has + * been paired with a MOD oscillator to modulate its depth and frequency. If you use + * this class, you will never need to interact directly with either of the contained + * LFOs for anything other than getting or setting parameter values. + * + * This class has been created as the LFO relies on the MOD being ready before + * it can perform certain operations, which means there are method calls to + * each oscillator which must be interleaved carefully. + * + * The following example shows how to set up this object for audio at 120bpm and 44.1kHz: + * @code + * RichterLFOPair lfoPair; + * lfoPair.prepareForNextBuffer(120, 0, 44100); + * @endcode + * + * Then the class can be used to process a buffer as follows: + * (where numSamples is the size of the buffer) + * @code + * for (size_t iii {0}; iii < numSamples; iii++) { + * buffer[iii] = buffer[iii] * lfoPair.getNextOutput(); + * } + * @endcode + * + * getNextOutput must be called once for each sample. Even if there is a sample in the buffer + * which you do not wish to apply processing to, getNextOutput must still be called otherwise + * subsequent samples will have the wrong gain calculation applied. + */ + + class RichterLFOPair : public ModulationSource { + public: + inline RichterLFOPair(); + virtual ~RichterLFOPair() override = default; + RichterLFOPair operator= (RichterLFOPair& other) = delete; + RichterLFOPair(RichterLFOPair& other) = delete; + + /** + * Prepares for processing the next buffer of samples. For example if using JUCE, you + * would call this in your processBlock() method before doing any processing. + * + * This calls various protected methods of each of the oscillators in a specific order + * to ensure calculations are done correctly. + * + * @param bpm Current bpm of the host. + * @param timeInSeconds Position of the host DAW's playhead at the start of + * playback. + */ + inline void prepareForNextBuffer(double bpm, double timeInSeconds); + + /** + * Sets the sample rate for both LFOs. + * + * @param sampleRate Current sample rate of the host + */ + inline void setSampleRate(double sampleRate); + + RichterLFO LFO; + std::shared_ptr MOD; + + private: + /** + * Returns a gain value which is intended to be multiplied with a single sample to apply the + * tremolo effect. + * + * Note: Calling this method will advance the oscillators internal counters by one + * sample. Calling this method will return a different value each time. + * + * @return The value of the RichterLFO's output at this moment, a value between 0 and 1. + */ + inline double _getNextOutputImpl(double inSample) override; + + /** + * Call each oscillator's reset method. + */ + inline void _resetImpl() override; + }; + + RichterLFOPair::RichterLFOPair() { + MOD = std::make_shared(); + LFO.addFreqModulationSource(MOD); + LFO.addDepthModulationSource(MOD); + } + + void RichterLFOPair::prepareForNextBuffer(double bpm, + double timeInSeconds) { + LFO.prepareForNextBuffer(bpm, timeInSeconds); + MOD->prepareForNextBuffer(bpm, timeInSeconds); + } + + void RichterLFOPair::setSampleRate(double sampleRate) { + LFO.setSampleRate(sampleRate); + MOD->setSampleRate(sampleRate); + } + + void RichterLFOPair::_resetImpl() { + LFO.reset(); + MOD->reset(); + } + + double RichterLFOPair::_getNextOutputImpl(double /*inSample*/) { + double retVal {1}; + + // Advance the modulation LFO state + MOD->getNextOutput(0); + + // Always call getNextOutput regardless of bypassed state + const double tempGain {LFO.getNextOutput(0)}; + + if (LFO.getBypassSwitch()) { + // The output of the LFO is a value in the range -1:1, we need to convert this into a + // gain in the range 0:1 and make sure the value is 1 when the depth is 0 + retVal = (tempGain / 2) + (2 - LFO.getDepth()) / 2; + } + + return retVal; + } +} diff --git a/ports-juce7/syndicate/WECore/RichterLFO/RichterParameters.h b/ports-juce7/syndicate/WECore/RichterLFO/RichterParameters.h new file mode 100644 index 00000000..1d276ec0 --- /dev/null +++ b/ports-juce7/syndicate/WECore/RichterLFO/RichterParameters.h @@ -0,0 +1,76 @@ +/* + * File: RichterParameters.h + * + * Created: 25/09/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/ParameterDefinition.h" +#include "RichterWavetables.h" + +namespace WECore::Richter::Parameters { + + class WaveParameter : public ParameterDefinition::BaseParameter { + public: + WaveParameter() : ParameterDefinition::BaseParameter(SINE, SIDECHAIN, SINE) {} + + static constexpr int SINE {1}, + SQUARE {2}, + SAW {3}, + SIDECHAIN {4}; + }; + + const WaveParameter WAVE; + + class OutputModeParameter : public ParameterDefinition::BaseParameter { + public: + OutputModeParameter() : ParameterDefinition::BaseParameter(UNIPOLAR, BIPOLAR, BIPOLAR) {} + + static constexpr int UNIPOLAR {1}, + BIPOLAR {2}; + }; + + const OutputModeParameter OUTPUTMODE; + + const ParameterDefinition::RangedParameter TEMPONUMER(1, 32, 1), + TEMPODENOM(1, 32, 1); + + const ParameterDefinition::RangedParameter DEPTH(0, 1, 0.5), + DEPTHMOD(0, 1, 0), + FREQ(0, 20, 2), + FREQMOD(0, 1, 0), + PHASE(0, 360, 0); + + constexpr bool LFOSWITCH_OFF = false, + LFOSWITCH_ON = true, + LFOSWITCH_DEFAULT = LFOSWITCH_OFF, + + TEMPOSYNC_OFF = false, + TEMPOSYNC_ON = true, + TEMPOSYNC_DEFAULT = TEMPOSYNC_OFF, + + PHASESYNC_OFF = false, + PHASESYNC_ON = true, + PHASESYNC_DEFAULT = PHASESYNC_ON, + + INVERT_OFF = false, + INVERT_ON = true, + INVERT_DEFAULT = INVERT_OFF; + +} diff --git a/ports-juce7/syndicate/WECore/RichterLFO/RichterWavetables.h b/ports-juce7/syndicate/WECore/RichterLFO/RichterWavetables.h new file mode 100644 index 00000000..6126624b --- /dev/null +++ b/ports-juce7/syndicate/WECore/RichterLFO/RichterWavetables.h @@ -0,0 +1,129 @@ +/* + * File: RichterWavetables.h + * + * Created: 13/07/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/CoreMath.h" + +namespace WECore::Richter { + + /** + * Singleton which contains the wavetables used by the Richter LFOs. + */ + class Wavetables { + public: + + /** + * Number of samples in each of the wavetables. + */ + static constexpr int SIZE {2000}; + + static const Wavetables* getInstance() { + static Wavetables instance; + return &instance; + } + + const double* getSine() const { return _sineTable; } + const double* getSquare() const { return _squareTable; } + const double* getSaw() const { return _sawTable; } + const double* getSidechain() const { return _sidechainTable; } + + private: + double _sineTable[SIZE]; + double _squareTable[SIZE]; + double _sawTable[SIZE]; + double _sidechainTable[SIZE]; + + /** + * Populates the available wavetables + */ + inline Wavetables(); + }; + + Wavetables::Wavetables() { + + // Sine wavetable + for (int idx = 0; idx < Wavetables::SIZE; idx++) { + const double radians {idx * CoreMath::DOUBLE_TAU / Wavetables::SIZE}; + + // Just a conventional sine + _sineTable[idx] = sin(radians); + } + + // Square wavetable + for (int idx = 0; idx < Wavetables::SIZE; idx++) { + const double radians {(idx * CoreMath::DOUBLE_TAU / Wavetables::SIZE) + 0.32}; + + // The fourier series for a square wave produces a very sharp square with some overshoot + // and ripple, so this actually uses slightly lower amplitudes for each harmonic than + // would normally be used. + // + // Because the harmonics are lower amplitude, it needs to be scaled up by 1.2 to reach + // a range of -1 to +1 + _squareTable[idx] = + ( + sin(radians) + + (0.3/1.0) * sin(3 * radians) + + (0.3/2.0) * sin(5 * radians) + + (0.3/4.0) * sin(7 * radians) + + (0.3/8.0) * sin(9 * radians) + + (0.3/16.0) * sin(11 * radians) + + (0.3/32.0) * sin(13 * radians) + ) * 1.2; + } + + // Saw wavetable + for (int idx = 0; idx < Wavetables::SIZE; idx++) { + const double radians {(idx * CoreMath::DOUBLE_TAU / Wavetables::SIZE) + CoreMath::DOUBLE_PI}; + + // Conventional fourier series for a saw wave, scaled to fit -1 to 1 + _sawTable[idx] = + ( + sin(radians) - + (1.0/2.0) * sin(2 * radians) + + (1.0/3.0) * sin(3 * radians) - + (1.0/4.0) * sin(4 * radians) + + (1.0/6.0) * sin(5 * radians) - + (1.0/8.0) * sin(6 * radians) + + (1.0/12.0) * sin(7 * radians) - + (1.0/16.0) * sin(8 * radians) + + (1.0/24.0) * sin(9 * radians) - + (1.0/32.0) * sin(10 * radians) + + (1.0/48.0) * sin(11 * radians) - + (1.0/64.0) * sin(12 * radians) + + (1.0/96.0) * sin(13 * radians) - + (1.0/128.0) * sin(14 * radians) + ) * (2.0 / 3.0); + } + + // Sidechain wavetable + for (int idx = 0; idx < Wavetables::SIZE; idx++) { + const double radians {idx * CoreMath::DOUBLE_TAU / Wavetables::SIZE}; + + _sidechainTable[idx] = + ( + radians < 0.4497 ? + -2 * sin(pow(0.2 * radians - 0.8245, 6) * 10) + 1 : + -2 * sin(pow(0.15 * radians - 0.802, 6) * 10) + 1 + ); + } + } +} diff --git a/ports-juce7/syndicate/WECore/RichterLFO/Tests/RichterLFOPairTests.cpp b/ports-juce7/syndicate/WECore/RichterLFO/Tests/RichterLFOPairTests.cpp new file mode 100644 index 00000000..e40a7b28 --- /dev/null +++ b/ports-juce7/syndicate/WECore/RichterLFO/Tests/RichterLFOPairTests.cpp @@ -0,0 +1,187 @@ +/* + * File: RichterLFOPairTests.cpp + * + * Created: 26/12/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "RichterLFO/RichterLFOPair.h" + +SCENARIO("RichterLFOPair: Parameters can be set and retrieved correctly") { + GIVEN("A new RichterLFOPair object") { + WECore::Richter::RichterLFOPair mLFOPair; + + WHEN("Nothing is changed") { + THEN("Parameters have their default values") { + CHECK(mLFOPair.LFO.getBypassSwitch() == false); + CHECK(mLFOPair.LFO.getPhaseSyncSwitch() == true); + CHECK(mLFOPair.LFO.getTempoSyncSwitch() == false); + CHECK(mLFOPair.LFO.getInvertSwitch() == false); + CHECK(mLFOPair.LFO.getWave() == 1); + CHECK(mLFOPair.LFO.getOutputMode() == 2); + CHECK(mLFOPair.LFO.getDepth() == Approx(0.5)); + CHECK(mLFOPair.LFO.getFreq() == Approx(2.0)); + CHECK(mLFOPair.LFO.getManualPhase() == Approx(0.0)); + CHECK(mLFOPair.LFO.getTempoNumer() == Approx(1.0)); + CHECK(mLFOPair.LFO.getTempoDenom() == Approx(1.0)); + + CHECK(mLFOPair.MOD->getBypassSwitch() == false); + CHECK(mLFOPair.MOD->getPhaseSyncSwitch() == true); + CHECK(mLFOPair.MOD->getTempoSyncSwitch() == false); + CHECK(mLFOPair.MOD->getInvertSwitch() == false); + CHECK(mLFOPair.MOD->getWave() == 1); + CHECK(mLFOPair.MOD->getOutputMode() == 2); + CHECK(mLFOPair.MOD->getDepth() == Approx(0.5)); + CHECK(mLFOPair.MOD->getFreq() == Approx(2.0)); + CHECK(mLFOPair.MOD->getManualPhase() == Approx(0.0)); + CHECK(mLFOPair.MOD->getTempoNumer() == Approx(1.0)); + CHECK(mLFOPair.MOD->getTempoDenom() == Approx(1.0)); + } + } + + WHEN("All parameters are changed to unique values") { + mLFOPair.LFO.setBypassSwitch(true); + mLFOPair.LFO.setPhaseSyncSwitch(false); + mLFOPair.LFO.setTempoSyncSwitch(true); + mLFOPair.LFO.setInvertSwitch(true); + mLFOPair.LFO.setWave(2); + mLFOPair.LFO.setOutputMode(1); + mLFOPair.LFO.setDepth(0.1); + mLFOPair.LFO.setFreq(3); + mLFOPair.LFO.setManualPhase(0.5); + mLFOPair.LFO.setTempoNumer(2); + mLFOPair.LFO.setTempoDenom(3); + + mLFOPair.MOD->setBypassSwitch(true); + mLFOPair.MOD->setPhaseSyncSwitch(true); + mLFOPair.MOD->setTempoSyncSwitch(true); + mLFOPair.MOD->setInvertSwitch(true); + mLFOPair.MOD->setWave(3); + mLFOPair.MOD->setOutputMode(1); + mLFOPair.MOD->setDepth(0.5); + mLFOPair.MOD->setFreq(6); + mLFOPair.MOD->setManualPhase(0.7); + mLFOPair.MOD->setTempoNumer(3); + mLFOPair.MOD->setTempoDenom(4); + + THEN("They all get their correct unique values") { + CHECK(mLFOPair.LFO.getBypassSwitch() == true); + CHECK(mLFOPair.LFO.getPhaseSyncSwitch() == false); + CHECK(mLFOPair.LFO.getTempoSyncSwitch() == true); + CHECK(mLFOPair.LFO.getInvertSwitch() == true); + CHECK(mLFOPair.LFO.getWave() == 2); + CHECK(mLFOPair.LFO.getOutputMode() == 1); + CHECK(mLFOPair.LFO.getDepth() == Approx(0.1)); + CHECK(mLFOPair.LFO.getFreq() == Approx(3.0)); + CHECK(mLFOPair.LFO.getManualPhase() == Approx(0.5)); + CHECK(mLFOPair.LFO.getTempoNumer() == Approx(2.0)); + CHECK(mLFOPair.LFO.getTempoDenom() == Approx(3.0)); + + CHECK(mLFOPair.MOD->getBypassSwitch() == true); + CHECK(mLFOPair.MOD->getPhaseSyncSwitch() == true); + CHECK(mLFOPair.MOD->getTempoSyncSwitch() == true); + CHECK(mLFOPair.MOD->getInvertSwitch() == true); + CHECK(mLFOPair.MOD->getWave() == 3); + CHECK(mLFOPair.MOD->getOutputMode() == 1); + CHECK(mLFOPair.MOD->getDepth() == Approx(0.5)); + CHECK(mLFOPair.MOD->getFreq() == Approx(6.0)); + CHECK(mLFOPair.MOD->getManualPhase() == Approx(0.7)); + CHECK(mLFOPair.MOD->getTempoNumer() == Approx(3.0)); + CHECK(mLFOPair.MOD->getTempoDenom() == Approx(4.0)); + } + } + } +} + +SCENARIO("RichterLFOPair: Parameters enforce their bounds correctly") { + GIVEN("A new RichterLFOPair object") { + WECore::Richter::RichterLFOPair mLFOPair; + + WHEN("All parameter values are too low") { + mLFOPair.LFO.setWave(-5); + mLFOPair.LFO.setOutputMode(-5); + mLFOPair.LFO.setDepth(-5); + mLFOPair.LFO.setFreq(-5); + mLFOPair.LFO.setManualPhase(-5); + mLFOPair.LFO.setTempoNumer(-5); + mLFOPair.LFO.setTempoDenom(-5); + + mLFOPair.MOD->setWave(-5); + mLFOPair.MOD->setOutputMode(-5); + mLFOPair.MOD->setDepth(-5); + mLFOPair.MOD->setFreq(-5); + mLFOPair.MOD->setManualPhase(-5); + mLFOPair.MOD->setTempoNumer(-5); + mLFOPair.MOD->setTempoDenom(-5); + + THEN("Parameters enforce their lower bounds") { + CHECK(mLFOPair.LFO.getWave() == 1); + CHECK(mLFOPair.LFO.getOutputMode() == 1); + CHECK(mLFOPair.LFO.getDepth() == Approx(0.0)); + CHECK(mLFOPair.LFO.getFreq() == Approx(0.0)); + CHECK(mLFOPair.LFO.getManualPhase() == Approx(0.0)); + CHECK(mLFOPair.LFO.getTempoNumer() == Approx(1.0)); + CHECK(mLFOPair.LFO.getTempoDenom() == Approx(1.0)); + + CHECK(mLFOPair.MOD->getWave() == 1); + CHECK(mLFOPair.MOD->getOutputMode() == 1); + CHECK(mLFOPair.MOD->getDepth() == Approx(0.0)); + CHECK(mLFOPair.MOD->getFreq() == Approx(0.0)); + CHECK(mLFOPair.MOD->getManualPhase() == Approx(0.0)); + CHECK(mLFOPair.MOD->getTempoNumer() == Approx(1.0)); + CHECK(mLFOPair.MOD->getTempoDenom() == Approx(1.0)); + } + } + + WHEN("All parameter values are too high") { + mLFOPair.LFO.setWave(100); + mLFOPair.LFO.setOutputMode(100); + mLFOPair.LFO.setDepth(100); + mLFOPair.LFO.setFreq(100); + mLFOPair.LFO.setManualPhase(10000); + mLFOPair.LFO.setTempoNumer(100); + mLFOPair.LFO.setTempoDenom(100); + + mLFOPair.MOD->setWave(100); + mLFOPair.MOD->setOutputMode(100); + mLFOPair.MOD->setDepth(100); + mLFOPair.MOD->setFreq(100); + mLFOPair.MOD->setManualPhase(10000); + mLFOPair.MOD->setTempoNumer(100); + mLFOPair.MOD->setTempoDenom(100); + + THEN("Parameters enforce their upper bounds") { + CHECK(mLFOPair.LFO.getWave() == 4); + CHECK(mLFOPair.LFO.getOutputMode() == 2); + CHECK(mLFOPair.LFO.getDepth() == Approx(1.0)); + CHECK(mLFOPair.LFO.getFreq() == Approx(20.0)); + CHECK(mLFOPair.LFO.getManualPhase() == Approx(360.0)); + CHECK(mLFOPair.LFO.getTempoNumer() == Approx(32.0)); + CHECK(mLFOPair.LFO.getTempoDenom() == Approx(32.0)); + + CHECK(mLFOPair.MOD->getWave() == 4); + CHECK(mLFOPair.MOD->getOutputMode() == 2); + CHECK(mLFOPair.MOD->getDepth() == Approx(1.0)); + CHECK(mLFOPair.MOD->getFreq() == Approx(20.0)); + CHECK(mLFOPair.MOD->getManualPhase() == Approx(360.0)); + CHECK(mLFOPair.MOD->getTempoNumer() == Approx(32.0)); + CHECK(mLFOPair.MOD->getTempoDenom() == Approx(32.0)); + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/RichterLFO/UI/RichterWaveViewer.h b/ports-juce7/syndicate/WECore/RichterLFO/UI/RichterWaveViewer.h new file mode 100644 index 00000000..7f8d54d0 --- /dev/null +++ b/ports-juce7/syndicate/WECore/RichterLFO/UI/RichterWaveViewer.h @@ -0,0 +1,99 @@ +/* + * File: RichterWaveViewer.h + * + * Created: 16/07/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 the WECore. If not, see . + */ + +#pragma once + +#include "JuceHeader.h" +#include "RichterLFO/RichterParameters.h" +#include "RichterLFO/RichterWavetables.h" + +namespace WECore::Richter { + + class WaveViewer : public juce::Component, + public juce::SettableTooltipClient { + public: + WaveViewer() : _waveArrayPointer(nullptr), _depth(0), _phaseShift(0), _isInverted(false) {} + + inline void setWave(const double* pointer, double depth, double phaseShift, bool isInverted); + + inline virtual void paint(juce::Graphics& g); + + void stop() { _waveArrayPointer = nullptr; } + + enum ColourIds + { + highlightColourId = 0x1201201 + }; + + private: + const double* _waveArrayPointer; + double _depth; + double _phaseShift; + bool _isInverted; + }; + + void WaveViewer::setWave(const double* pointer, double depth, double phaseShift, bool isInverted) { + _waveArrayPointer = pointer; + _depth = depth; + _phaseShift = phaseShift; + _isInverted = isInverted; + } + + void WaveViewer::paint(juce::Graphics &g) { + + // Down sample the wave array + constexpr int NUM_SAMPLES {25}; + constexpr float SCALE {0.4}; + constexpr float MARGIN { (1 - SCALE) / 2 }; + const float INCREMENT {static_cast(Wavetables::SIZE) / NUM_SAMPLES}; + + if (_waveArrayPointer != nullptr) { + juce::Path p; + + for (size_t idx {0}; idx < NUM_SAMPLES; idx++) { + // Calculate the index of the sample accounting for downsampling and phase shift + const int phaseIndexOffset { + static_cast((_phaseShift / Parameters::PHASE.maxValue) * Wavetables::SIZE) + }; + const int sampleIdx {( + (static_cast(idx * INCREMENT + phaseIndexOffset) % Wavetables::SIZE) + )}; + + // Get the sample for this value + const double sample {_waveArrayPointer[sampleIdx] * _depth * (_isInverted ? -1 : 1)}; + + // Invert the wave and scale to the height of this component + const double sampleX {(static_cast(idx) / NUM_SAMPLES) * getWidth()}; + const double sampleY = (0.5 - sample) * getHeight() * SCALE + getHeight() * MARGIN; + + // Add it to the path + if (idx == 0) { + p.startNewSubPath(0, sampleY); + } else { + p.lineTo(sampleX, sampleY); + } + } + + g.setColour(findColour(highlightColourId)); + g.strokePath(p, juce::PathStrokeType(3.0f)); + } + } +} diff --git a/ports-juce7/syndicate/WECore/SongbirdFilters/Formant.h b/ports-juce7/syndicate/WECore/SongbirdFilters/Formant.h new file mode 100644 index 00000000..f0466abd --- /dev/null +++ b/ports-juce7/syndicate/WECore/SongbirdFilters/Formant.h @@ -0,0 +1,39 @@ +/* + * File: Formant.h + * + * Created: 16/07/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +namespace WECore::Songbird { + /** + * Simple class to hold the data about an individual formant peak. + */ + class Formant { + public: + Formant() : frequency(0), gaindB(0) {} + + Formant(double newFreq, + double newGaindB) : frequency(newFreq), + gaindB(newGaindB) {} + + double frequency; + double gaindB; + }; +} diff --git a/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFilterModule.h b/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFilterModule.h new file mode 100644 index 00000000..d4b788f5 --- /dev/null +++ b/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFilterModule.h @@ -0,0 +1,504 @@ +/* + * File: SongbirdFilterModule.h + * + * Created: 12/06/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include +#include + +#include "SongbirdFormantFilter.h" +#include "SongbirdFiltersParameters.h" +#include "WEFilters/ModulationSource.h" + +namespace WECore::Songbird { + /** + * The number of formants (bandpass filters) which are used in a single vowel. + */ + constexpr int NUM_FORMANTS_PER_VOWEL {2}; + + /** + * The number of vowels supported. + */ + constexpr int NUM_VOWELS {5}; + + /** + * The number of formants that are used for the "air" filters that add brightness. + */ + constexpr int NUM_AIR_FORMANTS {2}; + + /** + * A type to make refering to a group of formants easier. + */ + typedef std::array Vowel; + + /** + * A filter module providing five different vowel sounds, any two of which can be selected + * simulaneously and blended between. + * + * Also provides an "air" function which is a pair of fixed frequency formant filters at higher + * frequency that can be blended into the output. + * + * To use this class, simply call reset, and the process methods as necessary, using the provided + * getter and setter methods to manipulate parameters. + * + * You must call setSampleRate before beginning any processing as the default sample rate might not + * be the one you want. + * + * Internally relies on the parameters provided in SongbirdFiltersParameters.h + * + * @see SongbirdFormantFilter - SongbirdFilterModule is composed of two pairs of + * SongbirdFormantFilters (pairs to allow stereo processing), each + * pair is assigned one of the five supported vowels at any time + * + * A SongbirdFilterModule can be created and used to process a buffer as follows: + * @code + * SongbirdFilterModule filter; + * filter.setSampleRate(44100); + * filter.setVowel1(VOWEL.VOWEL_O); + * filter.setVowel2(VOWEL.VOWEL_I); + * ... + * set any other parameters you need + * ... + * + * filter.Process2in2out(leftSamples, rightSamples, numSamples); + * @endcode + */ + template + class SongbirdFilterModule { + static_assert(std::is_floating_point::value, + "Must be provided with a floating point template type"); + + public: + /** + * Does some basic setup and defaulting of parameters, though do not rely on this + * to sensibly default all parameters. + */ + SongbirdFilterModule() : _vowel1(Parameters::VOWEL.VOWEL_A), + _vowel2(Parameters::VOWEL.VOWEL_E), + _filterPosition(Parameters::FILTER_POSITION.defaultValue), + _sampleRate(44100), + _mix(Parameters::MIX.defaultValue), + _airGain(Parameters::AIR_GAIN.defaultValue), + _outputGain(Parameters::OUTPUTGAIN.defaultValue), + _modMode(Parameters::MODMODE_DEFAULT) { + + // initialise the filters to some default values + setVowel1(_vowel1); + setVowel2(_vowel2); + + // Set up the air filters + const std::array airFormants { + Formant(2700, -15), Formant(3500, -25) + }; + + _filterAirLeft.setFormants(airFormants); + _filterAirRight.setFormants(airFormants); + } + + virtual ~SongbirdFilterModule() {} + + /** @name Setter Methods */ + /** @{ */ + + /** + * Sets the vowel sound that should be created by filter 1 using one of + * the built in Vowel objects stored in this class. + * + * @param[in] val Value that should be used for Vowel 1 + * + * @see VowelParameter for valid values + */ + inline void setVowel1(int val); + + /** + * Sets the vowel sound that should be created by filter 2. + * + * @param[in] val Value that should be used for Vowel 2 + * + * @see VowelParameter for valid values + */ + inline void setVowel2(int val); + + /** + * Sets the position between the two filters that have been selected. + * + * @param[in] val Filter position to use. + * + * @see FILTER_POSITION for valid values + * @see modMode which the effect of this parameter is dependent on + + */ + void setFilterPosition(double val) { _filterPosition = Parameters::FILTER_POSITION.BoundsCheck(val); } + + /** + * Set the sample rate that the filters expect of the audio which will be processed. + * + * @param[in] val The sample rate to set the filters to + */ + inline void setSampleRate(double val); + + /** + * Sets the dry/wet mix level. + * Lowest value = completely dry, unprocessed signal, no filtering applied. + * Highest value = completely wet signal, no unprocessed audio survives. + * + * @param[in] val Mix value that should be used + * + * @see MIX for valid values + */ + void setMix(double val) { _mix = Parameters::MIX.BoundsCheck(val); } + + /** + * Sets the output gain. + * + * @param[in] val The output gain that should be used + * + * @see OUTPUTGAIN for valid values + */ + void setOutputGain(double val) { _outputGain = Parameters::OUTPUTGAIN.BoundsCheck(val); } + + /** + * Sets the modulation mode to apply to the filters. + * + * @param[in] val Chooses the modulation mode + */ + void setModMode(bool val) { _modMode = val; } + + /** + * Sets the level of the upper formant filter peaks. + * + * @param[in] val Gain value that should be used + * + * @see AIR_GAIN for valid values + */ + void setAirGain(double val) { _airGain = Parameters::AIR_GAIN.BoundsCheck(val); } + + /** + * Sets the modulation source that will be used to modulate the filter position. + * + * @param[in] val The modulation source to use + */ + void setModulationSource(std::shared_ptr> val) { _modulationSource = val; } + + /** @} */ + + /** + * Resets all filters. + * Call this whenever the audio stream is interrupted (ie. the playhead is moved) + */ + inline void reset(); + + /** @name Getter Methods */ + /** @{ */ + + /** + * @see setVowel1 + */ + int getVowel1() const { return _vowel1; } + + /** + * @see setVowel2 + */ + int getVowel2() const { return _vowel2; } + + /** + * Return a vowel object describing one of the built in vowels. + * + * @see VowelParameter for valid values + */ + inline Vowel getVowelDescription(int val) const; + + /** + * @see setFilterPosition + */ + double getFilterPosition() const { return _filterPosition; } + + /** + * @see setMix + */ + double getMix() const { return _mix; } + + /** + * @see modMode + */ + bool getModMode() const { return _modMode; } + + /** + * @see setAirGain + */ + double getAirGain() const { return _airGain; } + + /** + * @see setOutputGain + */ + double getOutputGain() const { return _outputGain; } + + /** @} */ + + /** + * Applies the filtering to a mono buffer of samples. + * Expect seg faults or other memory issues if arguements passed are incorrect. + * + * + * @param inSamples Pointer to the first sample of the buffer + * @param numSamples Number of samples in the buffer. The left and right buffers + * must be the same size. + */ + inline void Process1in1out(T* inSamples, size_t numSamples); + + /** + * Applies the filtering to a stereo buffer of samples. + * Expect seg faults or other memory issues if arguements passed are incorrect. + * + * @param leftSamples Pointer to the first sample of the left channel's buffer + * @param rightSamples Pointer to the first sample of the right channel's buffer + * @param numSamples Number of samples in the buffer. The left and right buffers + * must be the same size. + */ + inline void Process2in2out(T* leftSamples, T* rightSamples, size_t numSamples); + + SongbirdFilterModule operator=(SongbirdFilterModule& other) = delete; + SongbirdFilterModule(SongbirdFilterModule& other) = delete; + + private: + int _vowel1, + _vowel2; + + double _filterPosition, + _sampleRate, + _mix, + _airGain, + _outputGain; + + bool _modMode; + + std::shared_ptr> _modulationSource; + + SongbirdFormantFilter _filter1Left; + SongbirdFormantFilter _filter1Right; + + SongbirdFormantFilter _filter2Left; + SongbirdFormantFilter _filter2Right; + + SongbirdFormantFilter _filterAirLeft; + SongbirdFormantFilter _filterAirRight; + + /** + * Sets the vowel sound that should be created by filter 1 using a Vowel object provided by + * the caller rather than one of the built in Vowel objects stored in this class. + * + * @param val Value that should be used for Vowel 1 + */ + inline void _setVowel1(const Vowel& val); + + /** + * Uses the filterPosition parameter and the modulation source to calculate the vowel that + * should be used when in MODMODE_FREQ, as this vowel will sit somewhere between the two + * vowels that have been selected by the user. + * + * @param modAmount Modulation amount to be applied + */ + inline Vowel _calcVowelForFreqMode(double modAmount); + + /** + * An array which defines all the formants that will be needed. + */ + // (TODO: could be made static again) + const Vowel _allFormants[NUM_VOWELS] { + {Formant(800, 0), Formant(1150, -4)}, + {Formant(400, 0), Formant(2100, -24)}, + {Formant(350, 0), Formant(2400, -20)}, + {Formant(450, 0), Formant(800, -9)}, + {Formant(325, 0), Formant(700, -12)}, + }; + }; + + template + void SongbirdFilterModule::setVowel1(int val) { + // perform a bounds check, then apply the appropriate formants + _vowel1 = Parameters::VOWEL.BoundsCheck(val); + + _filter1Left.setFormants(_allFormants[_vowel1 - 1]); + _filter1Right.setFormants(_allFormants[_vowel1 - 1]); + } + + template + void SongbirdFilterModule::setVowel2(int val) { + // perform a bounds check, then apply the appropriate formants + _vowel2 = Parameters::VOWEL.BoundsCheck(val); + + _filter2Left.setFormants(_allFormants[_vowel2 - 1]); + _filter2Right.setFormants(_allFormants[_vowel2 - 1]); + } + + template + void SongbirdFilterModule::setSampleRate(double val) { + _sampleRate = val; + + _filter1Left.setSampleRate(val); + _filter1Right.setSampleRate(val); + _filter2Left.setSampleRate(val); + _filter2Right.setSampleRate(val); + _filterAirLeft.setSampleRate(val); + _filterAirRight.setSampleRate(val); + } + + template + void SongbirdFilterModule::reset() { + _filter1Left.reset(); + _filter1Right.reset(); + _filter2Left.reset(); + _filter2Right.reset(); + _filterAirLeft.reset(); + _filterAirRight.reset(); + } + + template + Vowel SongbirdFilterModule::getVowelDescription(int val) const { + Vowel tempVowel; + + std::copy(&_allFormants[val - 1][0], + &_allFormants[val - 1][NUM_FORMANTS_PER_VOWEL], + std::begin(tempVowel)); + + return tempVowel; + } + + template + void SongbirdFilterModule::Process1in1out(T* inSamples, size_t numSamples) { + + for (size_t index {0}; index < numSamples; index++) { + + // Figure out the modulation here. We have two ways to modulation between + // two formant filters. + // For MODMODE_BLEND we modulation the filter position to blend between the two filters. + // For MODMOD_FREQ we set the filter position to 0 so that we're only using filter 1, + // and then modulate the freqency of filter 1 between the two vowels + double blendFilterPosition {0}; + const double modAmount {_modulationSource != nullptr ? _modulationSource->getNextOutput(inSamples[index]) : 0}; + if (_modMode == Parameters::MODMODE_BLEND) { + blendFilterPosition = _filterPosition + modAmount; + } else { + blendFilterPosition = 0; + _setVowel1(_calcVowelForFreqMode(modAmount)); + } + + // Do the processing for each filter + const T originalInput {inSamples[index]}; + const T filter1Out = _filter1Left.process(originalInput); + const T filter2Out = _filter2Left.process(originalInput); + const T filterAirOut = _filterAirLeft.process(originalInput); + + // Write to output, applying filter position and mix level + inSamples[index] = ( + originalInput * (1 - _mix) + + filter1Out * (1 - blendFilterPosition) * _mix + + filter2Out * blendFilterPosition * _mix + + filterAirOut * _airGain * _mix + ) + * _outputGain; + } + } + + template + void SongbirdFilterModule::Process2in2out(T* leftSamples, + T* rightSamples, + size_t numSamples) { + + for (size_t index {0}; index < numSamples; index++) { + + // Figure out the modulation here. We have two ways to modulation between + // two formant filters. + // For MODMODE_BLEND we modulation the filter position to blend between the two filters. + // For MODMOD_FREQ we set the filter position to 0 so that we're only using filter 1, + // and then modulate the freqency of filter 1 between the two vowels + double blendFilterPosition {0}; + const double modAmount { + _modulationSource != nullptr ? _modulationSource->getNextOutput((leftSamples[index] + rightSamples[index]) / 2) : 0 + }; + + if (_modMode == Parameters::MODMODE_BLEND) { + blendFilterPosition = _filterPosition + modAmount; + } else { + blendFilterPosition = 0; + _setVowel1(_calcVowelForFreqMode(modAmount)); + } + + // Do the processing for each filter + const T originalLeftIn {leftSamples[index]}; + const T originalRightIn {rightSamples[index]}; + + const T filter1LeftOut = _filter1Left.process(originalLeftIn); + const T filter1RightOut = _filter1Right.process(originalRightIn); + + const T filter2LeftOut = _filter2Left.process(originalLeftIn); + const T filter2RightOut = _filter2Right.process(originalRightIn); + + const T filterAirLeftOut = _filterAirLeft.process(originalLeftIn); + const T filterAirRightOut = _filterAirRight.process(originalRightIn); + + + // Write to output, applying filter position and mix level + leftSamples[index] = ( + originalLeftIn * (1 - _mix) + + filter1LeftOut * (1 - blendFilterPosition) * _mix + + filter2LeftOut * blendFilterPosition * _mix + + filterAirLeftOut * _airGain * _mix + ) + * _outputGain; + + rightSamples[index] = ( + originalRightIn * (1 - _mix) + + filter1RightOut * (1 - blendFilterPosition) * _mix + + filter2RightOut * blendFilterPosition * _mix + + filterAirRightOut * _airGain * _mix + ) + * _outputGain; + } + } + + template + void SongbirdFilterModule::_setVowel1(const Vowel& val) { + _filter1Left.setFormants(val); + _filter1Right.setFormants(val); + } + + template + Vowel SongbirdFilterModule::_calcVowelForFreqMode(double modAmount) { + // get the first and second vowels + Vowel tempVowel1(getVowelDescription(getVowel1())); + Vowel tempVowel2(getVowelDescription(getVowel2())); + + Vowel retVal(tempVowel1); + + for (size_t iii {0}; iii < NUM_FORMANTS_PER_VOWEL; iii++) { + // Calculate frequency modulation + const double freqDelta {tempVowel2[iii].frequency - tempVowel1[iii].frequency}; + retVal[iii].frequency = tempVowel1[iii].frequency + freqDelta * (_filterPosition + modAmount); + + // Calculate gain modulation + const double gainDelta {tempVowel2[iii].gaindB - tempVowel1[iii].gaindB}; + retVal[iii].gaindB = tempVowel1[iii].gaindB + gainDelta * (_filterPosition + modAmount); + } + + return retVal; + } +} diff --git a/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFiltersParameters.h b/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFiltersParameters.h new file mode 100644 index 00000000..9f11c57f --- /dev/null +++ b/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFiltersParameters.h @@ -0,0 +1,60 @@ +/* + * File: SongbirdFiltersParameters.h + * + * Created: 02/10/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/ParameterDefinition.h" + +namespace WECore::Songbird::Parameters { + + class VowelParameter : public ParameterDefinition::BaseParameter { + public: + VowelParameter() : ParameterDefinition::BaseParameter(VOWEL_A, VOWEL_U, VOWEL_A) { } + + static constexpr int VOWEL_A {1}, + VOWEL_E {2}, + VOWEL_I {3}, + VOWEL_O {4}, + VOWEL_U {5}; + }; + const VowelParameter VOWEL; + + //@{ + /** + * A parameter which can take any float value between the ranges defined. + * The values passed on construction are in the following order: + * minimum value, + * maximum value, + * default value + */ + const ParameterDefinition::RangedParameter FILTER_POSITION(0, 1, 0.5), + MIX(0, 1, 1), + AIR_GAIN(0, 1, 0.5), + OUTPUTGAIN(0, 2, 1); + //@} + + constexpr bool MODMODE_BLEND = false, + MODMODE_FREQ = true, + MODMODE_DEFAULT = MODMODE_FREQ; + + constexpr int FILTER_ORDER {8}; + +} diff --git a/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFormantFilter.h b/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFormantFilter.h new file mode 100644 index 00000000..d8ef45ce --- /dev/null +++ b/ports-juce7/syndicate/WECore/SongbirdFilters/SongbirdFormantFilter.h @@ -0,0 +1,149 @@ +/* + * File: SongbirdFormantFilter.h + * + * Created: 16/07/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "SongbirdFilters/Formant.h" +#include "WEFilters/TPTSVFilter.h" +#include +#include + +namespace WECore::Songbird { + /** + * A class containing a vector of bandpass filters to produce a vowel sound. + * + * Supports only mono audio processing. For stereo processing, you must create + * two objects of this type. (Do not reuse a single object for both channels) + * + * To use this class, simply call setFormants, reset, and process as necessary. + * + * @see setFormants - must be called before performing any processing + * @see Formant - Formant objects are required for operation of this class + */ + template + class SongbirdFormantFilter { + static_assert(std::is_floating_point::value, + "Must be provided with a floating point template type"); + + public: + /** + * Creates and stores the appropriate number of filters. + */ + SongbirdFormantFilter() { + for (size_t iii {0}; iii < NUM_FORMANTS; iii++) { + _filters[iii].setMode(TPTSVF::Parameters::FILTER_MODE.PEAK); + _filters[iii].setQ(10); + } + } + + virtual ~SongbirdFormantFilter() = default; + + /** + * Applies the filtering to a mono buffer of samples. + * Expect seg faults or other memory issues if arguements passed are incorrect. + * + * @param inSample Sample to process + */ + inline T process(T inSample); + + /** + * Sets the properties of each bandpass filter contained in the object. + * + * @param formants An array of Formants, the size of which must equal the + * number of bandpass filters in the object. + * + * @return A boolean value, true if the formants have been applied to the filters + * correctly, false if the operation failed + * + * @see Formant - This object is used to as a convenient container of all the + * parameters which can be supplied to a bandpass filter. + */ + inline bool setFormants(const std::array& formants); + + /** + * Sets the sample rate which the filters will be operating on. + */ + inline void setSampleRate(double val); + + /** + * Resets all filters. + * Call this whenever the audio stream is interrupted (ie. the playhead is moved) + */ + inline void reset(); + + private: + std::array, NUM_FORMANTS> _filters; + }; + + template + T SongbirdFormantFilter::process(T inSample) { + + T outSample {0}; + + // Perform the filtering for each formant peak + for (size_t filterNumber {0}; filterNumber < _filters.size(); filterNumber++) { + + T tempSample {inSample}; + + _filters[filterNumber].processBlock(&tempSample, 1); + + outSample += tempSample; + } + + return outSample; + } + + template + bool SongbirdFormantFilter::setFormants( + const std::array& formants) { + + bool retVal {false}; + + // if the correct number of formants have been supplied, + // apply them to each filter in turn + if (_filters.size() == formants.size()) { + retVal = true; + + for (size_t iii {0}; iii < _filters.size(); iii++) { + _filters[iii].setCutoff(formants[iii].frequency); + + double gainAbs = CoreMath::dBToLinear(formants[iii].gaindB); + _filters[iii].setGain(gainAbs); + } + } + + return retVal; + } + + template + void SongbirdFormantFilter::setSampleRate(double val) { + for (TPTSVF::TPTSVFilter& filter : _filters) { + filter.setSampleRate(val); + } + } + + template + void SongbirdFormantFilter::reset() { + for (TPTSVF::TPTSVFilter& filter : _filters) { + filter.reset(); + } + } +} diff --git a/ports-juce7/syndicate/WECore/SongbirdFilters/Tests/SongbirdFilterModuleTests.cpp b/ports-juce7/syndicate/WECore/SongbirdFilters/Tests/SongbirdFilterModuleTests.cpp new file mode 100644 index 00000000..a2d710f4 --- /dev/null +++ b/ports-juce7/syndicate/WECore/SongbirdFilters/Tests/SongbirdFilterModuleTests.cpp @@ -0,0 +1,208 @@ +/* + * File: SongbirdFilterModuleTests.cpp + * + * Created: 07/01/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "SongbirdFilters/SongbirdFilterModule.h" +#include "SongbirdFilters/Tests/TestData.h" + +#include + +SCENARIO("SongbirdFilterModule: Parameters can be set and retrieved correctly") { + GIVEN("A new SongbirdFilterModule object") { + WECore::Songbird::SongbirdFilterModule mSongbird; + + WHEN("Nothing is changed") { + THEN("Parameters have their default values") { + CHECK(mSongbird.getVowel1() == 1); + CHECK(mSongbird.getVowel2() == 2); + CHECK(mSongbird.getFilterPosition() == 0.5); + CHECK(mSongbird.getMix() == 1.0); + CHECK(mSongbird.getAirGain() == 0.5); + CHECK(mSongbird.getModMode() == true); + } + } + + WHEN("All parameters are changed to unique values") { + mSongbird.setVowel1(3); + mSongbird.setVowel2(4); + mSongbird.setFilterPosition(0.03); + mSongbird.setMix(0.04); + mSongbird.setAirGain(0.05); + mSongbird.setModMode(true); + + THEN("They all get their correct unique values") { + CHECK(mSongbird.getVowel1() == 3); + CHECK(mSongbird.getVowel2() == 4); + CHECK(mSongbird.getFilterPosition() == Approx(0.03)); + CHECK(mSongbird.getMix() == Approx(0.04)); + CHECK(mSongbird.getAirGain() == Approx(0.05)); + CHECK(mSongbird.getModMode() == true); + } + } + } +} + +SCENARIO("SongbirdFilterModule: Parameters enforce their bounds correctly") { + GIVEN("A new SongbirdFilterModule object") { + WECore::Songbird::SongbirdFilterModule mSongbird; + + WHEN("All parameter values are too low") { + mSongbird.setVowel1(-5); + mSongbird.setVowel2(-5); + mSongbird.setFilterPosition(-5); + mSongbird.setMix(-5); + mSongbird.setAirGain(-5); + + THEN("Parameters enforce their lower bounds") { + CHECK(mSongbird.getVowel1() == 1); + CHECK(mSongbird.getVowel2() == 1); + CHECK(mSongbird.getFilterPosition() == Approx(0.0)); + CHECK(mSongbird.getMix() == Approx(0.0)); + CHECK(mSongbird.getAirGain() == Approx(0.0)); + } + } + + WHEN("All parameter values are too high") { + mSongbird.setVowel1(1000); + mSongbird.setVowel2(1000); + mSongbird.setFilterPosition(1000); + mSongbird.setMix(1000); + mSongbird.setAirGain(1000); + + THEN("Parameters enforce their upper bounds") { + CHECK(mSongbird.getVowel1() == 5); + CHECK(mSongbird.getVowel2() == 5); + CHECK(mSongbird.getFilterPosition() == Approx(1.0)); + CHECK(mSongbird.getMix() == Approx(1.0)); + CHECK(mSongbird.getAirGain() == Approx(1.0)); + } + } + } +} + +SCENARIO("SongbirdFilterModule: Silence in = silence out") { + GIVEN("A SongbirdFilterModule and a buffer of silent samples") { + std::vector leftBuffer(1024); + std::vector rightBuffer(1024); + WECore::Songbird::SongbirdFilterModule mSongbird; + + // fill the buffer + std::fill(leftBuffer.begin(), leftBuffer.end(), 0); + std::fill(rightBuffer.begin(), rightBuffer.end(), 0); + + WHEN("The silence samples are processed") { + + // do processing + mSongbird.Process2in2out(&leftBuffer[0], &rightBuffer[0], leftBuffer.size()); + + THEN("The output is silence") { + for (size_t iii {0}; iii < leftBuffer.size(); iii++) { + CHECK(leftBuffer[iii] == Approx(0.0)); + CHECK(rightBuffer[iii] == Approx(0.0)); + } + } + } + } +} + +SCENARIO("SongbirdFilterModule: Freq mode") { + GIVEN("A SongbirdFilterModule and a buffer of sine samples") { + std::vector leftBuffer(1024); + std::vector rightBuffer(1024); + const std::vector& expectedOutputLeft = + TestData::Songbird::Data.at(Catch::getResultCapture().getCurrentTestName() + "-left"); + const std::vector& expectedOutputRight = + TestData::Songbird::Data.at(Catch::getResultCapture().getCurrentTestName() + "-right"); + + WECore::Songbird::SongbirdFilterModule mSongbird; + + // Set some parameters for the input signal + constexpr size_t SAMPLE_RATE {44100}; + constexpr size_t SINE_FREQ {1000}; + constexpr double SAMPLES_PER_CYCLE {SAMPLE_RATE / SINE_FREQ}; + + // fill the buffers, phase shift the right one so that they're not identical + std::generate(leftBuffer.begin(), + leftBuffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE));} ); + std::generate(rightBuffer.begin(), + rightBuffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE) + WECore::CoreMath::LONG_PI);} ); + WHEN("The parameters are set and samples are processed") { + // Set freq mode + mSongbird.setVowel1(1); + mSongbird.setVowel2(2); + mSongbird.setAirGain(1); + mSongbird.setModMode(true); + + mSongbird.Process2in2out(&leftBuffer[0], &rightBuffer[0], leftBuffer.size()); + + THEN("The output is as expected") { + for (size_t iii {0}; iii < leftBuffer.size(); iii++) { + CHECK(leftBuffer[iii] == Approx(expectedOutputLeft[iii]).margin(0.00001)); + CHECK(rightBuffer[iii] == Approx(expectedOutputRight[iii]).margin(0.00001)); + } + } + } + } +} + +SCENARIO("SongbirdFilterModule: Blend mode") { + GIVEN("A SongbirdFilterModule and a buffer of sine samples") { + std::vector leftBuffer(1024); + std::vector rightBuffer(1024); + const std::vector& expectedOutputLeft = + TestData::Songbird::Data.at(Catch::getResultCapture().getCurrentTestName() + "-left"); + const std::vector& expectedOutputRight = + TestData::Songbird::Data.at(Catch::getResultCapture().getCurrentTestName() + "-right"); + + WECore::Songbird::SongbirdFilterModule mSongbird; + + // Set some parameters for the input signal + constexpr size_t SAMPLE_RATE {44100}; + constexpr size_t SINE_FREQ {1000}; + constexpr double SAMPLES_PER_CYCLE {SAMPLE_RATE / SINE_FREQ}; + + // fill the buffers, phase shift the right one so that they're not identical + std::generate(leftBuffer.begin(), + leftBuffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE));} ); + std::generate(rightBuffer.begin(), + rightBuffer.end(), + [iii = 0]() mutable {return std::sin(WECore::CoreMath::LONG_TAU * (iii++ / SAMPLES_PER_CYCLE) + WECore::CoreMath::LONG_PI);} ); + WHEN("The parameters are set and samples are processed") { + // Set blend mode + mSongbird.setVowel1(1); + mSongbird.setVowel2(2); + mSongbird.setAirGain(1); + mSongbird.setModMode(false); + + mSongbird.Process2in2out(&leftBuffer[0], &rightBuffer[0], leftBuffer.size()); + + THEN("The output is as expected") { + for (size_t iii {0}; iii < leftBuffer.size(); iii++) { + CHECK(leftBuffer[iii] == Approx(expectedOutputLeft[iii]).margin(0.00001)); + CHECK(rightBuffer[iii] == Approx(expectedOutputRight[iii]).margin(0.00001)); + } + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/SongbirdFilters/Tests/TestData.h b/ports-juce7/syndicate/WECore/SongbirdFilters/Tests/TestData.h new file mode 100644 index 00000000..5b310df7 --- /dev/null +++ b/ports-juce7/syndicate/WECore/SongbirdFilters/Tests/TestData.h @@ -0,0 +1,49 @@ +/* + * File: TestData.h + * + * Created: 31/03/2018 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include + +namespace TestData::Songbird { + const std::unordered_map> Data = + { + { + "Scenario: SongbirdFilterModule: Freq mode-left", + {0, 0.0158007, 0.0611703, 0.130759, 0.217165, 0.312035, 0.407199, 0.49564, 0.572165, 0.633683, 0.679105, 0.708928, 0.724639, 0.728066, 0.72081, 0.703871, 0.677486, 0.641201, 0.594111, 0.535193, 0.463653, 0.379216, 0.282299, 0.174072, 0.056405, -0.0682739, -0.197149, -0.3272, -0.45537, -0.578702, -0.69446, -0.800231, -0.894034, -0.974416, -1.04055, -1.09229, -1.13017, -1.15535, -1.16941, -1.17414, -1.17123, -1.16195, -1.14689, -1.12582, -1.09761, -1.0604, -1.01188, -0.949649, -0.871636, -0.776525, -0.664047, -0.535145, -0.391957, -0.237648, -0.0760936, 0.0884999, 0.251988, 0.410621, 0.561273, 0.701536, 0.829684, 0.944528, 1.04522, 1.13101, 1.20114, 1.25467, 1.29052, 1.30757, 1.3048, 1.2815, 1.23746, 1.17316, 1.0898, 0.989372, 0.874552, 0.748575, 0.615053, 0.477769, 0.340474, 0.206708, 0.0796444, -0.0380197, -0.144139, -0.237159, -0.316125, -0.380658, -0.430907, -0.467473, -0.491316, -0.503643, -0.505789, -0.499111, -0.484887, -0.464253, -0.43818, -0.407478, -0.372853, -0.334974, -0.294558, -0.25244, -0.20962, -0.167269, -0.126694, -0.0892585, -0.056283, -0.0289307, -0.00810586, 0.0056201, 0.0120396, 0.0112936, 0.00381222, -0.00977554, -0.0287467, -0.0523834, -0.0800492, -0.111228, -0.145523, -0.182611, -0.222175, -0.263811, -0.30695, -0.350783, -0.394233, -0.43595, -0.474356, -0.507717, -0.534238, -0.55217, -0.559923, -0.556151, -0.539841, -0.510354, -0.467463, -0.411355, -0.34262, -0.262225, -0.171472, -0.0719559, 0.0344914, 0.145853, 0.259982, 0.374658, 0.487638, 0.596701, 0.699691, 0.794554, 0.879364, 0.952356, 1.01194, 1.05675, 1.08565, 1.09776, 1.09254, 1.06978, 1.02963, 0.972653, 0.899805, 0.812425, 0.712202, 0.601113, 0.481358, 0.355269, 0.225225, 0.0935759, -0.0374328, -0.165707, -0.289334, -0.406601, -0.515981, -0.61613, -0.705861, -0.784134, -0.850042, -0.902819, -0.941846, -0.966674, -0.977046, -0.972923, -0.954502, -0.922234, -0.87682, -0.819207, -0.750563, -0.672259, -0.585823, -0.492912, -0.39527, -0.294692, -0.19299, -0.0919632, 0.0066338, 0.101113, 0.189879, 0.271449, 0.34448, 0.407783, 0.460352, 0.501379, 0.530274, 0.546687, 0.550523, 0.541954, 0.521427, 0.489668, 0.447675, 0.396705, 0.338245, 0.273983, 0.205758, 0.135509, 0.0652172, -0.00315684, -0.0677331, -0.126768, -0.178703, -0.222205, -0.256193, -0.279857, -0.292667, -0.294371, -0.284988, -0.264802, -0.234343, -0.194373, -0.14587, -0.0900091, -0.0281405, 0.038236, 0.1075, 0.177943, 0.24781, 0.315339, 0.378803, 0.436559, 0.487084, 0.529018, 0.561192, 0.582656, 0.592696, 0.590845, 0.576887, 0.550856, 0.513022, 0.46389, 0.404177, 0.3348, 0.256856, 0.171603, 0.0804429, -0.0151056, -0.113424, -0.212821, -0.311555, -0.407862, -0.499984, -0.5862, -0.664855, -0.734396, -0.793401, -0.840613, -0.874968, -0.895628, -0.901995, -0.893737, -0.870794, -0.833387, -0.782014, -0.717442, -0.640694, -0.55303, -0.455922, -0.351025, -0.240149, -0.125225, -0.00827165, 0.108645, 0.223444, 0.334076, 0.438554, 0.535001, 0.621686, 0.697063, 0.759805, 0.808836, 0.843354, 0.862853, 0.86713, 0.856294, 0.830758, 0.791227, 0.738679, 0.674342, 0.599662, 0.516266, 0.425928, 0.330529, 0.232015, 0.132358, 0.0335166, -0.0626033, -0.154185, -0.239533, -0.317109, -0.385552, -0.44371, -0.490656, -0.525705, -0.548422, -0.558634, -0.556423, -0.542128, -0.516333, -0.479852, -0.433713, -0.379128, -0.317473, -0.250254, -0.179075, -0.105605, -0.031541, 0.0414248, 0.11164, 0.177524, 0.237599, 0.29052, 0.335098, 0.370323, 0.395389, 0.409703, 0.412902, 0.404863, 0.385704, 0.355789, 0.315717, 0.266319, 0.208638, 0.143912, 0.0735487, -0.000903307, -0.0777877, -0.155375, -0.2319, -0.305602, -0.37476, -0.437734, -0.492999, -0.539181, -0.575085, -0.599724, -0.61234, -0.612422, -0.59972, -0.57425, -0.536299, -0.486418, -0.425416, -0.354341, -0.274464, -0.187251, -0.0943358, 0.00251365, 0.101432, 0.200496, 0.29776, 0.391302, 0.479262, 0.559877, 0.631524, 0.692747, 0.742293, 0.779133, 0.802484, 0.811824, 0.806903, 0.787746, 0.754652, 0.708187, 0.649174, 0.578673, 0.497966, 0.408525, 0.311987, 0.210125, 0.104809, -0.00202783, -0.108424, -0.21243, -0.312145, -0.405754, -0.491559, -0.568019, -0.633772, -0.687666, -0.728779, -0.756435, -0.770221, -0.769989, -0.755862, -0.728228, -0.687734, -0.63527, -0.571956, -0.499113, -0.418241, -0.330991, -0.239126, -0.144493, -0.0489809, 0.0455146, 0.137129, 0.224067, 0.304638, 0.377292, 0.440651, 0.493532, 0.534978, 0.564275, 0.580963, 0.584848, 0.576005, 0.554776, 0.521761, 0.477802, 0.423971, 0.361539, 0.291956, 0.216811, 0.137807, 0.0567175, -0.0246466, -0.104478, -0.181009, -0.252552, -0.31753, -0.374515, -0.422254, -0.459697, -0.486018, -0.500633, -0.50321, -0.493678, -0.472229, -0.439309, -0.395614, -0.342071, -0.279819, -0.210187, -0.134663, -0.0548667, 0.0274895, 0.110632, 0.192763, 0.272102, 0.346916, 0.415562, 0.476516, 0.528405, 0.57004, 0.600435, 0.618831, 0.624712, 0.617814, 0.598135, 0.565935, 0.521729, 0.466281, 0.400589, 0.325864, 0.243509, 0.155091, 0.0623067, -0.0330457, -0.129106, -0.223988, -0.315817, -0.402768, -0.483105, -0.555209, -0.617621, -0.669062, -0.708466, -0.734999, -0.748079, -0.747386, -0.732869, -0.70475, -0.663521, -0.60993, -0.544972, -0.469868, -0.38604, -0.295083, -0.198736, -0.0988428, 0.00268113, 0.103888, 0.202839, 0.297638, 0.386475, 0.46766, 0.539656, 0.601111, 0.650885, 0.688071, 0.712013, 0.722319, 0.718871, 0.701821, 0.671591, 0.628863, 0.574564, 0.509848, 0.436071, 0.354763, 0.267595, 0.176349, 0.0828792, -0.0109269, -0.103185, -0.192051, -0.275762, -0.352667, -0.421264, -0.480227, -0.528435, -0.564989, -0.589236, -0.600778, -0.599478, -0.585464, -0.559126, -0.521104, -0.47228, -0.413751, -0.346815, -0.272938, -0.193725, -0.110886, -0.0262037, 0.0585062, 0.141432, 0.220801, 0.294922, 0.362212, 0.421236, 0.470732, 0.509639, 0.537119, 0.552576, 0.555664, 0.546299, 0.524658, 0.491176, 0.446539, 0.391669, 0.327702, 0.255971, 0.177973, 0.0953424, 0.00981279, -0.0768145, -0.16271, -0.246053, -0.32507, -0.39807, -0.463482, -0.519887, -0.566045, -0.600925, -0.623724, -0.633885, -0.63111, -0.615365, -0.586882, -0.546155, -0.493932, -0.431197, -0.359152, -0.279195, -0.192887, -0.101928, -0.00811302, 0.0866946, 0.180608, 0.27175, 0.358295, 0.438503, 0.510759, 0.573599, 0.625746, 0.666134, 0.693928, 0.708543, 0.709656, 0.697212, 0.671425, 0.632776, 0.582, 0.520075, 0.448202, 0.367777, 0.280369, 0.187684, 0.0915349, -0.00619921, -0.103607, -0.198786, -0.289878, -0.375107, -0.452817, -0.521502, -0.579838, -0.626708, -0.661227, -0.682756, -0.690916, -0.685597, -0.666957, -0.635421, -0.59167, -0.536631, -0.471451, -0.397481, -0.316242, -0.229401, -0.13873, -0.0460739, 0.0466879, 0.137679, 0.225067, 0.307094, 0.382121, 0.448654, 0.505379, 0.551183, 0.585183, 0.606739, 0.615467, 0.611249, 0.594231, 0.564824, 0.523692, 0.47174, 0.410091, 0.34007, 0.263168, 0.181018, 0.0953572, 0.00799298, -0.0792353, -0.164494, -0.245992, -0.322019, -0.390981, -0.451429, -0.502096, -0.541917, -0.57005, -0.585901, -0.589128, -0.579653, -0.557659, -0.523592, -0.478149, -0.42226, -0.357078, -0.283945, -0.204373, -0.120008, -0.0325975, 0.0560437, 0.144073, 0.229656, 0.311007, 0.38642, 0.454311, 0.513244, 0.561967, 0.59943, 0.624816, 0.63755, 0.637316, 0.624062, 0.598001, 0.559608, 0.509612, 0.448979, 0.378894, 0.300737, 0.216055, 0.126533, 0.0339558, -0.0598243, -0.152927, -0.243481, -0.329662, -0.409732, -0.482072, -0.545215, -0.597876, -0.638982, -0.667688, -0.683398, -0.685776, -0.674753, -0.650531, -0.613574, -0.564605, -0.504587, -0.434704, -0.35634, -0.271048, -0.180522, -0.086558, 0.00897677, 0.104185, 0.197178, 0.286109, 0.369218, 0.444861, 0.511545, 0.56796, 0.613001, 0.645796, 0.665719, 0.672402, 0.665746, 0.645921, 0.61336, 0.568754, 0.513035, 0.447358, 0.373075, 0.291712, 0.204934, 0.114512, 0.0222881, -0.069864, -0.160075, -0.246519, -0.327451, -0.40124, -0.466406, -0.521647, -0.565865, -0.598189, -0.617994, -0.624911, -0.618835, -0.599928, -0.568614, -0.525571, -0.471715, -0.408185, -0.336315, -0.25761, -0.173713, -0.0863714, 0.00259637, 0.0913416, 0.178021, 0.260836, 0.338067, 0.408112, 0.469517, 0.521006, 0.561508, 0.59018, 0.60642, 0.609885, 0.600494, 0.578431, 0.544141, 0.498323, 0.441913, 0.376064, 0.302127, 0.221619, 0.136196, 0.0476152, -0.0423, -0.131695, -0.218724, -0.301588, -0.37857, -0.44807, -0.508643, -0.55902, -0.598142, -0.625177, -0.639538, -0.640899, -0.629195, -0.604631, -0.567671, -0.519035, -0.459681, -0.390786, -0.313723, -0.230034, -0.141397, -0.0495933, 0.0435301, 0.136096, 0.226236, 0.312129, 0.392037, 0.464342, 0.527576, 0.580457, 0.621905, 0.651075, 0.667367, 0.670441, 0.660223, 0.636907, 0.600951, 0.55307, 0.494217, 0.425569, 0.348497, 0.264547, 0.175399, 0.0828404, -0.0112742, -0.105058, -0.196634, -0.284166, -0.365904, -0.440215, -0.505615, -0.560802, -0.604682, -0.636388, -0.655301, -0.661061, -0.653573, -0.633012, -0.599816, -0.554678, -0.498532, -0.432535, -0.358041, -0.276575, -0.189802, -0.0994937, -0.00748855, 0.0843408, 0.174128, 0.260051, 0.340369, 0.413455, 0.477836, 0.532213, 0.575496, 0.606823, 0.625573, 0.631388, 0.624169, 0.604089, 0.571579, 0.527327, 0.472258, 0.407521, 0.334459, 0.254585, 0.169551, 0.0811143, -0.00890012, -0.0986358, -0.186243, -0.269915, -0.347929, -0.418676, -0.480699, -0.532718, -0.57366, -0.602678, -0.619171, -0.622794, -0.613466, -0.591373, -0.55696, -0.510929, -0.454216, -0.387979, -0.313573, -0.232519, -0.146475, -0.0572048, 0.0334625, 0.123667, 0.211558, 0.295328, 0.373254, 0.443731, 0.505304, 0.556699, 0.596848, 0.624912, 0.640297, 0.642669, 0.631955, 0.608353, 0.572321, 0.524571, 0.466055, 0.397944, 0.321607, 0.238579, 0.150534, 0.0592489, -0.033431, -0.125632, -0.215487, -0.301176, -0.380963, -0.453229, -0.516506, -0.569509, -0.611158, -0.640605, -0.657247, -0.660741, -0.651009, -0.62824, -0.59289, -0.545667, -0.487519, -0.419618, -0.34333, -0.260194, -0.171885, -0.0801828, 0.0130624, 0.105971, 0.196671, 0.283335, 0.364217, 0.43769, 0.502278, 0.556683, 0.599816, 0.630816, 0.64907, 0.654221, 0.64618, 0.625124, 0.591494, 0.545988, 0.48954, 0.423309, 0.348652, 0.267093, 0.180298, 0.090037, -0.00185181, -0.0934974, -0.183036, -0.268647, -0.348593, -0.421251, -0.48515, -0.538998, -0.581707, -0.61242, -0.630522, -0.635658, -0.627737, -0.606934, -0.573687, -0.528689, -0.472872, -0.407388, -0.333586, -0.252985, -0.167242, -0.0781182, 0.0125562, 0.10292, 0.191119, 0.275344, 0.353866, 0.425075, 0.487509, 0.539887, 0.581133, 0.610398, 0.627079, 0.630831, 0.621573, 0.599489, 0.565026, 0.518885, 0.462006, 0.395547, 0.320866, 0.239485, 0.153067, 0.0633769, -0.0277515, -0.118455, -0.206877, -0.291209, -0.369721, -0.440805, -0.503001, -0.555029, -0.595818, -0.624524, -0.640548, -0.64355, -0.633456, -0.610456, -0.575006, -0.527815, -0.469828, -0.402216, -0.326341, -0.243738, -0.156077, -0.0651335, 0.0272509, 0.119203} + }, + { + "Scenario: SongbirdFilterModule: Freq mode-right", + {1.35963e-17, -0.0158007, -0.0611703, -0.130759, -0.217165, -0.312035, -0.407199, -0.49564, -0.572165, -0.633683, -0.679105, -0.708928, -0.724639, -0.728066, -0.72081, -0.703871, -0.677486, -0.641201, -0.594111, -0.535193, -0.463653, -0.379216, -0.282299, -0.174072, -0.056405, 0.0682739, 0.197149, 0.3272, 0.45537, 0.578702, 0.69446, 0.800231, 0.894034, 0.974416, 1.04055, 1.09229, 1.13017, 1.15535, 1.16941, 1.17414, 1.17123, 1.16195, 1.14689, 1.12582, 1.09761, 1.0604, 1.01188, 0.949649, 0.871636, 0.776525, 0.664047, 0.535145, 0.391957, 0.237648, 0.0760936, -0.0884999, -0.251988, -0.410621, -0.561273, -0.701536, -0.829684, -0.944528, -1.04522, -1.13101, -1.20114, -1.25467, -1.29052, -1.30757, -1.3048, -1.2815, -1.23746, -1.17316, -1.0898, -0.989372, -0.874552, -0.748575, -0.615053, -0.477769, -0.340474, -0.206708, -0.0796444, 0.0380197, 0.144139, 0.237159, 0.316125, 0.380658, 0.430907, 0.467473, 0.491316, 0.503643, 0.505789, 0.499111, 0.484887, 0.464253, 0.43818, 0.407478, 0.372853, 0.334974, 0.294558, 0.25244, 0.20962, 0.167269, 0.126694, 0.0892585, 0.056283, 0.0289307, 0.00810586, -0.0056201, -0.0120396, -0.0112936, -0.00381222, 0.00977554, 0.0287467, 0.0523834, 0.0800492, 0.111228, 0.145523, 0.182611, 0.222175, 0.263811, 0.30695, 0.350783, 0.394233, 0.43595, 0.474356, 0.507717, 0.534238, 0.55217, 0.559923, 0.556151, 0.539841, 0.510354, 0.467463, 0.411355, 0.34262, 0.262225, 0.171472, 0.0719559, -0.0344914, -0.145853, -0.259982, -0.374658, -0.487638, -0.596701, -0.699691, -0.794554, -0.879364, -0.952356, -1.01194, -1.05675, -1.08565, -1.09776, -1.09254, -1.06978, -1.02963, -0.972653, -0.899805, -0.812425, -0.712202, -0.601113, -0.481358, -0.355269, -0.225225, -0.0935759, 0.0374328, 0.165707, 0.289334, 0.406601, 0.515981, 0.61613, 0.705861, 0.784134, 0.850042, 0.902819, 0.941846, 0.966674, 0.977046, 0.972923, 0.954502, 0.922234, 0.87682, 0.819207, 0.750563, 0.672259, 0.585823, 0.492912, 0.39527, 0.294692, 0.19299, 0.0919632, -0.0066338, -0.101113, -0.189879, -0.271449, -0.34448, -0.407783, -0.460352, -0.501379, -0.530274, -0.546687, -0.550523, -0.541954, -0.521427, -0.489668, -0.447675, -0.396705, -0.338245, -0.273983, -0.205758, -0.135509, -0.0652172, 0.00315684, 0.0677331, 0.126768, 0.178703, 0.222205, 0.256193, 0.279857, 0.292667, 0.294371, 0.284988, 0.264802, 0.234343, 0.194373, 0.14587, 0.0900091, 0.0281405, -0.038236, -0.1075, -0.177943, -0.24781, -0.315339, -0.378803, -0.436559, -0.487084, -0.529018, -0.561192, -0.582656, -0.592696, -0.590845, -0.576887, -0.550856, -0.513022, -0.46389, -0.404177, -0.3348, -0.256856, -0.171603, -0.0804429, 0.0151056, 0.113424, 0.212821, 0.311555, 0.407862, 0.499984, 0.5862, 0.664855, 0.734396, 0.793401, 0.840613, 0.874968, 0.895628, 0.901995, 0.893737, 0.870794, 0.833387, 0.782014, 0.717442, 0.640694, 0.55303, 0.455922, 0.351025, 0.240149, 0.125225, 0.00827165, -0.108645, -0.223444, -0.334076, -0.438554, -0.535001, -0.621686, -0.697063, -0.759805, -0.808836, -0.843354, -0.862853, -0.86713, -0.856294, -0.830758, -0.791227, -0.738679, -0.674342, -0.599662, -0.516266, -0.425928, -0.330529, -0.232015, -0.132358, -0.0335166, 0.0626033, 0.154185, 0.239533, 0.317109, 0.385552, 0.44371, 0.490656, 0.525705, 0.548422, 0.558634, 0.556423, 0.542128, 0.516333, 0.479852, 0.433713, 0.379128, 0.317473, 0.250254, 0.179075, 0.105605, 0.031541, -0.0414248, -0.11164, -0.177524, -0.237599, -0.29052, -0.335098, -0.370323, -0.395389, -0.409703, -0.412902, -0.404863, -0.385704, -0.355789, -0.315717, -0.266319, -0.208638, -0.143912, -0.0735487, 0.000903307, 0.0777877, 0.155375, 0.2319, 0.305602, 0.37476, 0.437734, 0.492999, 0.539181, 0.575085, 0.599724, 0.61234, 0.612422, 0.59972, 0.57425, 0.536299, 0.486418, 0.425416, 0.354341, 0.274464, 0.187251, 0.0943358, -0.00251365, -0.101432, -0.200496, -0.29776, -0.391302, -0.479262, -0.559877, -0.631524, -0.692747, -0.742293, -0.779133, -0.802484, -0.811824, -0.806903, -0.787746, -0.754652, -0.708187, -0.649174, -0.578673, -0.497966, -0.408525, -0.311987, -0.210125, -0.104809, 0.00202783, 0.108424, 0.21243, 0.312145, 0.405754, 0.491559, 0.568019, 0.633772, 0.687666, 0.728779, 0.756435, 0.770221, 0.769989, 0.755862, 0.728228, 0.687734, 0.63527, 0.571956, 0.499113, 0.418241, 0.330991, 0.239126, 0.144493, 0.0489809, -0.0455146, -0.137129, -0.224067, -0.304638, -0.377292, -0.440651, -0.493532, -0.534978, -0.564275, -0.580963, -0.584848, -0.576005, -0.554776, -0.521761, -0.477802, -0.423971, -0.361539, -0.291956, -0.216811, -0.137807, -0.0567175, 0.0246466, 0.104478, 0.181009, 0.252552, 0.31753, 0.374515, 0.422254, 0.459697, 0.486018, 0.500633, 0.50321, 0.493678, 0.472229, 0.439309, 0.395614, 0.342071, 0.279819, 0.210187, 0.134663, 0.0548667, -0.0274895, -0.110632, -0.192763, -0.272102, -0.346916, -0.415562, -0.476516, -0.528405, -0.57004, -0.600435, -0.618831, -0.624712, -0.617814, -0.598135, -0.565935, -0.521729, -0.466281, -0.400589, -0.325864, -0.243509, -0.155091, -0.0623067, 0.0330457, 0.129106, 0.223988, 0.315817, 0.402768, 0.483105, 0.555209, 0.617621, 0.669062, 0.708466, 0.734999, 0.748079, 0.747386, 0.732869, 0.70475, 0.663521, 0.60993, 0.544972, 0.469868, 0.38604, 0.295083, 0.198736, 0.0988428, -0.00268113, -0.103888, -0.202839, -0.297638, -0.386475, -0.46766, -0.539656, -0.601111, -0.650885, -0.688071, -0.712013, -0.722319, -0.718871, -0.701821, -0.671591, -0.628863, -0.574564, -0.509848, -0.436071, -0.354763, -0.267595, -0.176349, -0.0828792, 0.0109269, 0.103185, 0.192051, 0.275762, 0.352667, 0.421264, 0.480227, 0.528435, 0.564989, 0.589236, 0.600778, 0.599478, 0.585464, 0.559126, 0.521104, 0.47228, 0.413751, 0.346815, 0.272938, 0.193725, 0.110886, 0.0262037, -0.0585062, -0.141432, -0.220801, -0.294922, -0.362212, -0.421236, -0.470732, -0.509639, -0.537119, -0.552576, -0.555664, -0.546299, -0.524658, -0.491176, -0.446539, -0.391669, -0.327702, -0.255971, -0.177973, -0.0953424, -0.00981279, 0.0768145, 0.16271, 0.246053, 0.32507, 0.39807, 0.463482, 0.519887, 0.566045, 0.600925, 0.623724, 0.633885, 0.63111, 0.615365, 0.586882, 0.546155, 0.493932, 0.431197, 0.359152, 0.279195, 0.192887, 0.101928, 0.00811302, -0.0866946, -0.180608, -0.27175, -0.358295, -0.438503, -0.510759, -0.573599, -0.625746, -0.666134, -0.693928, -0.708543, -0.709656, -0.697212, -0.671425, -0.632776, -0.582, -0.520075, -0.448202, -0.367777, -0.280369, -0.187684, -0.0915349, 0.00619921, 0.103607, 0.198786, 0.289878, 0.375107, 0.452817, 0.521502, 0.579838, 0.626708, 0.661227, 0.682756, 0.690916, 0.685597, 0.666957, 0.635421, 0.59167, 0.536631, 0.471451, 0.397481, 0.316242, 0.229401, 0.13873, 0.0460739, -0.0466879, -0.137679, -0.225067, -0.307094, -0.382121, -0.448654, -0.505379, -0.551183, -0.585183, -0.606739, -0.615467, -0.611249, -0.594231, -0.564824, -0.523692, -0.47174, -0.410091, -0.34007, -0.263168, -0.181018, -0.0953572, -0.00799298, 0.0792353, 0.164494, 0.245992, 0.322019, 0.390981, 0.451429, 0.502096, 0.541917, 0.57005, 0.585901, 0.589128, 0.579653, 0.557659, 0.523592, 0.478149, 0.42226, 0.357078, 0.283945, 0.204373, 0.120008, 0.0325975, -0.0560437, -0.144073, -0.229656, -0.311007, -0.38642, -0.454311, -0.513244, -0.561967, -0.59943, -0.624816, -0.63755, -0.637316, -0.624062, -0.598001, -0.559608, -0.509612, -0.448979, -0.378894, -0.300737, -0.216055, -0.126533, -0.0339558, 0.0598243, 0.152927, 0.243481, 0.329662, 0.409732, 0.482072, 0.545215, 0.597876, 0.638982, 0.667688, 0.683398, 0.685776, 0.674753, 0.650531, 0.613574, 0.564605, 0.504587, 0.434704, 0.35634, 0.271048, 0.180522, 0.086558, -0.00897677, -0.104185, -0.197178, -0.286109, -0.369218, -0.444861, -0.511545, -0.56796, -0.613001, -0.645796, -0.665719, -0.672402, -0.665746, -0.645921, -0.61336, -0.568754, -0.513035, -0.447358, -0.373075, -0.291712, -0.204934, -0.114512, -0.0222881, 0.069864, 0.160075, 0.246519, 0.327451, 0.40124, 0.466406, 0.521647, 0.565865, 0.598189, 0.617994, 0.624911, 0.618835, 0.599928, 0.568614, 0.525571, 0.471715, 0.408185, 0.336315, 0.25761, 0.173713, 0.0863714, -0.00259637, -0.0913416, -0.178021, -0.260836, -0.338067, -0.408112, -0.469517, -0.521006, -0.561508, -0.59018, -0.60642, -0.609885, -0.600494, -0.578431, -0.544141, -0.498323, -0.441913, -0.376064, -0.302127, -0.221619, -0.136196, -0.0476152, 0.0423, 0.131695, 0.218724, 0.301588, 0.37857, 0.44807, 0.508643, 0.55902, 0.598142, 0.625177, 0.639538, 0.640899, 0.629195, 0.604631, 0.567671, 0.519035, 0.459681, 0.390786, 0.313723, 0.230034, 0.141397, 0.0495933, -0.0435301, -0.136096, -0.226236, -0.312129, -0.392037, -0.464342, -0.527576, -0.580457, -0.621905, -0.651075, -0.667367, -0.670441, -0.660223, -0.636907, -0.600951, -0.55307, -0.494217, -0.425569, -0.348497, -0.264547, -0.175399, -0.0828404, 0.0112742, 0.105058, 0.196634, 0.284166, 0.365904, 0.440215, 0.505615, 0.560802, 0.604682, 0.636388, 0.655301, 0.661061, 0.653573, 0.633012, 0.599816, 0.554678, 0.498532, 0.432535, 0.358041, 0.276575, 0.189802, 0.0994937, 0.00748855, -0.0843408, -0.174128, -0.260051, -0.340369, -0.413455, -0.477836, -0.532213, -0.575496, -0.606823, -0.625573, -0.631388, -0.624169, -0.604089, -0.571579, -0.527327, -0.472258, -0.407521, -0.334459, -0.254585, -0.169551, -0.0811143, 0.00890012, 0.0986358, 0.186243, 0.269915, 0.347929, 0.418676, 0.480699, 0.532718, 0.57366, 0.602678, 0.619171, 0.622794, 0.613466, 0.591373, 0.55696, 0.510929, 0.454216, 0.387979, 0.313573, 0.232519, 0.146475, 0.0572048, -0.0334625, -0.123667, -0.211558, -0.295328, -0.373254, -0.443731, -0.505304, -0.556699, -0.596848, -0.624912, -0.640297, -0.642669, -0.631955, -0.608353, -0.572321, -0.524571, -0.466055, -0.397944, -0.321607, -0.238579, -0.150534, -0.0592489, 0.033431, 0.125632, 0.215487, 0.301176, 0.380963, 0.453229, 0.516506, 0.569509, 0.611158, 0.640605, 0.657247, 0.660741, 0.651009, 0.62824, 0.59289, 0.545667, 0.487519, 0.419618, 0.34333, 0.260194, 0.171885, 0.0801828, -0.0130624, -0.105971, -0.196671, -0.283335, -0.364217, -0.43769, -0.502278, -0.556683, -0.599816, -0.630816, -0.64907, -0.654221, -0.64618, -0.625124, -0.591494, -0.545988, -0.48954, -0.423309, -0.348652, -0.267093, -0.180298, -0.090037, 0.00185181, 0.0934974, 0.183036, 0.268647, 0.348593, 0.421251, 0.48515, 0.538998, 0.581707, 0.61242, 0.630522, 0.635658, 0.627737, 0.606934, 0.573687, 0.528689, 0.472872, 0.407388, 0.333586, 0.252985, 0.167242, 0.0781182, -0.0125562, -0.10292, -0.191119, -0.275344, -0.353866, -0.425075, -0.487509, -0.539887, -0.581133, -0.610398, -0.627079, -0.630831, -0.621573, -0.599489, -0.565026, -0.518885, -0.462006, -0.395547, -0.320866, -0.239485, -0.153067, -0.0633769, 0.0277515, 0.118455, 0.206877, 0.291209, 0.369721, 0.440805, 0.503001, 0.555029, 0.595818, 0.624524, 0.640548, 0.64355, 0.633456, 0.610456, 0.575006, 0.527815, 0.469828, 0.402216, 0.326341, 0.243738, 0.156077, 0.0651335, -0.0272509, -0.119203} + }, + { + "Scenario: SongbirdFilterModule: Blend mode-left", + {0, 0.0168587, 0.0653725, 0.140111, 0.233549, 0.337159, 0.442548, 0.542417, 0.631223, 0.705456, 0.763528, 0.80536, 0.831785, 0.843896, 0.842506, 0.827776, 0.799091, 0.755167, 0.694331, 0.614909, 0.515627, 0.395961, 0.256366, 0.0983899, -0.0753426, -0.261235, -0.454886, -0.651281, -0.844989, -1.03037, -1.20175, -1.35366, -1.48105, -1.57952, -1.64556, -1.67675, -1.67191, -1.63113, -1.55574, -1.44807, -1.31129, -1.14899, -0.964993, -0.763024, -0.546629, -0.319147, -0.0838115, 0.156048, 0.396782, 0.634225, 0.863581, 1.07945, 1.27598, 1.44722, 1.58746, 1.69175, 1.75626, 1.77861, 1.75807, 1.69555, 1.59348, 1.45549, 1.2861, 1.09032, 0.873335, 0.64023, 0.395852, 0.144753, -0.108772, -0.360596, -0.60665, -0.84284, -1.065, -1.26894, -1.45047, -1.60562, -1.73074, -1.8227, -1.87905, -1.89817, -1.87935, -1.82283, -1.72985, -1.60257, -1.44405, -1.25812, -1.04926, -0.822472, -0.583096, -0.336619, -0.088494, 0.156037, 0.392103, 0.615329, 0.821909, 1.00864, 1.17291, 1.31266, 1.42629, 1.51265, 1.57094, 1.60074, 1.60199, 1.57507, 1.52084, 1.44071, 1.33673, 1.21154, 1.06838, 0.910976, 0.743411, 0.569959, 0.394889, 0.222283, 0.055881, -0.101034, -0.245705, -0.375922, -0.489995, -0.586705, -0.665244, -0.725148, -0.766253, -0.788668, -0.792765, -0.779195, -0.748903, -0.70315, -0.643521, -0.571915, -0.490518, -0.401749, -0.308191, -0.212505, -0.117344, -0.025255, 0.0613999, 0.140522, 0.210336, 0.269434, 0.316802, 0.351833, 0.374316, 0.384411, 0.382614, 0.369705, 0.346688, 0.314733, 0.275115, 0.229165, 0.178228, 0.123634, 0.0666883, 0.008669, -0.0491712, -0.105597, -0.159391, -0.20936, -0.254356, -0.293312, -0.32528, -0.349485, -0.36537, -0.372635, -0.371261, -0.361517, -0.343943, -0.31932, -0.288621, -0.252948, -0.21348, -0.171414, -0.127916, -0.084094, -0.0409705, 0.000521778, 0.0395416, 0.075334, 0.107225, 0.134617, 0.156995, 0.173927, 0.185083, 0.190242, 0.189312, 0.182339, 0.169515, 0.151179, 0.12781, 0.100019, 0.0685237, 0.0341324, -0.0022847, -0.039822, -0.0775646, -0.114613, -0.150106, -0.183238, -0.213275, -0.239561, -0.261528, -0.278692, -0.290656, -0.2971, -0.297782, -0.292533, -0.281256, -0.263924, -0.240594, -0.211408, -0.176606, -0.136535, -0.0916541, -0.0425411, 0.0101146, 0.0655201, 0.122796, 0.180998, 0.239143, 0.296236, 0.351296, 0.403379, 0.451595, 0.495119, 0.533202, 0.565172, 0.590432, 0.608464, 0.618826, 0.621159, 0.61519, 0.600739, 0.577739, 0.546238, 0.50642, 0.458609, 0.403279, 0.341056, 0.27271, 0.199155, 0.121428, 0.040675, -0.0418709, -0.124912, -0.20711, -0.287114, -0.363584, -0.435211, -0.50075, -0.559031, -0.608988, -0.649674, -0.680278, -0.700144, -0.708783, -0.705885, -0.691332, -0.665206, -0.627793, -0.579588, -0.521295, -0.453819, -0.378257, -0.295887, -0.208141, -0.116588, -0.0228969, 0.0711926, 0.163906, 0.253471, 0.338154, 0.416297, 0.486347, 0.546893, 0.596685, 0.634665, 0.659985, 0.672022, 0.670395, 0.654977, 0.625897, 0.583551, 0.528596, 0.461944, 0.384756, 0.29842, 0.20453, 0.104858, 0.00131738, -0.104072, -0.209227, -0.312048, -0.41046, -0.502459, -0.586153, -0.659805, -0.721868, -0.771016, -0.806172, -0.82653, -0.83157, -0.821072, -0.795116, -0.754088, -0.698667, -0.629817, -0.54877, -0.457, -0.356202, -0.248254, -0.135187, -0.0191396, 0.0976789, 0.213036, 0.324721, 0.430589, 0.528608, 0.616903, 0.693791, 0.75782, 0.807803, 0.842835, 0.862318, 0.865969, 0.853826, 0.826243, 0.783887, 0.727716, 0.658961, 0.5791, 0.489824, 0.393007, 0.290661, 0.184902, 0.0778977, -0.0281688, -0.13115, -0.228976, -0.3197, -0.401535, -0.472898, -0.532437, -0.579061, -0.611964, -0.630636, -0.634877, -0.624795, -0.600805, -0.563612, -0.5142, -0.453801, -0.383877, -0.306075, -0.222201, -0.134177, -0.0439949, 0.046317, 0.134744, 0.219325, 0.298195, 0.369623, 0.432049, 0.484116, 0.524703, 0.55294, 0.568235, 0.57028, 0.559058, 0.534843, 0.498194, 0.44994, 0.391157, 0.323152, 0.247425, 0.165638, 0.0795784, -0.00888177, -0.0978264, -0.185337, -0.269535, -0.34862, -0.420909, -0.484871, -0.53916, -0.582641, -0.614413, -0.633829, -0.640508, -0.634341, -0.615494, -0.584404, -0.541766, -0.488521, -0.425834, -0.355067, -0.27775, -0.195548, -0.110224, -0.0236011, 0.0624771, 0.146186, 0.22576, 0.299528, 0.365949, 0.423648, 0.47144, 0.508355, 0.533661, 0.546877, 0.54778, 0.536413, 0.513081, 0.478344, 0.433006, 0.378096, 0.314847, 0.244668, 0.169116, 0.0898613, 0.00865284, -0.0727201, -0.152465, -0.228824, -0.300111, -0.364748, -0.421297, -0.468493, -0.505265, -0.530764, -0.544376, -0.545739, -0.534747, -0.511551, -0.476562, -0.430436, -0.374062, -0.308547, -0.235192, -0.155462, -0.070961, 0.0166022, 0.10545, 0.19377, 0.279753, 0.361633, 0.437716, 0.506422, 0.566314, 0.616126, 0.654793, 0.681469, 0.695547, 0.69667, 0.68474, 0.659918, 0.622622, 0.57352, 0.513513, 0.443722, 0.365463, 0.28022, 0.189619, 0.0953925, -0.000653582, -0.096675, -0.190827, -0.281298, -0.36635, -0.444348, -0.513797, -0.573368, -0.621924, -0.658547, -0.682551, -0.693499, -0.691209, -0.675762, -0.647493, -0.606992, -0.555087, -0.49283, -0.421475, -0.342456, -0.257353, -0.167866, -0.0757785, 0.017075, 0.108849, 0.197719, 0.281922, 0.359788, 0.429774, 0.490493, 0.540746, 0.57954, 0.60611, 0.619936, 0.62075, 0.608542, 0.583563, 0.546317, 0.497551, 0.438243, 0.36958, 0.292933, 0.209835, 0.121945, 0.031021, -0.0611213, -0.152639, -0.241703, -0.326528, -0.405414, -0.476779, -0.539184, -0.591371, -0.63228, -0.661075, -0.677157, -0.680179, -0.670053, -0.646947, -0.611288, -0.56375, -0.505241, -0.436887, -0.360008, -0.27609, -0.186761, -0.093752, 0.00113113, 0.096048, 0.189158, 0.278659, 0.362819, 0.440016, 0.508764, 0.56775, 0.615851, 0.652163, 0.676019, 0.686995, 0.684927, 0.669907, 0.642286, 0.602664, 0.551877, 0.490984, 0.42124, 0.344075, 0.261064, 0.173893, 0.084327, -0.00582433, -0.0947464, -0.180654, -0.261829, -0.336653, -0.403642, -0.461474, -0.509017, -0.545352, -0.569791, -0.58189, -0.581458, -0.568563, -0.543524, -0.506913, -0.459536, -0.40242, -0.336788, -0.264041, -0.185722, -0.103486, -0.0190691, 0.0657496, 0.149185, 0.229481, 0.304947, 0.373996, 0.435169, 0.487175, 0.528911, 0.559484, 0.578237, 0.584752, 0.578867, 0.560676, 0.530526, 0.489013, 0.436968, 0.37544, 0.305674, 0.229086, 0.147237, 0.0617948, -0.0254949, -0.112847, -0.198473, -0.280619, -0.357597, -0.427829, -0.489868, -0.542438, -0.584454, -0.615047, -0.633583, -0.639671, -0.63318, -0.614233, -0.583209, -0.540735, -0.487675, -0.425107, -0.354306, -0.276716, -0.19392, -0.107608, -0.0195427, 0.0684774, 0.154655, 0.237232, 0.314522, 0.38495, 0.447081, 0.499652, 0.541598, 0.572071, 0.590462, 0.59641, 0.589812, 0.570825, 0.53986, 0.497578, 0.444873, 0.382856, 0.312831, 0.236268, 0.154774, 0.0700586, -0.0161005, -0.101894, -0.185519, -0.265216, -0.339303, -0.406215, -0.464533, -0.513012, -0.550613, -0.576516, -0.590146, -0.591178, -0.579548, -0.555455, -0.519353, -0.471947, -0.414178, -0.347198, -0.272357, -0.191165, -0.10527, -0.0164188, 0.073578, 0.16288, 0.249658, 0.33213, 0.408597, 0.477478, 0.537346, 0.586952, 0.625254, 0.651439, 0.664936, 0.665432, 0.652878, 0.627489, 0.58974, 0.540357, 0.480304, 0.410759, 0.333097, 0.248857, 0.159711, 0.0674351, -0.0261335, -0.119129, -0.209695, -0.296025, -0.376396, -0.449207, -0.513007, -0.566529, -0.608711, -0.638723, -0.655978, -0.660151, -0.651177, -0.62926, -0.594865, -0.548709, -0.491746, -0.425148, -0.350283, -0.268685, -0.182023, -0.092067, -0.000653902, 0.090352, 0.179096, 0.26377, 0.342651, 0.414133, 0.476765, 0.529273, 0.570591, 0.599883, 0.616555, 0.620274, 0.610969, 0.588832, 0.55432, 0.50814, 0.451237, 0.384774, 0.310108, 0.228763, 0.142401, 0.0527833, -0.0382607, -0.128873, -0.217203, -0.301448, -0.379886, -0.450914, -0.513078, -0.565104, -0.605922, -0.634691, -0.650812, -0.653943, -0.644004, -0.621181, -0.585917, -0.538911, -0.481096, -0.413624, -0.337842, -0.255265, -0.167544, -0.0764326, 0.0162459, 0.108639, 0.1989, 0.285227, 0.365898, 0.439306, 0.503994, 0.558678, 0.602281, 0.633947, 0.653066, 0.659277, 0.652484, 0.63285, 0.600802, 0.557012, 0.502392, 0.43807, 0.365368, 0.285779, 0.200928, 0.112549, 0.0224408, -0.0675632, -0.155636, -0.239992, -0.318924, -0.390839, -0.454289, -0.508, -0.550898, -0.582132, -0.601091, -0.607413, -0.600996, -0.581999, -0.550836, -0.508172, -0.454905, -0.392147, -0.321208, -0.24356, -0.160814, -0.0746832, 0.0130505, 0.100573, 0.186077, 0.267794, 0.344036, 0.413226, 0.473934, 0.5249, 0.565067, 0.593597, 0.609891, 0.613598, 0.604627, 0.583145, 0.549575, 0.504588, 0.449086, 0.384188, 0.311206, 0.231615, 0.147028, 0.0591581, -0.0302115, -0.119268, -0.206203, -0.289251, -0.366725, -0.437051, -0.498799, -0.550714, -0.59174, -0.621042, -0.638023, -0.642338, -0.633896, -0.612867, -0.579679, -0.535002, -0.479745, -0.415027, -0.342162, -0.262629, -0.178041, -0.090116, -0.000636451, 0.0885824, 0.175731, 0.259044, 0.336833, 0.407524, 0.469686, 0.522066, 0.563607, 0.593477, 0.611078, 0.616067, 0.608356, 0.588117, 0.555778, 0.512014, 0.457733, 0.394059, 0.322306, 0.243956, 0.160623, 0.0740258, -0.0140522, -0.101796, -0.187396, -0.269088, -0.345187, -0.414119, -0.474461, -0.524961, -0.564569, -0.592459, -0.608041, -0.610979, -0.601196, -0.578872, -0.544446, -0.498604, -0.442267, -0.37657, -0.30284, -0.22257, -0.137388, -0.049024, 0.0407257, 0.130034, 0.217083, 0.300096, 0.377378, 0.447349, 0.508576, 0.559802, 0.599972, 0.628256, 0.644064, 0.647056, 0.637156, 0.614547, 0.579671, 0.533219, 0.476115, 0.409503, 0.334717, 0.25326, 0.166769, 0.0769835, -0.0142884, -0.105209, -0.193948, -0.278716, -0.357809, -0.429633, -0.492745, -0.545875, -0.587959, -0.618156, -0.635865, -0.640739, -0.632693, -0.611902, -0.578802, -0.534076, -0.478646, -0.413649, -0.340418, -0.260449, -0.17538, -0.0869485, 0.00303945, 0.0927462, 0.180341, 0.264035, 0.342123, 0.41301, 0.47525, 0.527575, 0.568917, 0.598434, 0.615523, 0.619836, 0.611287, 0.590048, 0.556555, 0.511489, 0.455771, 0.390538, 0.31712, 0.237016, 0.151861, 0.0633922, -0.0265837, -0.116229, -0.203713, -0.287248, -0.365126, -0.435753, -0.497683, -0.549647, -0.590577, -0.61963, -0.636203, -0.639948, -0.630778, -0.608866, -0.574646, -0.528801, -0.472252, -0.406135, -0.331782, -0.250693, -0.164503, -0.0749513, 0.0161537, 0.106973, 0.195673, 0.280463, 0.359633, 0.431586, 0.494873, 0.548219, 0.590553, 0.621026, 0.639032, 0.644218, 0.636488, 0.616013, 0.583219, 0.538783, 0.483619, 0.418858, 0.345823, 0.266009, 0.181043, 0.0926589, 0.00265804, -0.0871266, -0.174868, -0.25878, -0.337159, -0.408412, -0.471094, -0.523934, -0.565865, -0.59604, -0.613855, -0.618956, -0.611251, -0.590907, -0.558351, -0.514258, -0.459539, -0.395322, -0.322927, -0.243843, -0.159694, -0.0722063, 0.0168233, 0.105569, 0.192209, 0.274966, 0.352142, 0.422151, 0.483557, 0.535095, 0.575704, 0.604546} + }, + { + "Scenario: SongbirdFilterModule: Blend mode-right", + {1.45068e-17, -0.0168587, -0.0653725, -0.140111, -0.233549, -0.337159, -0.442548, -0.542417, -0.631223, -0.705456, -0.763528, -0.80536, -0.831785, -0.843896, -0.842506, -0.827776, -0.799091, -0.755167, -0.694331, -0.614909, -0.515627, -0.395961, -0.256366, -0.0983899, 0.0753426, 0.261235, 0.454886, 0.651281, 0.844989, 1.03037, 1.20175, 1.35366, 1.48105, 1.57952, 1.64556, 1.67675, 1.67191, 1.63113, 1.55574, 1.44807, 1.31129, 1.14899, 0.964993, 0.763024, 0.546629, 0.319147, 0.0838115, -0.156048, -0.396782, -0.634225, -0.863581, -1.07945, -1.27598, -1.44722, -1.58746, -1.69175, -1.75626, -1.77861, -1.75807, -1.69555, -1.59348, -1.45549, -1.2861, -1.09032, -0.873335, -0.64023, -0.395852, -0.144753, 0.108772, 0.360596, 0.60665, 0.84284, 1.065, 1.26894, 1.45047, 1.60562, 1.73074, 1.8227, 1.87905, 1.89817, 1.87935, 1.82283, 1.72985, 1.60257, 1.44405, 1.25812, 1.04926, 0.822472, 0.583096, 0.336619, 0.088494, -0.156037, -0.392103, -0.615329, -0.821909, -1.00864, -1.17291, -1.31266, -1.42629, -1.51265, -1.57094, -1.60074, -1.60199, -1.57507, -1.52084, -1.44071, -1.33673, -1.21154, -1.06838, -0.910976, -0.743411, -0.569959, -0.394889, -0.222283, -0.055881, 0.101034, 0.245705, 0.375922, 0.489995, 0.586705, 0.665244, 0.725148, 0.766253, 0.788668, 0.792765, 0.779195, 0.748903, 0.70315, 0.643521, 0.571915, 0.490518, 0.401749, 0.308191, 0.212505, 0.117344, 0.025255, -0.0613999, -0.140522, -0.210336, -0.269434, -0.316802, -0.351833, -0.374316, -0.384411, -0.382614, -0.369705, -0.346688, -0.314733, -0.275115, -0.229165, -0.178228, -0.123634, -0.0666883, -0.008669, 0.0491712, 0.105597, 0.159391, 0.20936, 0.254356, 0.293312, 0.32528, 0.349485, 0.36537, 0.372635, 0.371261, 0.361517, 0.343943, 0.31932, 0.288621, 0.252948, 0.21348, 0.171414, 0.127916, 0.084094, 0.0409705, -0.000521778, -0.0395416, -0.075334, -0.107225, -0.134617, -0.156995, -0.173927, -0.185083, -0.190242, -0.189312, -0.182339, -0.169515, -0.151179, -0.12781, -0.100019, -0.0685237, -0.0341324, 0.0022847, 0.039822, 0.0775646, 0.114613, 0.150106, 0.183238, 0.213275, 0.239561, 0.261528, 0.278692, 0.290656, 0.2971, 0.297782, 0.292533, 0.281256, 0.263924, 0.240594, 0.211408, 0.176606, 0.136535, 0.0916541, 0.0425411, -0.0101146, -0.0655201, -0.122796, -0.180998, -0.239143, -0.296236, -0.351296, -0.403379, -0.451595, -0.495119, -0.533202, -0.565172, -0.590432, -0.608464, -0.618826, -0.621159, -0.61519, -0.600739, -0.577739, -0.546238, -0.50642, -0.458609, -0.403279, -0.341056, -0.27271, -0.199155, -0.121428, -0.040675, 0.0418709, 0.124912, 0.20711, 0.287114, 0.363584, 0.435211, 0.50075, 0.559031, 0.608988, 0.649674, 0.680278, 0.700144, 0.708783, 0.705885, 0.691332, 0.665206, 0.627793, 0.579588, 0.521295, 0.453819, 0.378257, 0.295887, 0.208141, 0.116588, 0.0228969, -0.0711926, -0.163906, -0.253471, -0.338154, -0.416297, -0.486347, -0.546893, -0.596685, -0.634665, -0.659985, -0.672022, -0.670395, -0.654977, -0.625897, -0.583551, -0.528596, -0.461944, -0.384756, -0.29842, -0.20453, -0.104858, -0.00131738, 0.104072, 0.209227, 0.312048, 0.41046, 0.502459, 0.586153, 0.659805, 0.721868, 0.771016, 0.806172, 0.82653, 0.83157, 0.821072, 0.795116, 0.754088, 0.698667, 0.629817, 0.54877, 0.457, 0.356202, 0.248254, 0.135187, 0.0191396, -0.0976789, -0.213036, -0.324721, -0.430589, -0.528608, -0.616903, -0.693791, -0.75782, -0.807803, -0.842835, -0.862318, -0.865969, -0.853826, -0.826243, -0.783887, -0.727716, -0.658961, -0.5791, -0.489824, -0.393007, -0.290661, -0.184902, -0.0778977, 0.0281688, 0.13115, 0.228976, 0.3197, 0.401535, 0.472898, 0.532437, 0.579061, 0.611964, 0.630636, 0.634877, 0.624795, 0.600805, 0.563612, 0.5142, 0.453801, 0.383877, 0.306075, 0.222201, 0.134177, 0.0439949, -0.046317, -0.134744, -0.219325, -0.298195, -0.369623, -0.432049, -0.484116, -0.524703, -0.55294, -0.568235, -0.57028, -0.559058, -0.534843, -0.498194, -0.44994, -0.391157, -0.323152, -0.247425, -0.165638, -0.0795784, 0.00888177, 0.0978264, 0.185337, 0.269535, 0.34862, 0.420909, 0.484871, 0.53916, 0.582641, 0.614413, 0.633829, 0.640508, 0.634341, 0.615494, 0.584404, 0.541766, 0.488521, 0.425834, 0.355067, 0.27775, 0.195548, 0.110224, 0.0236011, -0.0624771, -0.146186, -0.22576, -0.299528, -0.365949, -0.423648, -0.47144, -0.508355, -0.533661, -0.546877, -0.54778, -0.536413, -0.513081, -0.478344, -0.433006, -0.378096, -0.314847, -0.244668, -0.169116, -0.0898613, -0.00865284, 0.0727201, 0.152465, 0.228824, 0.300111, 0.364748, 0.421297, 0.468493, 0.505265, 0.530764, 0.544376, 0.545739, 0.534747, 0.511551, 0.476562, 0.430436, 0.374062, 0.308547, 0.235192, 0.155462, 0.070961, -0.0166022, -0.10545, -0.19377, -0.279753, -0.361633, -0.437716, -0.506422, -0.566314, -0.616126, -0.654793, -0.681469, -0.695547, -0.69667, -0.68474, -0.659918, -0.622622, -0.57352, -0.513513, -0.443722, -0.365463, -0.28022, -0.189619, -0.0953925, 0.000653582, 0.096675, 0.190827, 0.281298, 0.36635, 0.444348, 0.513797, 0.573368, 0.621924, 0.658547, 0.682551, 0.693499, 0.691209, 0.675762, 0.647493, 0.606992, 0.555087, 0.49283, 0.421475, 0.342456, 0.257353, 0.167866, 0.0757785, -0.017075, -0.108849, -0.197719, -0.281922, -0.359788, -0.429774, -0.490493, -0.540746, -0.57954, -0.60611, -0.619936, -0.62075, -0.608542, -0.583563, -0.546317, -0.497551, -0.438243, -0.36958, -0.292933, -0.209835, -0.121945, -0.031021, 0.0611213, 0.152639, 0.241703, 0.326528, 0.405414, 0.476779, 0.539184, 0.591371, 0.63228, 0.661075, 0.677157, 0.680179, 0.670053, 0.646947, 0.611288, 0.56375, 0.505241, 0.436887, 0.360008, 0.27609, 0.186761, 0.093752, -0.00113113, -0.096048, -0.189158, -0.278659, -0.362819, -0.440016, -0.508764, -0.56775, -0.615851, -0.652163, -0.676019, -0.686995, -0.684927, -0.669907, -0.642286, -0.602664, -0.551877, -0.490984, -0.42124, -0.344075, -0.261064, -0.173893, -0.084327, 0.00582433, 0.0947464, 0.180654, 0.261829, 0.336653, 0.403642, 0.461474, 0.509017, 0.545352, 0.569791, 0.58189, 0.581458, 0.568563, 0.543524, 0.506913, 0.459536, 0.40242, 0.336788, 0.264041, 0.185722, 0.103486, 0.0190691, -0.0657496, -0.149185, -0.229481, -0.304947, -0.373996, -0.435169, -0.487175, -0.528911, -0.559484, -0.578237, -0.584752, -0.578867, -0.560676, -0.530526, -0.489013, -0.436968, -0.37544, -0.305674, -0.229086, -0.147237, -0.0617948, 0.0254949, 0.112847, 0.198473, 0.280619, 0.357597, 0.427829, 0.489868, 0.542438, 0.584454, 0.615047, 0.633583, 0.639671, 0.63318, 0.614233, 0.583209, 0.540735, 0.487675, 0.425107, 0.354306, 0.276716, 0.19392, 0.107608, 0.0195427, -0.0684774, -0.154655, -0.237232, -0.314522, -0.38495, -0.447081, -0.499652, -0.541598, -0.572071, -0.590462, -0.59641, -0.589812, -0.570825, -0.53986, -0.497578, -0.444873, -0.382856, -0.312831, -0.236268, -0.154774, -0.0700586, 0.0161005, 0.101894, 0.185519, 0.265216, 0.339303, 0.406215, 0.464533, 0.513012, 0.550613, 0.576516, 0.590146, 0.591178, 0.579548, 0.555455, 0.519353, 0.471947, 0.414178, 0.347198, 0.272357, 0.191165, 0.10527, 0.0164188, -0.073578, -0.16288, -0.249658, -0.33213, -0.408597, -0.477478, -0.537346, -0.586952, -0.625254, -0.651439, -0.664936, -0.665432, -0.652878, -0.627489, -0.58974, -0.540357, -0.480304, -0.410759, -0.333097, -0.248857, -0.159711, -0.0674351, 0.0261335, 0.119129, 0.209695, 0.296025, 0.376396, 0.449207, 0.513007, 0.566529, 0.608711, 0.638723, 0.655978, 0.660151, 0.651177, 0.62926, 0.594865, 0.548709, 0.491746, 0.425148, 0.350283, 0.268685, 0.182023, 0.092067, 0.000653902, -0.090352, -0.179096, -0.26377, -0.342651, -0.414133, -0.476765, -0.529273, -0.570591, -0.599883, -0.616555, -0.620274, -0.610969, -0.588832, -0.55432, -0.50814, -0.451237, -0.384774, -0.310108, -0.228763, -0.142401, -0.0527833, 0.0382607, 0.128873, 0.217203, 0.301448, 0.379886, 0.450914, 0.513078, 0.565104, 0.605922, 0.634691, 0.650812, 0.653943, 0.644004, 0.621181, 0.585917, 0.538911, 0.481096, 0.413624, 0.337842, 0.255265, 0.167544, 0.0764326, -0.0162459, -0.108639, -0.1989, -0.285227, -0.365898, -0.439306, -0.503994, -0.558678, -0.602281, -0.633947, -0.653066, -0.659277, -0.652484, -0.63285, -0.600802, -0.557012, -0.502392, -0.43807, -0.365368, -0.285779, -0.200928, -0.112549, -0.0224408, 0.0675632, 0.155636, 0.239992, 0.318924, 0.390839, 0.454289, 0.508, 0.550898, 0.582132, 0.601091, 0.607413, 0.600996, 0.581999, 0.550836, 0.508172, 0.454905, 0.392147, 0.321208, 0.24356, 0.160814, 0.0746832, -0.0130505, -0.100573, -0.186077, -0.267794, -0.344036, -0.413226, -0.473934, -0.5249, -0.565067, -0.593597, -0.609891, -0.613598, -0.604627, -0.583145, -0.549575, -0.504588, -0.449086, -0.384188, -0.311206, -0.231615, -0.147028, -0.0591581, 0.0302115, 0.119268, 0.206203, 0.289251, 0.366725, 0.437051, 0.498799, 0.550714, 0.59174, 0.621042, 0.638023, 0.642338, 0.633896, 0.612867, 0.579679, 0.535002, 0.479745, 0.415027, 0.342162, 0.262629, 0.178041, 0.090116, 0.000636451, -0.0885824, -0.175731, -0.259044, -0.336833, -0.407524, -0.469686, -0.522066, -0.563607, -0.593477, -0.611078, -0.616067, -0.608356, -0.588117, -0.555778, -0.512014, -0.457733, -0.394059, -0.322306, -0.243956, -0.160623, -0.0740258, 0.0140522, 0.101796, 0.187396, 0.269088, 0.345187, 0.414119, 0.474461, 0.524961, 0.564569, 0.592459, 0.608041, 0.610979, 0.601196, 0.578872, 0.544446, 0.498604, 0.442267, 0.37657, 0.30284, 0.22257, 0.137388, 0.049024, -0.0407257, -0.130034, -0.217083, -0.300096, -0.377378, -0.447349, -0.508576, -0.559802, -0.599972, -0.628256, -0.644064, -0.647056, -0.637156, -0.614547, -0.579671, -0.533219, -0.476115, -0.409503, -0.334717, -0.25326, -0.166769, -0.0769835, 0.0142884, 0.105209, 0.193948, 0.278716, 0.357809, 0.429633, 0.492745, 0.545875, 0.587959, 0.618156, 0.635865, 0.640739, 0.632693, 0.611902, 0.578802, 0.534076, 0.478646, 0.413649, 0.340418, 0.260449, 0.17538, 0.0869485, -0.00303945, -0.0927462, -0.180341, -0.264035, -0.342123, -0.41301, -0.47525, -0.527575, -0.568917, -0.598434, -0.615523, -0.619836, -0.611287, -0.590048, -0.556555, -0.511489, -0.455771, -0.390538, -0.31712, -0.237016, -0.151861, -0.0633922, 0.0265837, 0.116229, 0.203713, 0.287248, 0.365126, 0.435753, 0.497683, 0.549647, 0.590577, 0.61963, 0.636203, 0.639948, 0.630778, 0.608866, 0.574646, 0.528801, 0.472252, 0.406135, 0.331782, 0.250693, 0.164503, 0.0749513, -0.0161537, -0.106973, -0.195673, -0.280463, -0.359633, -0.431586, -0.494873, -0.548219, -0.590553, -0.621026, -0.639032, -0.644218, -0.636488, -0.616013, -0.583219, -0.538783, -0.483619, -0.418858, -0.345823, -0.266009, -0.181043, -0.0926589, -0.00265804, 0.0871266, 0.174868, 0.25878, 0.337159, 0.408412, 0.471094, 0.523934, 0.565865, 0.59604, 0.613855, 0.618956, 0.611251, 0.590907, 0.558351, 0.514258, 0.459539, 0.395322, 0.322927, 0.243843, 0.159694, 0.0722063, -0.0168233, -0.105569, -0.192209, -0.274966, -0.352142, -0.422151, -0.483557, -0.535095, -0.575704, -0.604546} + }, + }; +} + + + diff --git a/ports-juce7/syndicate/WECore/Tests/PerformanceTests.cpp b/ports-juce7/syndicate/WECore/Tests/PerformanceTests.cpp new file mode 100644 index 00000000..cae67cdd --- /dev/null +++ b/ports-juce7/syndicate/WECore/Tests/PerformanceTests.cpp @@ -0,0 +1,250 @@ +/* + * File: PerformanceTests.cpp + * + * Created: 26/05/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include +#include +#include +#include +#include +#include + +#include "CarveDSP/CarveDSPUnit.h" +#include "SongbirdFilters/SongbirdFilterModule.h" + + +/** + * Contains most of the useful stuff for the performance tests. + * If it gets much larger then it's probably more useful to do some refactoring and move this + * elsewhere. + */ +namespace { + /** + * Contains performance stats + */ + struct Stats { + double average; + double deviation; + }; + std::ostream& operator<< (std::ostream& stream, const Stats& stats) { + stream << "Average: " << stats.average << " Deviation: " << stats.deviation; + return stream; + } + + /** + * Contains limits for multiple performance critera + */ + struct Limits { + const double INDIVIDUAL; + const double AVERAGE; + const double DEVIATION; + }; + + /** + * Does pretty much what it says. + * clang complains if this isn't inlined + */ + inline Stats calcAverageAndDeviation(const std::vector& executionTimes) { + Stats retVal; + + // calculate the average first + retVal.average = 0; + for (double time : executionTimes) { + retVal.average += time; + } + retVal.average = retVal.average / static_cast(executionTimes.size()); + + // now calculate deviation + retVal.deviation = 0; + for (const double& time : executionTimes) { + retVal.deviation += std::pow((time - retVal.average), 2); + } + retVal.deviation = retVal.deviation / (static_cast(executionTimes.size()) - 1); + retVal.deviation = std::sqrt(retVal.deviation); + + return retVal; + } + + bool isNewRun {true}; + inline void appendToResultsFile(const Stats& stats, const std::string& testName) { + const std::string FILE_PATH("wecore_performance.log"); + std::ofstream outStream; + outStream.open(FILE_PATH, std::ios_base::app); + + if (isNewRun) { + isNewRun = false; + + std::time_t now {std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())}; + outStream << std::endl << std::endl << "**** New Test Run: " + << std::put_time(std::localtime(&now), "%F %T"); + } + outStream << std::endl << testName << ": " << stats; + + } +} + + +/*** TESTS BEGIN HERE ***/ + +SCENARIO("Performance: CarveDSPUnit, 100 buffers of 1024 samples each") { + GIVEN("A CarveDSPUnit and a buffer of samples") { + + const int NUM_BUFFERS {100}; + std::vector buffer(1024); + WECore::Carve::CarveDSPUnit mCarve; + + // set the performance limits + Limits mLimits{0.11, 0.07, 0.01}; + + // store the execution time for each buffer + std::vector executionTimes; + + WHEN("The samples are processed") { + // turn the unit on + mCarve.setMode(2); + + for (int nbuf {0}; nbuf < NUM_BUFFERS; nbuf++) { + + // fill the buffer with a sine wave + int iii {0}; + std::generate(buffer.begin(), buffer.end(), [&iii]{ return std::sin(iii++); }); + + + // do processing + const clock_t startTime {clock()}; + for (size_t jjj {0}; jjj < buffer.size(); jjj++) { + buffer[jjj] = mCarve.process(buffer[jjj]); + } + const clock_t endTime {clock()}; + + // calculate the execution time + const double CLOCKS_PER_MICROSEC {static_cast(CLOCKS_PER_SEC) / 1000}; + const double executionTime {static_cast(endTime - startTime) / CLOCKS_PER_MICROSEC}; + executionTimes.push_back(executionTime); + CHECK(executionTime < mLimits.INDIVIDUAL); + } + + THEN("The average and variance are within limits") { + Stats mStats = calcAverageAndDeviation(executionTimes); + CHECK(mStats.average < mLimits.AVERAGE); + CHECK(mStats.deviation < mLimits.DEVIATION); + + appendToResultsFile(mStats, Catch::getResultCapture().getCurrentTestName()); + } + } + } +} + +SCENARIO("Performance: SongbirdFilterModule (blend mode), 100 buffers of 1024 samples each") { + GIVEN("A SongbirdFilterModule and a buffer of samples") { + + const int NUM_BUFFERS {100}; + std::vector leftBuffer(1024); + std::vector rightBuffer(1024); + WECore::Songbird::SongbirdFilterModule mSongbird; + mSongbird.setModMode(false); + + // set the performance limits + Limits mLimits{1.8, 1.5, 0.12}; + + // store the execution time for each buffer + std::vector executionTimes; + + WHEN("The samples are processed") { + + for (int nbuf {0}; nbuf < NUM_BUFFERS; nbuf++) { + + // fill the buffer with a sine wave + int iii {0}; + std::generate(leftBuffer.begin(), leftBuffer.end(), [&iii]{ return std::sin(iii++); }); + rightBuffer = leftBuffer; + + + // do processing + const clock_t startTime {clock()}; + mSongbird.Process2in2out(&leftBuffer[0], &rightBuffer[0], leftBuffer.size()); + const clock_t endTime {clock()}; + + // calculate the execution time + const double CLOCKS_PER_MICROSEC {static_cast(CLOCKS_PER_SEC) / 1000}; + const double executionTime {static_cast(endTime - startTime) / CLOCKS_PER_MICROSEC}; + executionTimes.push_back(executionTime); + CHECK(executionTime < mLimits.INDIVIDUAL); + } + + THEN("The average and variance are within limits") { + Stats mStats = calcAverageAndDeviation(executionTimes); + CHECK(mStats.average < mLimits.AVERAGE); + CHECK(mStats.deviation < mLimits.DEVIATION); + + appendToResultsFile(mStats, Catch::getResultCapture().getCurrentTestName()); + } + } + } +} + +SCENARIO("Performance: SongbirdFilterModule (freq mode), 100 buffers of 1024 samples each") { + GIVEN("A SongbirdFilterModule and a buffer of samples") { + + const int NUM_BUFFERS {100}; + std::vector leftBuffer(1024); + std::vector rightBuffer(1024); + WECore::Songbird::SongbirdFilterModule mSongbird; + mSongbird.setModMode(true); + + // set the performance limits + Limits mLimits{1.8, 1.5, 0.12}; + + // store the execution time for each buffer + std::vector executionTimes; + + WHEN("The samples are processed") { + + for (int nbuf {0}; nbuf < NUM_BUFFERS; nbuf++) { + + // fill the buffer with a sine wave + int iii {0}; + std::generate(leftBuffer.begin(), leftBuffer.end(), [&iii]{ return std::sin(iii++); }); + rightBuffer = leftBuffer; + + + // do processing + const clock_t startTime {clock()}; + mSongbird.Process2in2out(&leftBuffer[0], &rightBuffer[0], leftBuffer.size()); + const clock_t endTime {clock()}; + + // calculate the execution time + const double CLOCKS_PER_MICROSEC {static_cast(CLOCKS_PER_SEC) / 1000}; + const double executionTime {static_cast(endTime - startTime) / CLOCKS_PER_MICROSEC}; + executionTimes.push_back(executionTime); + CHECK(executionTime < mLimits.INDIVIDUAL); + } + + THEN("The average and variance are within limits") { + Stats mStats = calcAverageAndDeviation(executionTimes); + CHECK(mStats.average < mLimits.AVERAGE); + CHECK(mStats.deviation < mLimits.DEVIATION); + + appendToResultsFile(mStats, Catch::getResultCapture().getCurrentTestName()); + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/Tests/catchMain.cpp b/ports-juce7/syndicate/WECore/Tests/catchMain.cpp new file mode 100644 index 00000000..c5d7da88 --- /dev/null +++ b/ports-juce7/syndicate/WECore/Tests/catchMain.cpp @@ -0,0 +1,23 @@ +/* + * File: catchMain.cpp + * + * Created: 26/12/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#define CATCH_CONFIG_MAIN +#include "catch.hpp" diff --git a/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerBase.h b/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerBase.h new file mode 100644 index 00000000..83f2aa5c --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerBase.h @@ -0,0 +1,199 @@ +/* + * File: AREnveloperFollowerBase.h + * + * Created: 27/05/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "AREnvelopeFollowerParameters.h" +#include "ModulationSource.h" +#include "TPTSVFilter.h" + +namespace WECore::AREnv { + /** + * Base class for an envelope follower with controls for attack and release times, with optional + * low/high cut filters before the envelope stage. + */ + class AREnvelopeFollowerBase : public ModulationSource { + public: + AREnvelopeFollowerBase() : _attackTimeMs(Parameters::ATTACK_MS.defaultValue), + _releaseTimeMs(Parameters::RELEASE_MS.defaultValue), + _filterEnabled(Parameters::FILTER_ENABLED.defaultValue) { + // call this here rather than setting it in initialiser list so that the coefficients get + // setup + reset(); + setSampleRate(44100); + + _lowCutFilter.setCutoff(Parameters::LOW_CUT.defaultValue); + _highCutFilter.setCutoff(Parameters::HIGH_CUT.defaultValue); + _lowCutFilter.setMode(TPTSVF::Parameters::FILTER_MODE.HIGHPASS); + _highCutFilter.setMode(TPTSVF::Parameters::FILTER_MODE.LOWPASS); + } + + virtual ~AREnvelopeFollowerBase() override = default; + + /** @name Setter Methods */ + /** @{ */ + + /** + * Sets the sample rate the envelope will operate at. + * It is recommended that you call this at some point before calling clockUpdateEnvelope. + * + * @param[in] sampleRate The sample rate in Hz + */ + inline void setSampleRate(double sampleRate); + + /** + * Sets the attack time of the envelope. + * + * @see ATTACK_MS for valid values + * + * @param[in] time Attack time in milliseconds + */ + inline void setAttackTimeMs(double time); + + /** + * Sets the release time of the envelope. + * + * @see RELEASE_MS for valid values + * + * @param[in] time Release time in milliseconds + */ + inline void setReleaseTimeMs(double time); + + /** + * Sets enables or disables the filter in front of the envelope. + * + * @see FILTER_ENABLED for valid values + * + * @param[in] isEnabled Set to true to enabled the filter. + */ + void setFilterEnabled(bool isEnabled) { _filterEnabled = isEnabled; } + + /** + * Sets the low cut applied before the envelope. + * + * @see LOW_CUT for valid values + * + * @param[in] freq Cutoff frequency in Hz + */ + void setLowCutHz(double freq) { _lowCutFilter.setCutoff(freq); } + + /** + * Sets the high cut applied before the envelope. + * + * @see HIGH_CUT for valid values + * + * @param[in] freq Cutoff frequency in Hz + */ + void setHighCutHz(double freq) { _highCutFilter.setCutoff(freq); } + /** @} */ + + /** @name Getter Methods */ + /** @{ */ + + /** + * @see setAttackTimeMs + */ + double getAttackTimeMs() const { return _attackTimeMs; } + + /** + * @see setReleaseTimeMs + */ + double getReleaseTimeMs() const { return _releaseTimeMs; } + + /** + * @see setFilterEnabled + */ + bool getFilterEnabled() const { return _filterEnabled; } + + /** + * @see setLowCutHz + */ + double getLowCutHz() const { return _lowCutFilter.getCutoff(); } + + /** + * @see setHighCutHz + */ + double getHighCutHz() const { return _highCutFilter.getCutoff(); } + /** @} */ + + protected: + double _envVal; + + double _attackTimeMs; + double _releaseTimeMs; + + double _attackCoef; + double _releaseCoef; + + bool _filterEnabled; + TPTSVF::TPTSVFilter _lowCutFilter; + TPTSVF::TPTSVFilter _highCutFilter; + + double _sampleRate; + + /** + * Applies the filters if needed, then calls _envGetNextOutputImpl. + */ + inline double _getNextOutputImpl(double inSample) override; + + virtual inline double _envGetNextOutputImpl(double inSample) = 0; + + inline void _resetImpl() override; + + double _calcCoef(double timeMs) { + return exp(log(0.01) / (timeMs * _sampleRate * 0.001)); + } + }; + + void AREnvelopeFollowerBase::setSampleRate(double sampleRate) { + _sampleRate = sampleRate; + _attackCoef = _calcCoef(_attackTimeMs); + _releaseCoef = _calcCoef(_releaseTimeMs); + + _lowCutFilter.setSampleRate(sampleRate); + _highCutFilter.setSampleRate(sampleRate); + } + + void AREnvelopeFollowerBase::setAttackTimeMs(double time) { + _attackTimeMs = Parameters::ATTACK_MS.BoundsCheck(time); + _attackCoef = _calcCoef(_attackTimeMs); + } + + void AREnvelopeFollowerBase::setReleaseTimeMs(double time) { + _releaseTimeMs = Parameters::RELEASE_MS.BoundsCheck(time); + _releaseCoef = _calcCoef(_releaseTimeMs); + } + + double AREnvelopeFollowerBase::_getNextOutputImpl(double inSample) { + if (_filterEnabled) { + _lowCutFilter.processBlock(&inSample, 1); + _highCutFilter.processBlock(&inSample, 1); + } + + return _envGetNextOutputImpl(inSample); + } + + void AREnvelopeFollowerBase::_resetImpl() { + _envVal = 0; + _lowCutFilter.reset(); + _highCutFilter.reset(); + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerFullWave.h b/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerFullWave.h new file mode 100644 index 00000000..630591ff --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerFullWave.h @@ -0,0 +1,61 @@ +/* + * File: AREnveloperFollowerFullWave.h + * + * Created: 17/11/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/CoreMath.h" +#include "AREnvelopeFollowerParameters.h" +#include "AREnvelopeFollowerBase.h" + +namespace WECore::AREnv { + /** + * Implements a full wave rectification envelope follower. + */ + class AREnvelopeFollowerFullWave : public AREnvelopeFollowerBase { + public: + + AREnvelopeFollowerFullWave() = default; + virtual ~AREnvelopeFollowerFullWave() = default; + + /** + * Updates the envelope with the current sample and returns the updated envelope value. Must + * be called for every sample. + * + * @param[in] inSample Sample used to update the envelope state + * + * @return The updated envelope value + */ + virtual double _envGetNextOutputImpl(double inSample) override { + const double tmp = std::abs(inSample); + + if (tmp > _envVal) + { + _envVal = _attackCoef * (_envVal - tmp) + tmp; + } + else + { + _envVal = _releaseCoef * (_envVal - tmp) + tmp; + } + + return _envVal; + } + }; +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerParameters.h b/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerParameters.h new file mode 100644 index 00000000..281c0982 --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerParameters.h @@ -0,0 +1,43 @@ +/* + * File: AREnveloperFollowerParameters.h + * + * Created: 07/05/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/ParameterDefinition.h" +#include "TPTSVFilterParameters.h" + +namespace WECore::AREnv::Parameters { + //@{ + /** + * A parameter which can take any float value between the ranges defined. + * The values passed on construction are in the following order: + * minimum value, + * maximum value, + * default value + */ + const ParameterDefinition::RangedParameter ATTACK_MS(0.1, 10000, 20), + RELEASE_MS(0.1, 10000, 200), + LOW_CUT(TPTSVF::Parameters::CUTOFF.minValue, TPTSVF::Parameters::CUTOFF.maxValue, 0), + HIGH_CUT(TPTSVF::Parameters::CUTOFF.minValue, TPTSVF::Parameters::CUTOFF.maxValue, 20000); + //@} + + const ParameterDefinition::BooleanParameter FILTER_ENABLED(false); +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerSquareLaw.h b/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerSquareLaw.h new file mode 100644 index 00000000..cff86e1f --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/AREnvelopeFollowerSquareLaw.h @@ -0,0 +1,61 @@ +/* + * File: AREnveloperFollowerSquareLaw.h + * + * Created: 17/11/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/CoreMath.h" +#include "AREnvelopeFollowerParameters.h" +#include "AREnvelopeFollowerBase.h" + +namespace WECore::AREnv { + /** + * Implements a real square law envelope follower. + */ + class AREnvelopeFollowerSquareLaw : public AREnvelopeFollowerBase { + public: + + AREnvelopeFollowerSquareLaw() = default; + virtual ~AREnvelopeFollowerSquareLaw() override = default; + + /** + * Updates the envelope with the current sample and returns the updated envelope value. Must + * be called for every sample. + * + * @param[in] inSample Sample used to update the envelope state + * + * @return The updated envelope value + */ + virtual double _envGetNextOutputImpl(double inSample) override { + const double tmp = inSample * inSample; + + if (tmp > _envVal) + { + _envVal = _attackCoef * (_envVal - tmp) + tmp; + } + else + { + _envVal = _releaseCoef * (_envVal - tmp) + tmp; + } + + return std::sqrt(_envVal); + } + }; +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/EffectsProcessor.h b/ports-juce7/syndicate/WECore/WEFilters/EffectsProcessor.h new file mode 100644 index 00000000..ad121fd6 --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/EffectsProcessor.h @@ -0,0 +1,81 @@ +/* + * File: EffectsProcessor.h + * + * Created: 03/12/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +namespace WECore { + + /** + * Base class for EffectsProcessors with different channel configurations to inherit from. + */ + template + class EffectsProcessorBase { + static_assert(std::is_floating_point::value, + "Must be provided with a floating point template type"); + + public: + EffectsProcessorBase() = default; + virtual ~EffectsProcessorBase() = default; + + /** + * Resets the internal state of the processor. + * + * Override this to reset everything as necessary. + */ + inline virtual void reset() {} + }; + + /** + * Provides a standard interface for effects which process a mono buffer of samples. + */ + template + class EffectsProcessor1in1out : public EffectsProcessorBase { + public: + EffectsProcessor1in1out() = default; + virtual ~EffectsProcessor1in1out() override = default; + + virtual void process1in1out(SampleType* inSamples, size_t numSamples) = 0; + }; + + /** + * Provides a standard interface for effects which process a mono to stereo buffer of samples. + */ + template + class EffectsProcessor1in2out : public EffectsProcessorBase { + public: + EffectsProcessor1in2out() = default; + virtual ~EffectsProcessor1in2out() override = default; + + virtual void process1in2out(SampleType* inSamplesLeft, SampleType* inSamplesRight, size_t numSamples) = 0; + }; + + /** + * Provides a standard interface for effects which process stereo buffers of samples. + */ + template + class EffectsProcessor2in2out : public EffectsProcessorBase { + public: + EffectsProcessor2in2out() = default; + virtual ~EffectsProcessor2in2out() override = default; + + virtual void process2in2out(SampleType* inSamplesLeft, SampleType* inSamplesRight, size_t numSamples) = 0; + }; +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/ModulationSource.h b/ports-juce7/syndicate/WECore/WEFilters/ModulationSource.h new file mode 100644 index 00000000..96985d6b --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/ModulationSource.h @@ -0,0 +1,103 @@ +/* + * File: ModulationSource.h + * + * Created: 22/11/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +namespace WECore { + + /** + * Provides a standard interface for modulation sources such as LFOs and envelope followers to + * follow. + * + * This interface only defines how to get output from the modulation source and reset it, and + * say nothing about setting its parameters, sample rate, etc as these will vary. + */ + template + class ModulationSource { + static_assert(std::is_floating_point::value, + "Must be provided with a floating point template type"); + + public: + ModulationSource() : _cachedOutput(0) {} + virtual ~ModulationSource() = default; + + /** + * Given the provided audio sample, calculates the next output value and advances the + * internal state (if applicable). + */ + inline SampleType getNextOutput(SampleType inSample); + + /** + * Returns the most recent output of getNextOutput without advancing the internal state. + */ + virtual SampleType getLastOutput() const { return _cachedOutput; } + + /** + * Resets the internal state of the modulation source. + */ + inline void reset(); + + protected: + SampleType _cachedOutput; + + /** + * Must be overriden by the inheriting class to provide the specific implementation of this + * modulation source. + * + * The implementation may or may not need to use the provided audio sample. + */ + virtual SampleType _getNextOutputImpl(SampleType inSample) = 0; + + /** + * Must be overriden by the inheriting class to reset the internat state as required. + */ + virtual void _resetImpl() = 0; + }; + + template + SampleType ModulationSource::getNextOutput(SampleType inSample) { + _cachedOutput = _getNextOutputImpl(inSample); + return _cachedOutput; + } + + template + void ModulationSource::reset() { + _resetImpl(); + _cachedOutput = 0; + } + + template + struct ModulationSourceWrapper { + std::shared_ptr> source; + double amount; + }; + + template + SampleType calcModValue(const std::vector>& sources) { + SampleType retVal {0}; + + for (const ModulationSourceWrapper& source : sources) { + retVal += source.source->getLastOutput() * source.amount; + } + + return retVal; + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/PerlinSource.hpp b/ports-juce7/syndicate/WECore/WEFilters/PerlinSource.hpp new file mode 100644 index 00000000..03e873a7 --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/PerlinSource.hpp @@ -0,0 +1,275 @@ +/* + * File: PerlinSource.hpp + * + * Created: 30/12/2024 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include +#include +#include +#include "ModulationSource.h" +#include "General/ParameterDefinition.h" + +namespace WECore::Perlin { + inline float fade(float t) { + return t * t * t * (t * (t * 6 - 15) + 10); + } + + inline float lerp(float a, float b, float t) { + return a + t * (b - a); + } + + inline float grad(int hash, float x) { + // Keep only the last 4 bits + const int h = hash & 15; + + // Randomly return a negtive if the top bit is 1 + if (h & 8) { + return -x; + } else { + return x; + } + } + + inline float perlin1D(float x, const std::vector& permutation) { + // Get the indexes that our x is between + const int x0 = static_cast(std::floor(x)) & 255; + const int x1 = (x0 + 1) & 255; + + // Look up the indexes + const int g0 = permutation[x0]; + const int g1 = permutation[x1]; + + // Get the distance of x from the lower index + const float xf = x - floor(x); + + // Get the gradients at our position x for each index + const float dot0 = grad(g0, xf); + const float dot1 = grad(g1, xf - 1.0f); + + // Interpolate between gradient contributions + const float u = fade(xf); + return lerp(dot0, dot1, u); + } + + namespace Parameters { + const ParameterDefinition::RangedParameter FREQ(0, 20, 1), + DEPTH(0, 1, 0.5); + + class OutputModeParameter : public ParameterDefinition::BaseParameter { + public: + OutputModeParameter() : ParameterDefinition::BaseParameter(UNIPOLAR, BIPOLAR, BIPOLAR) {} + + static constexpr int UNIPOLAR {1}, + BIPOLAR {2}; + }; + + const OutputModeParameter OUTPUTMODE; + } + + class PerlinSource : public ModulationSource { + public: + PerlinSource() : _outputMode(Parameters::OUTPUTMODE.defaultValue), + _rawFreq(Parameters::FREQ.defaultValue), + _modulatedFreqValue(0), + _rawDepth(Parameters::DEPTH.defaultValue), + _modulatedDepthValue(0), + _sampleRate(44100), + _wavePosition(0) { + // Initialize the permutation vector with the default permutation + _permutation = { + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, + 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, + 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, + 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, + 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, + 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, + 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, + 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, + 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, + 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, + 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, + 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, + 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, + 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, + 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 + }; + } + + virtual ~PerlinSource() override = default; + + void setFreq(double freq) { _rawFreq = Parameters::FREQ.BoundsCheck(freq); } + void setDepth(double depth) { _rawDepth = Parameters::DEPTH.BoundsCheck(depth); } + void setOutputMode(int val) { _outputMode = Parameters::OUTPUTMODE.BoundsCheck(val); } + void setSampleRate(double val) { _sampleRate = val; } + + double getFreq() const { return _rawFreq; } + double getModulatedFreqValue() const { return _modulatedFreqValue; } + double getDepth() const { return _rawDepth; } + double getModulatedDepthValue() const { return _modulatedDepthValue; } + int getOutputMode() { return _outputMode; } + + inline bool addFreqModulationSource(std::shared_ptr source); + inline bool removeFreqModulationSource(std::shared_ptr source); + inline bool setFreqModulationAmount(size_t index, double amount); + void setFreqModulationSources(std::vector> sources) { _freqModulationSources = sources; } + std::vector> getFreqModulationSources() const { return _freqModulationSources; } + + inline bool addDepthModulationSource(std::shared_ptr source); + inline bool removeDepthModulationSource(std::shared_ptr source); + inline bool setDepthModulationAmount(size_t index, double amount); + void setDepthModulationSources(std::vector> sources) { _depthModulationSources = sources; } + std::vector> getDepthModulationSources() const { return _depthModulationSources; } + + /** + * Returns a pointer to a deep copy of this instance, including all state. The caller is + * responsible for managing the lifetime of the returned object. + */ + PerlinSource* clone() const { + return new PerlinSource(*this); + } + + protected: + int _outputMode; + + double _rawFreq, + _modulatedFreqValue, + _rawDepth, + _modulatedDepthValue, + _sampleRate, + _wavePosition; + + std::vector> _freqModulationSources, + _depthModulationSources; + + std::vector _permutation; + + inline double _getNextOutputImpl(double inSample) override; + + void _resetImpl() override { _wavePosition = 0; } + + private: + // Used when cloning + PerlinSource(const PerlinSource& other) { + _outputMode = other._outputMode; + + _rawFreq = other._rawFreq; + _rawDepth = other._rawDepth; + _sampleRate = other._sampleRate; + _wavePosition = other._wavePosition; + + _freqModulationSources = other._freqModulationSources; + _depthModulationSources = other._depthModulationSources; + + _permutation = other._permutation; + } + }; + + bool PerlinSource::addFreqModulationSource(std::shared_ptr source) { + // Check if the source is already in the list + for (const ModulationSourceWrapper& existingSource : _freqModulationSources) { + if (existingSource.source == source) { + return false; + } + } + + _freqModulationSources.push_back({source, 0}); + return true; + } + + bool PerlinSource::removeFreqModulationSource(std::shared_ptr source) { + for (auto it = _freqModulationSources.begin(); it != _freqModulationSources.end(); it++) { + if ((*it).source == source) { + _freqModulationSources.erase(it); + return true; + } + } + + return false; + } + + bool PerlinSource::setFreqModulationAmount(size_t index, double amount) { + if (index >= _freqModulationSources.size()) { + return false; + } + + _freqModulationSources[index].amount = amount; + return true; + } + + bool PerlinSource::addDepthModulationSource(std::shared_ptr source) { + // Check if the source is already in the list + for (const ModulationSourceWrapper& existingSource : _depthModulationSources) { + if (existingSource.source == source) { + return false; + } + } + + _depthModulationSources.push_back({source, 0}); + return true; + } + + bool PerlinSource::removeDepthModulationSource(std::shared_ptr source) { + for (auto it = _depthModulationSources.begin(); it != _depthModulationSources.end(); it++) { + if ((*it).source == source) { + _depthModulationSources.erase(it); + return true; + } + } + + return false; + } + + bool PerlinSource::setDepthModulationAmount(size_t index, double amount) { + if (index >= _depthModulationSources.size()) { + return false; + } + + _depthModulationSources[index].amount = amount; + return true; + } + + double PerlinSource::_getNextOutputImpl(double /*inSample*/) { + // Get the mod amount to use, divide by 2 to reduce range to -0.5:0.5 + const double freqModValue {calcModValue(_freqModulationSources) / 2}; + const double depthModValue {calcModValue(_depthModulationSources) / 2}; + + // Calculate the frequency value after modulation + const double freq {Parameters::FREQ.BoundsCheck( + _rawFreq + (Parameters::FREQ.maxValue * freqModValue) + )}; + _modulatedFreqValue = freq; + + // Calculate the depth value after modulation + const double depth {Parameters::DEPTH.BoundsCheck( + _rawDepth + (Parameters::DEPTH.maxValue * depthModValue) + )}; + _modulatedDepthValue = depth; + + // Calculate the Perlin noise value + double noiseValue {perlin1D(_wavePosition, _permutation)}; + _wavePosition += (_modulatedFreqValue / 2) / _sampleRate; + + noiseValue = _outputMode == Parameters::OUTPUTMODE.BIPOLAR ? noiseValue : (noiseValue + 0.5); + + return noiseValue * _modulatedDepthValue; + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/SimpleCompressor.h b/ports-juce7/syndicate/WECore/WEFilters/SimpleCompressor.h new file mode 100644 index 00000000..dc6ea009 --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/SimpleCompressor.h @@ -0,0 +1,306 @@ +/* + * File: SimpleCompressor.h + * + * Created: 03/12/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/CoreMath.h" +#include "EffectsProcessor.h" +#include "SimpleCompressorParameters.h" + +namespace WECore::SimpleCompressor { + + enum class Direction { + UPWARD, + DOWNWARD + }; + + /** + * Simple implementation of a feedforward log domain compressor supporting both upward and + * downward compression types. + */ + template + class SimpleCompressor : public EffectsProcessor1in1out { + public: + SimpleCompressor(); + virtual ~SimpleCompressor() override = default; + + /** @name Setter Methods */ + /** @{ */ + + /** + * Sets the sample rate being used. + * + * @param val The sample rate + */ + inline void setSampleRate(double val); + + /** + * Sets whether the compressor operates in upward or downward mode. + * + * @param val The mode to be applied + */ + inline void setDirection(Direction val); + + /** + * Sets the attack time in milliseconds. + * + * @param val The attack to be applied. + * + * @see ATTACK for valid values + */ + inline void setAttack(double val); + + /** + * Sets the release time in milliseconds. + * + * @param val The release to be applied. + * + * @see RELEASE for valid values + */ + inline void setRelease(double val); + + /** + * Sets the threshold in dB. + * + * @param val The threshold to be applied. + * + * @see THRESHOLD for valid values + */ + void setThreshold(double val) { _threshold = Parameters::THRESHOLD.BoundsCheck(val); } + + /** + * Sets the compression ratio. + * + * @param val The ratio to be applied. + * + * @see RATIO for valid values + */ + void setRatio(double val) { _ratio = Parameters::RATIO.BoundsCheck(val); } + + /** + * Sets the knee width. + * + * @param val The knee width to be applied. + * + * @see KNEE_WIDTH for valid values + */ + void setKneeWidth(double val) { _kneeWidth = Parameters::KNEE_WIDTH.BoundsCheck(val); } + + /** @} */ + + /** @name Getter Methods */ + /** @{ */ + + /** + * @see setDirection + */ + Direction getDirection() const { return _direction; } + + /** + * @see setAttack + */ + double getAttack() const { return _attackMs; } + + /** + * @see setRelease + */ + double getRelease() const { return _releaseMs; } + + /** + * @see setThreshold + */ + double getThreshold() const { return _threshold; } + + /** + * @see setRatio + */ + double getRatio() const { return _ratio; } + + /** + * @see setKneeWidth + */ + double getKneeWidth() const { return _kneeWidth; } + + /** + * Returns the last gain value applied to an input signal. + */ + SampleType getGainApplied() const { return _gainApplied; } + + /** @} */ + + inline virtual void process1in1out(SampleType* inSamples, + size_t numSamples) override; + + inline virtual void reset() override; + + private: + Direction _direction; + + double _sampleRate; + + double _attackMs; + double _attackCoef; + double _releaseMs; + double _releaseCoef; + double _threshold; + double _ratio; + double _kneeWidth; + + SampleType _levelDetectorState; + SampleType _gainApplied; + + double _calcCoef(double timeMs) { return exp(log(0.01) / (timeMs * _sampleRate * 0.001)); } + + inline SampleType _computeLevel(SampleType inSample); + + inline SampleType _computeGain(SampleType inSample); + }; + + template + SimpleCompressor::SimpleCompressor() : _direction(Direction::DOWNWARD), + _attackMs(Parameters::ATTACK_MS.defaultValue), + _releaseMs(Parameters::RELEASE_MS.defaultValue), + _threshold(Parameters::THRESHOLD.defaultValue), + _ratio(Parameters::RATIO.defaultValue), + _kneeWidth(Parameters::KNEE_WIDTH.defaultValue), + _levelDetectorState(0), + _gainApplied(0) { + // Call this here rather than setting it in initialiser list so that the coefficients get + // setup + setSampleRate(44100); + } + + template + void SimpleCompressor::setSampleRate(double val) { + if (!WECore::CoreMath::compareFloatsEqual(_sampleRate, val)) { + _sampleRate = val; + + _attackCoef = _calcCoef(_attackMs); + _releaseCoef = _calcCoef(_releaseMs); + + reset(); + } + } + + template + void SimpleCompressor::setDirection(Direction val) { + if (_direction != val) { + _direction = val; + reset(); + } + } + + template + void SimpleCompressor::setAttack(double val) { + _attackMs = Parameters::ATTACK_MS.BoundsCheck(val); + _attackCoef = _calcCoef(_attackMs); + } + + template + void SimpleCompressor::setRelease(double val) { + _releaseMs = Parameters::RELEASE_MS.BoundsCheck(val); + _releaseCoef = _calcCoef(_releaseMs); + } + + template + void SimpleCompressor::process1in1out(SampleType* inSamples, + size_t numSamples) { + + for (size_t index {0}; index < numSamples; index++) { + + // Rectify the input and convert to dB + const SampleType absdB {static_cast( + CoreMath::linearTodB(std::abs(inSamples[index])) + )}; + + // Compute gain + const SampleType gainComp {_computeGain(absdB)}; + + // Take the difference and pass to the level detection + const SampleType level {_computeLevel(absdB - gainComp)}; + + // Convert to linear and apply the gain + _gainApplied = static_cast(CoreMath::dBToLinear(-level)); + + inSamples[index] = inSamples[index] * _gainApplied; + } + } + + template + void SimpleCompressor::reset() { + _levelDetectorState = 0; + _gainApplied = 0; + } + + template + SampleType SimpleCompressor::_computeLevel(SampleType inSample) { + + if (inSample > _levelDetectorState) { + _levelDetectorState = _attackCoef * _levelDetectorState + (1 - _attackCoef) * inSample; + } else { + _levelDetectorState = _releaseCoef * _levelDetectorState + (1 - _releaseCoef) * inSample; + } + + return _levelDetectorState; + } + + template + SampleType SimpleCompressor::_computeGain(SampleType inSample) { + + SampleType retVal {inSample}; + + if (_direction == Direction::DOWNWARD) { + + // Downward compression + if (inSample - _threshold < -_kneeWidth / 2) { + // Level is below threshold and knee - do nothing + + } else if (std::abs(inSample - _threshold) <= (_kneeWidth / 2)) { + // Level is within the range of the knee + retVal = inSample + (1 / _ratio - 1) + * std::pow(inSample - _threshold + (_kneeWidth / 2), 2); + + } else { + // Level is above threshold and knee + retVal = _threshold + (inSample - _threshold) / _ratio; + } + + } else { + + // Upward compression + if (inSample - _threshold < -_kneeWidth / 2) { + // Level is below theshold and knee + retVal = _threshold - (_threshold - inSample) / _ratio; + + } else if (std::abs(inSample - _threshold) <= (_kneeWidth / 2)) { + // Level is within the range of the knee + retVal = inSample + (1 - 1 / _ratio) \ + * std::pow(inSample - _threshold - _kneeWidth / 2, 2) / (2 * _kneeWidth); + + } else { + // Level is above threshold and knee - do nothing + } + + } + + return retVal; + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/SimpleCompressorParameters.h b/ports-juce7/syndicate/WECore/WEFilters/SimpleCompressorParameters.h new file mode 100644 index 00000000..c6226f0a --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/SimpleCompressorParameters.h @@ -0,0 +1,42 @@ +/* + * File: SimpleCompressorParameters.h + * + * Created: 07/05/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/ParameterDefinition.h" + +namespace WECore::SimpleCompressor::Parameters { + //@{ + /** + * A parameter which can take any float value between the ranges defined. + * The values passed on construction are in the following order: + * minimum value, + * maximum value, + * default value + */ + const ParameterDefinition::RangedParameter ATTACK_MS(0.1, 500, 10), + RELEASE_MS(1, 5000, 100), + THRESHOLD(-60, 0, 0), + RATIO(1, 30, 2), + KNEE_WIDTH(1, 10, 2); + + //@} +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/StereoWidthProcessor.h b/ports-juce7/syndicate/WECore/WEFilters/StereoWidthProcessor.h new file mode 100644 index 00000000..38477828 --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/StereoWidthProcessor.h @@ -0,0 +1,80 @@ +/* + * File: StereoWidthProcessor.h + * + * Created: 03/12/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include + +#include "EffectsProcessor.h" +#include "StereoWidthProcessorParameters.h" + +namespace WECore::StereoWidth { + + /** + * Processor which can apply stereo widening or narrowing. + */ + template + class StereoWidthProcessor : public EffectsProcessor2in2out { + public: + StereoWidthProcessor() : _width(Parameters::WIDTH.defaultValue) {} + virtual ~StereoWidthProcessor() override = default; + + /** + * Sets the stereo width. + * 0 = mono + * 1 = neutral, no processing + * 2 = highest width + * + * @param val The width to be applied. + * + * @see WIDTH for valid values + */ + void setWidth(double val) { _width = Parameters::WIDTH.BoundsCheck(val); } + + /** + * @see setWidth + */ + double getWidth() const { return _width; } + + + inline virtual void process2in2out(SampleType* inSamplesLeft, + SampleType* inSamplesRight, + size_t numSamples) override; + + private: + double _width; + }; + + template + void StereoWidthProcessor::process2in2out(SampleType* inSamplesLeft, + SampleType* inSamplesRight, + size_t numSamples) { + + for (size_t index {0}; index < numSamples; index++) { + + double mid {(inSamplesLeft[index] + inSamplesRight[index]) * 0.5}; + double side {(inSamplesRight[index] - inSamplesLeft[index]) * (_width / 2)}; + + inSamplesLeft[index] = mid - side; + inSamplesRight[index] = mid + side; + } + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/StereoWidthProcessorParameters.h b/ports-juce7/syndicate/WECore/WEFilters/StereoWidthProcessorParameters.h new file mode 100644 index 00000000..75e6161d --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/StereoWidthProcessorParameters.h @@ -0,0 +1,37 @@ +/* + * File: StereoWidthProcessorParameters.h + * + * Created: 03/12/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/ParameterDefinition.h" + +namespace WECore::StereoWidth::Parameters { + //@{ + /** + * A parameter which can take any float value between the ranges defined. + * The values passed on construction are in the following order: + * minimum value, + * maximum value, + * default value + */ + const ParameterDefinition::RangedParameter WIDTH(0, 2, 1); + //@} +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/TPTSVFilter.h b/ports-juce7/syndicate/WECore/WEFilters/TPTSVFilter.h new file mode 100644 index 00000000..6c2d8abc --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/TPTSVFilter.h @@ -0,0 +1,187 @@ +/* + * File: TPTSVFilter.h + * + * Created: 22/12/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/CoreMath.h" +#include "WEFilters/TPTSVFilterParameters.h" + +namespace WECore::TPTSVF { + /** + * A state variable filter from a topology-preserving transform. + * + * To use this class, simply call reset, and the process methods as necessary, using the provided + * getter and setter methods to manipulate parameters. + * + * Internally relies on the parameters provided in TPTSVFilterParameters.h + * + * Based on a talk given by Ivan Cohen: https://www.youtube.com/watch?v=esjHXGPyrhg + */ + template + class TPTSVFilter { + static_assert(std::is_floating_point::value, + "Must be provided with a floating point template type"); + + public: + TPTSVFilter() : _sampleRate(44100), + _cutoffHz(Parameters::CUTOFF.defaultValue), + _q(Parameters::Q.defaultValue), + _gain(Parameters::GAIN.defaultValue), + _s1(0), + _s2(0), + _g(0), + _h(0), + _mode(Parameters::FILTER_MODE.BYPASS) { + _calculateCoefficients(); + } + + virtual ~TPTSVFilter() = default; + + /** + * Applies the filtering to a buffer of samples. + * Expect seg faults or other memory issues if arguements passed are incorrect. + * + * @param[out] inSamples Pointer to the first sample of the left channel's buffer + * @param[in] numSamples Number of samples in the buffer + */ + void processBlock(T* inSamples, size_t numSamples); + + /** + * Resets filter coefficients. + * Call this whenever the audio stream is interrupted (ie. the playhead is moved) + */ + void reset() { + _s1 = 0; + _s2 = 0; + } + + /** @name Getter Methods */ + /** @{ */ + + int getMode() const { return _mode; } + double getCutoff() const { return _cutoffHz; } + double getQ() const { return _q; } + double getGain() const { return _gain; } + + /** @} */ + + /** @name Setter Methods */ + /** @{ */ + + void setMode(int val) { _mode = Parameters::FILTER_MODE.BoundsCheck(val); } + inline void setCutoff(double val); + inline void setQ(double val); + void setGain(double val) { _gain = Parameters::GAIN.BoundsCheck(val); } + inline void setSampleRate(double val); + + /** @} */ + + TPTSVFilter clone() const { + return TPTSVFilter(*this); + } + + private: + double _sampleRate, + _cutoffHz, + _q, + _gain; + + T _s1, + _s2, + _g, + _h; + + int _mode; + + void _calculateCoefficients(); + + TPTSVFilter(const TPTSVFilter& other) { + _sampleRate = other._sampleRate; + _cutoffHz = other._cutoffHz; + _q = other._q; + _gain = other._gain; + + _s1 = other._s1; + _s2 = other._s2; + _g = other._g; + _h = other._h; + + _mode = other._mode; + } + }; + + template + void TPTSVFilter::processBlock(T* inSamples, size_t numSamples) { + + if (_mode != Parameters::FILTER_MODE.BYPASS) { + + for (size_t idx {0}; idx < numSamples; idx++) { + const T sample {inSamples[idx]}; + + const T yH {static_cast(_h * (sample - (1.0f / _q + _g) * _s1 - _s2))}; + + const T yB {static_cast(_g * yH + _s1)}; + _s1 = _g * yH + yB; + + const T yL {static_cast(_g * yB + _s2)}; + _s2 = _g * yB + yL; + + switch (_mode) { + case Parameters::ModeParameter::PEAK: + inSamples[idx] = yB * static_cast(_gain); + break; + + case Parameters::ModeParameter::HIGHPASS: + inSamples[idx] = yH * static_cast(_gain); + break; + + default: + inSamples[idx] = yL * static_cast(_gain); + break; + } + } + } + } + + template + void TPTSVFilter::setCutoff(double val) { + _cutoffHz = Parameters::CUTOFF.BoundsCheck(val); + _calculateCoefficients(); + } + + template + void TPTSVFilter::setQ(double val) { + _q = Parameters::Q.BoundsCheck(val); + _calculateCoefficients(); + } + + template + void TPTSVFilter::setSampleRate(double val) { + _sampleRate = val; + _calculateCoefficients(); + } + + template + void TPTSVFilter::_calculateCoefficients() { + _g = static_cast(std::tan(CoreMath::DOUBLE_PI * _cutoffHz / _sampleRate)); + _h = static_cast(1.0 / (1 + _g / _q + _g * _g)); + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/TPTSVFilterParameters.h b/ports-juce7/syndicate/WECore/WEFilters/TPTSVFilterParameters.h new file mode 100644 index 00000000..a0a2704e --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/TPTSVFilterParameters.h @@ -0,0 +1,53 @@ +/* + * File: TPTSVFilterParameters.h + * + * Created: 25/12/2016 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#pragma once + +#include "General/ParameterDefinition.h" + +namespace WECore::TPTSVF::Parameters { + + class ModeParameter : public ParameterDefinition::BaseParameter { + public: + ModeParameter() : ParameterDefinition::BaseParameter::BaseParameter( + BYPASS, HIGHPASS, BYPASS) { } + + static constexpr int BYPASS = 1, + LOWPASS = 2, + PEAK = 3, + HIGHPASS = 4; + }; + const ModeParameter FILTER_MODE; + + //@{ + /** + * A parameter which can take any float value between the ranges defined. + * The values passed on construction are in the following order: + * minimum value, + * maximum value, + * default value + */ + const ParameterDefinition::RangedParameter CUTOFF(0, 20000, 20), + Q(0.1, 20, 0.5), + GAIN(0, 2, 1); + //@} + +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/Tests/AREnvelopeFollowerTests.cpp b/ports-juce7/syndicate/WECore/WEFilters/Tests/AREnvelopeFollowerTests.cpp new file mode 100644 index 00000000..fad1e221 --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/Tests/AREnvelopeFollowerTests.cpp @@ -0,0 +1,89 @@ +/* + * File: AREnvelopeFollowerTests.cpp + * + * Created: 03/06/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "WEFilters/AREnvelopeFollowerSquareLaw.h" + +SCENARIO("AREnvelopeFollowerSquareLaw: Parameters can be set and retrieved correctly") { + GIVEN("A new AREnvelopeFollower object") { + WECore::AREnv::AREnvelopeFollowerSquareLaw mEnv; + + WHEN("Nothing is changed") { + THEN("Parameters have their default values") { + CHECK(mEnv.getAttackTimeMs() == Approx(20.0)); + CHECK(mEnv.getReleaseTimeMs() == Approx(200.0)); + CHECK(mEnv.getFilterEnabled() == false); + CHECK(mEnv.getLowCutHz() == Approx(0.0)); + CHECK(mEnv.getHighCutHz() == Approx(20000.0)); + } + } + + WHEN("All parameters are changed to unique values") { + mEnv.setAttackTimeMs(21.0); + mEnv.setReleaseTimeMs(22.0); + mEnv.setFilterEnabled(true); + mEnv.setLowCutHz(23.0); + mEnv.setHighCutHz(24.0); + + THEN("They all get their correct unique values") { + CHECK(mEnv.getAttackTimeMs() == Approx(21.0)); + CHECK(mEnv.getReleaseTimeMs() == Approx(22.0)); + CHECK(mEnv.getFilterEnabled() == true); + CHECK(mEnv.getLowCutHz() == Approx(23.0)); + CHECK(mEnv.getHighCutHz() == Approx(24.0)); + } + } + } +} + +SCENARIO("AREnvelopeFollowerSquareLaw: Parameters enforce their bounds correctly") { + GIVEN("A new AREnvelopeFollower object") { + WECore::AREnv::AREnvelopeFollowerSquareLaw mEnv; + + WHEN("All parameter values are too low") { + mEnv.setAttackTimeMs(0); + mEnv.setReleaseTimeMs(0); + mEnv.setLowCutHz(-1); + mEnv.setHighCutHz(-1); + + THEN("Parameters enforce their lower bounds") { + CHECK(mEnv.getAttackTimeMs() == Approx(0.1)); + CHECK(mEnv.getReleaseTimeMs() == Approx(0.1)); + CHECK(mEnv.getLowCutHz() == Approx(0.0)); + CHECK(mEnv.getHighCutHz() == Approx(0.0)); + } + } + + WHEN("All parameter values are too high") { + mEnv.setAttackTimeMs(10001); + mEnv.setReleaseTimeMs(10001); + mEnv.setLowCutHz(20001); + mEnv.setHighCutHz(20001); + + THEN("Parameters enforce their upper bounds") { + CHECK(mEnv.getAttackTimeMs() == Approx(10000)); + CHECK(mEnv.getReleaseTimeMs() == Approx(10000)); + CHECK(mEnv.getLowCutHz() == Approx(20000.0)); + CHECK(mEnv.getHighCutHz() == Approx(20000.0)); + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/Tests/SimpleCompressorTests.cpp b/ports-juce7/syndicate/WECore/WEFilters/Tests/SimpleCompressorTests.cpp new file mode 100644 index 00000000..04e2ad90 --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/Tests/SimpleCompressorTests.cpp @@ -0,0 +1,122 @@ +/* + * File: TPTSVFilterTests.cpp + * + * Created: 04/12/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "WEFilters/SimpleCompressor.h" + +namespace Comp = WECore::SimpleCompressor; + +SCENARIO("SimpleCompressor: Parameters can be set and retrieved correctly") { + GIVEN("A new SimpleCompressor object") { + + Comp::SimpleCompressor compressor; + + WHEN("Nothing is changed") { + THEN("Parameters have their default values") { + CHECK(compressor.getDirection() == Comp::Direction::DOWNWARD); + CHECK(compressor.getAttack() == Approx(10.0)); + CHECK(compressor.getRelease() == Approx(100.0)); + CHECK(compressor.getThreshold() == Approx(0.0)); + CHECK(compressor.getRatio() == Approx(2.0)); + CHECK(compressor.getKneeWidth() == Approx(2.0)); + } + } + + WHEN("All parameters are changed to unique values") { + compressor.setDirection(Comp::Direction::UPWARD); + compressor.setAttack(0.11); + compressor.setRelease(1.1); + compressor.setThreshold(-10.0); + compressor.setRatio(1.2); + compressor.setKneeWidth(1.3); + + THEN("They all get their correct unique values") { + CHECK(compressor.getDirection() == Comp::Direction::UPWARD); + CHECK(compressor.getAttack() == Approx(0.11)); + CHECK(compressor.getRelease() == Approx(1.1)); + CHECK(compressor.getThreshold() == Approx(-10.0)); + CHECK(compressor.getRatio() == Approx(1.2)); + CHECK(compressor.getKneeWidth() == Approx(1.3)); + } + } + } +} + +SCENARIO("SimpleCompressor: Parameters enforce their bounds correctly") { + GIVEN("A new SimpleCompressor object") { + + Comp::SimpleCompressor compressor; + + WHEN("All parameter values are too low") { + compressor.setAttack(-10); + compressor.setRelease(-10); + compressor.setThreshold(-100); + compressor.setRatio(-10); + compressor.setKneeWidth(-10); + + THEN("Parameters enforce their lower bounds") { + CHECK(compressor.getAttack() == Approx(0.1)); + CHECK(compressor.getRelease() == Approx(1.0)); + CHECK(compressor.getThreshold() == Approx(-60.0)); + CHECK(compressor.getRatio() == Approx(1.0)); + CHECK(compressor.getKneeWidth() == Approx(1.0)); + } + } + + WHEN("All parameter values are too high") { + compressor.setAttack(1000); + compressor.setRelease(10000); + compressor.setThreshold(10); + compressor.setRatio(100); + compressor.setKneeWidth(100); + + THEN("Parameters enforce their upper bounds") { + CHECK(compressor.getAttack() == Approx(500)); + CHECK(compressor.getRelease() == Approx(5000)); + CHECK(compressor.getThreshold() == Approx(0)); + CHECK(compressor.getRatio() == Approx(30)); + CHECK(compressor.getKneeWidth() == Approx(10)); + } + } + } +} + +SCENARIO("SimpleCompressor: Silence in = silence out") { + GIVEN("A SimpleCompressor and a buffer of silent samples") { + + std::vector buffer(1024); + Comp::SimpleCompressor compressor; + + WHEN("The silence samples are processed") { + // fill the buffer + std::fill(buffer.begin(), buffer.end(), 0); + + // do processing + compressor.process1in1out(&buffer[0], buffer.size()); + + THEN("The output is silence") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(0.0)); + } + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/Tests/StereoWidthProcessorTests.cpp b/ports-juce7/syndicate/WECore/WEFilters/Tests/StereoWidthProcessorTests.cpp new file mode 100644 index 00000000..23025808 --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/Tests/StereoWidthProcessorTests.cpp @@ -0,0 +1,153 @@ +/* + * File: StereoWidthProcessorTests.cpp + * + * Created: 05/12/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "General/CoreMath.h" +#include "TestUtils.h" +#include "WEFilters/StereoWidthProcessor.h" + +SCENARIO("StereoWidthProcessor: Parameters can be set and retrieved correctly") { + GIVEN("A new StereoWidthProcessor object") { + + WECore::StereoWidth::StereoWidthProcessor processor; + + WHEN("Nothing is changed") { + THEN("Parameters have their default values") { + CHECK(processor.getWidth() == Approx(1.0)); + } + } + + WHEN("All parameters are changed to unique values") { + processor.setWidth(1.5); + + THEN("They all get their correct unique values") { + CHECK(processor.getWidth() == Approx(1.5)); + } + } + } +} + +SCENARIO("StereoWidthProcessor: Parameters enforce their bounds correctly") { + GIVEN("A new StereoWidthProcessor object") { + + WECore::StereoWidth::StereoWidthProcessor processor; + + WHEN("All parameter values are too low") { + processor.setWidth(-10); + + THEN("Parameters enforce their lower bounds") { + CHECK(processor.getWidth() == Approx(0.0)); + } + } + + WHEN("All parameter values are too high") { + processor.setWidth(10); + + + THEN("Parameters enforce their upper bounds") { + CHECK(processor.getWidth() == Approx(2.0)); + + } + } + } +} + +SCENARIO("StereoWidthProcessor: Silence in = silence out") { + GIVEN("A StereoWidthProcessor and a buffer of silent samples") { + + std::vector leftBuffer(1024); + std::vector rightBuffer(1024); + WECore::StereoWidth::StereoWidthProcessor processor; + + // Fill the buffer + std::fill(leftBuffer.begin(), leftBuffer.end(), 0); + std::fill(rightBuffer.begin(), rightBuffer.end(), 0); + + WHEN("The silence samples are processed") { + + // Do processing + processor.process2in2out(&leftBuffer[0], &rightBuffer[0], leftBuffer.size()); + + THEN("The output is silence") { + for (size_t index {0}; index < leftBuffer.size(); index++) { + CHECK(leftBuffer[index] == Approx(0.0)); + CHECK(rightBuffer[index] == Approx(0.0)); + } + } + } + } +} + +SCENARIO("StereoWidthProcessor: Neutral width position") { + GIVEN("A StereoWidthProcessor and a buffer of sine samples") { + + std::vector leftBuffer(1024); + std::vector rightBuffer(1024); + WECore::StereoWidth::StereoWidthProcessor processor; + + // Fill the buffers + WECore::TestUtils::generateSine(leftBuffer, 44100, 1000); + std::copy(leftBuffer.begin(), leftBuffer.end() , rightBuffer.begin()); + + // Set the expected output + std::vector expectedOutput(1024); + std::copy(leftBuffer.begin(), leftBuffer.end() , expectedOutput.begin()); + + WHEN("The samples are processed") { + // Do processing + processor.process2in2out(&leftBuffer[0], &rightBuffer[0], leftBuffer.size()); + + THEN("The output is silence") { + for (size_t index {0}; index < leftBuffer.size(); index++) { + CHECK(leftBuffer[index] == Approx(expectedOutput[index])); + CHECK(rightBuffer[index] == Approx(expectedOutput[index])); + } + } + } + } +} + +SCENARIO("StereoWidthProcessor: Stereo to full mono") { + GIVEN("A StereoWidthProcessor and a buffer of sine samples") { + + std::vector leftBuffer(1024); + std::vector rightBuffer(1024); + WECore::StereoWidth::StereoWidthProcessor processor; + + // Fill the buffers with different frequency sines + WECore::TestUtils::generateSine(leftBuffer, 44100, 1000); + WECore::TestUtils::generateSine(leftBuffer, 44100, 1500); + + WHEN("The width is set to mono and samples are processed") { + + processor.setWidth(0); + + // Do processing + processor.process2in2out(&leftBuffer[0], &rightBuffer[0], leftBuffer.size()); + + THEN("The output is the same for each channel") { + for (size_t index {0}; index < leftBuffer.size(); index++) { + CHECK(leftBuffer[index] == Approx(rightBuffer[index])); + } + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/Tests/TPTSVFilterTests.cpp b/ports-juce7/syndicate/WECore/WEFilters/Tests/TPTSVFilterTests.cpp new file mode 100644 index 00000000..1a828c5a --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/Tests/TPTSVFilterTests.cpp @@ -0,0 +1,155 @@ +/* + * File: TPTSVFilterTests.cpp + * + * Created: 09/05/2017 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include "catch.hpp" +#include "WEFilters/TPTSVFilter.h" + +SCENARIO("TPTSVFilter: Parameters can be set and retrieved correctly") { + GIVEN("A new TPTSVFilter object") { + WECore::TPTSVF::TPTSVFilter mFilter; + + WHEN("Nothing is changed") { + THEN("Parameters have their default values") { + CHECK(mFilter.getMode() == 1); + CHECK(mFilter.getCutoff() == Approx(20.0)); + CHECK(mFilter.getQ() == Approx(0.5)); + CHECK(mFilter.getGain() == Approx(1.0)); + } + } + + WHEN("All parameters are changed to unique values") { + mFilter.setMode(2); + mFilter.setCutoff(21); + mFilter.setQ(0.6); + mFilter.setGain(1.1); + + THEN("They all get their correct unique values") { + CHECK(mFilter.getMode() == 2); + CHECK(mFilter.getCutoff() == Approx(21.0)); + CHECK(mFilter.getQ() == Approx(0.6)); + CHECK(mFilter.getGain() == Approx(1.1)); + } + } + } +} + +SCENARIO("TPTSVFilter: Parameters enforce their bounds correctly") { + GIVEN("A new TPTSVFilter object") { + WECore::TPTSVF::TPTSVFilter mFilter; + + WHEN("All parameter values are too low") { + mFilter.setMode(-1); + mFilter.setCutoff(-1); + mFilter.setQ(0); + mFilter.setGain(-1); + + THEN("Parameters enforce their lower bounds") { + CHECK(mFilter.getMode() == 1); + CHECK(mFilter.getCutoff() == Approx(0.0)); + CHECK(mFilter.getQ() == Approx(0.1)); + CHECK(mFilter.getGain() == Approx(0.0)); + } + } + + WHEN("All parameter values are too high") { + mFilter.setMode(5); + mFilter.setCutoff(20001); + mFilter.setQ(21); + mFilter.setGain(3); + + THEN("Parameters enforce their upper bounds") { + CHECK(mFilter.getMode() == 4); + CHECK(mFilter.getCutoff() == Approx(20000.0)); + CHECK(mFilter.getQ() == Approx(20.0)); + CHECK(mFilter.getGain() == Approx(2.0)); + } + } + } +} + +SCENARIO("TPTSVFilter: Silence in = silence out") { + GIVEN("A TPTSVFilter and a buffer of silent samples") { + std::vector buffer(1024); + WECore::TPTSVF::TPTSVFilter mFilter; + + WHEN("The silence samples are processed") { + // fill the buffer + std::fill(buffer.begin(), buffer.end(), 0); + + // do processing + mFilter.processBlock(&buffer[0], buffer.size()); + + THEN("The output is silence") { + for (size_t iii {0}; iii < buffer.size(); iii++) { + CHECK(buffer[iii] == Approx(0.0)); + } + } + } + } +} + +SCENARIO("TPTSVFilter: Clone works correctly") { + GIVEN("A TPTSVFilter and a buffer of samples") { + auto generateBuffer = []() { + std::vector buffer(1024); + std::fill(buffer.begin(), buffer.end(), 0); + std::fill(buffer.begin() + 300, buffer.end(), 0.5); + std::fill(buffer.begin() + 600, buffer.end(), 0.7); + return buffer; + }; + + std::vector initialBuffer = generateBuffer(); + WECore::TPTSVF::TPTSVFilter filter; + + // Set some unique values so we can test for them later + filter.setMode(WECore::TPTSVF::Parameters::FILTER_MODE.HIGHPASS); + filter.setCutoff(1001); + filter.setQ(1.1); + filter.setGain(0.8); + filter.setSampleRate(48000); + + // Set up some internal state + filter.processBlock(&initialBuffer[0], initialBuffer.size()); + + WHEN("It is cloned") { + WECore::TPTSVF::TPTSVFilter clonedFilter = filter.clone(); + + THEN("The cloned filter is equal to the original") { + CHECK(clonedFilter.getMode() == filter.getMode()); + CHECK(clonedFilter.getCutoff() == Approx(filter.getCutoff())); + CHECK(clonedFilter.getQ() == Approx(filter.getQ())); + CHECK(clonedFilter.getGain() == Approx(filter.getGain())); + // CHECK(clonedFilter.getSampleRate() == Approx(filter.getSampleRate())); + + // Check internal state + std::vector buffer1 = generateBuffer(); + filter.processBlock(&buffer1[0], buffer1.size()); + + std::vector buffer2 = generateBuffer(); + clonedFilter.processBlock(&buffer2[0], buffer2.size()); + + for (size_t index {0}; index < buffer1.size(); index++) { + CHECK(buffer1[index] == Approx(buffer2[index])); + } + } + } + } +} diff --git a/ports-juce7/syndicate/WECore/WEFilters/Tests/TestUtils.h b/ports-juce7/syndicate/WECore/WEFilters/Tests/TestUtils.h new file mode 100644 index 00000000..8edc731c --- /dev/null +++ b/ports-juce7/syndicate/WECore/WEFilters/Tests/TestUtils.h @@ -0,0 +1,35 @@ +/* + * File: TestUtils.h + * + * Created: 05/12/2020 + * + * This file is part of WECore. + * + * WECore 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. + * + * WECore 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 WECore. If not, see . + */ + +#include + +#include "General/CoreMath.h" + +namespace WECore::TestUtils { + static void generateSine(std::vector& buffer, double sampleRate, double frequency) { + + const double samplesPerCycle {sampleRate / frequency}; + + std::generate(buffer.begin(), + buffer.end(), + [index = 0, samplesPerCycle]() mutable {return std::sin(CoreMath::LONG_TAU * (index++ / samplesPerCycle));} ); + } +} diff --git a/ports-juce7/syndicate/meson.build b/ports-juce7/syndicate/meson.build new file mode 100644 index 00000000..249c9ac5 --- /dev/null +++ b/ports-juce7/syndicate/meson.build @@ -0,0 +1,124 @@ +############################################################################### + +plugin_extra_build_flags = [ + '-Wno-deprecated-declarations', +] + +plugin_extra_include_dirs = include_directories([ + 'AllCommon', + 'PluginCommon', + 'PluginCommon/PluginHosting', + 'PluginCommon/PluginScanning', + 'PluginCommon/Processor', + 'PluginCommon/Processor/DataModel', + 'PluginCommon/Processor/Mutators', + 'PluginCommon/Processor/Processing', + 'Syndicate', + 'Syndicate/UI', + 'Syndicate/UI/HeaderComponents', + 'Syndicate/UI/HeaderComponents/Crossover', + 'Syndicate/UI/ModulationBar', + 'Syndicate/UI/PluginGraph', + 'WECore', +]) + +plugin_srcs = files([ + 'AllCommon/MainLogger.cpp', + 'PluginCommon/PluginHosting/GuestPluginWindow.cpp', + 'PluginCommon/PluginScanning/ConfigurePopover.cpp', + 'PluginCommon/PluginScanning/PluginScanClient.cpp', + 'PluginCommon/PluginScanning/PluginScanStatusBar.cpp', + 'PluginCommon/PluginScanning/PluginSelectorComponent.cpp', + 'PluginCommon/PluginScanning/PluginSelectorList.cpp', + 'PluginCommon/PluginScanning/PluginSelectorState.cpp', + 'PluginCommon/PluginScanning/PluginSelectorWindow.cpp', + 'PluginCommon/PluginScanning/ScanConfiguration.cpp', + 'PluginCommon/Processor/DataModel/CloneableDelayLine.cpp', + 'PluginCommon/Processor/DataModel/CloneableLRFilter.cpp', + 'PluginCommon/Processor/DataModel/FFTProvider.cpp', + 'PluginCommon/Processor/DataModel/LatencyListener.cpp', + 'PluginCommon/Processor/DataModel/PluginConfigurator.cpp', + 'PluginCommon/Processor/Mutators/ChainMutators.cpp', + 'PluginCommon/Processor/Mutators/CrossoverMutators.cpp', + 'PluginCommon/Processor/Mutators/ModulationMutators.cpp', + 'PluginCommon/Processor/Mutators/MutatorsInterface.cpp', + 'PluginCommon/Processor/Mutators/SplitterMutators.cpp', + 'PluginCommon/Processor/Mutators/XmlReader.cpp', + 'PluginCommon/Processor/Mutators/XmlWriter.cpp', + 'PluginCommon/Processor/Processing/ChainProcessors.cpp', + 'PluginCommon/Processor/Processing/ChainSlotProcessors.cpp', + 'PluginCommon/Processor/Processing/CrossoverProcessors.cpp', + 'PluginCommon/Processor/Processing/ModulationProcessors.cpp', + 'PluginCommon/Processor/Processing/ProcessingInterface.cpp', + 'PluginCommon/Processor/Processing/SplitterProcessors.cpp', + 'Syndicate/PluginProcessor.cpp', + 'Syndicate/UI/HeaderComponents/ChainButton.cpp', + 'Syndicate/UI/HeaderComponents/ChainButtonsComponent.cpp', + 'Syndicate/UI/HeaderComponents/Crossover/CrossoverImagerComponent.cpp', + 'Syndicate/UI/HeaderComponents/Crossover/CrossoverMouseListener.cpp', + 'Syndicate/UI/HeaderComponents/Crossover/CrossoverParameterComponent.cpp', + 'Syndicate/UI/HeaderComponents/Crossover/CrossoverWrapperComponent.cpp', + 'Syndicate/UI/HeaderComponents/LeftrightSplitterSubComponent.cpp', + 'Syndicate/UI/HeaderComponents/MidsideSplitterSubComponent.cpp', + 'Syndicate/UI/HeaderComponents/MultibandSplitterSubComponent.cpp', + 'Syndicate/UI/HeaderComponents/ParallelSplitterSubComponent.cpp', + 'Syndicate/UI/HeaderComponents/SeriesSplitterSubComponent.cpp', + 'Syndicate/UI/HeaderComponents/SplitterHeaderComponent.cpp', + 'Syndicate/UI/ImportExportComponent.cpp', + 'Syndicate/UI/MacroComponent.cpp', + 'Syndicate/UI/MacrosComponent.cpp', + 'Syndicate/UI/MetadataEditComponent.cpp', + 'Syndicate/UI/ModulationBar/ModulatableParameter.cpp', + 'Syndicate/UI/ModulationBar/ModulationBar.cpp', + 'Syndicate/UI/ModulationBar/ModulationBarEnvelope.cpp', + 'Syndicate/UI/ModulationBar/ModulationBarLfo.cpp', + 'Syndicate/UI/ModulationBar/ModulationBarRandom.cpp', + 'Syndicate/UI/ModulationBar/ModulationButton.cpp', + 'Syndicate/UI/ModulationTargetSlider.cpp', + 'Syndicate/UI/ModulationTargetSourceSlider.cpp', + 'Syndicate/UI/OutputComponent.cpp', + 'Syndicate/UI/PluginEditor.cpp', + 'Syndicate/UI/PluginGraph/BaseSlotComponent.cpp', + 'Syndicate/UI/PluginGraph/ChainViewComponent.cpp', + 'Syndicate/UI/PluginGraph/EmptyPluginSlotComponent.cpp', + 'Syndicate/UI/PluginGraph/GainStageSlotComponent.cpp', + 'Syndicate/UI/PluginGraph/GraphViewComponent.cpp', + 'Syndicate/UI/PluginGraph/PluginModulationInterface.cpp', + 'Syndicate/UI/PluginGraph/PluginModulationTarget.cpp', + 'Syndicate/UI/PluginGraph/PluginParameterSelectorComponent.cpp', + 'Syndicate/UI/PluginGraph/PluginParameterSelectorList.cpp', + 'Syndicate/UI/PluginGraph/PluginParameterSelectorState.cpp', + 'Syndicate/UI/PluginGraph/PluginParameterSelectorWindow.cpp', + 'Syndicate/UI/PluginGraph/PluginSelectionInterface.cpp', + 'Syndicate/UI/PluginGraph/PluginSlotComponent.cpp', + 'Syndicate/UI/PluginGraph/PluginSlotModulationTray.cpp', + 'Syndicate/UI/SplitterButtonsComponent.cpp', + 'Syndicate/UI/UIUtils.cpp', + 'Syndicate/UI/UndoRedoComponent.cpp', +]) + +plugin_name = 'Syndicate' + +scanner = executable('PluginScanServer', + sources: files([ + 'AllCommon/MainLogger.cpp', + 'PluginScanServer/Main.cpp', + ]), + include_directories: [ + include_directories([ + '../../libs/juce7', + '../../libs/juce7/source', + '../../libs/juce7/source/modules', + '../../libs/juce-plugin', + 'AllCommon', + 'PluginScanServer', + ]), + ], + c_args: build_flags, + cpp_args: build_flags_cpp + ['-std=gnu++17'], + link_args: link_flags, + link_with: [ lib_juce7 ], + install: false, +) + +############################################################################### diff --git a/scripts/install-syndicate-scanner.sh b/scripts/install-syndicate-scanner.sh new file mode 100644 index 00000000..89c1c151 --- /dev/null +++ b/scripts/install-syndicate-scanner.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -e + +SCANNER="${MESON_BUILD_ROOT}/ports-juce7/syndicate/PluginScanServer" + +if [ "$(uname -s)" = "Darwin" ]; then + VST2_PATH=Contents/Resources +else + VST2_PATH=Resources +fi + +mkdir -p ${MESON_INSTALL_DESTDIR_PREFIX}/lib/lv2/Syndicate.lv2 +mkdir -p ${MESON_INSTALL_DESTDIR_PREFIX}/lib/vst/Syndicate.vst/${VST2_PATH} +mkdir -p ${MESON_INSTALL_DESTDIR_PREFIX}/lib/vst3/Syndicate.vst3/Contents/Resources + +cp -v "${SCANNER}" ${MESON_INSTALL_DESTDIR_PREFIX}/lib/lv2/Syndicate.lv2/ +cp -v "${SCANNER}" ${MESON_INSTALL_DESTDIR_PREFIX}/lib/vst/Syndicate.vst/${VST2_PATH}/ +cp -v "${SCANNER}" ${MESON_INSTALL_DESTDIR_PREFIX}/lib/vst3/Syndicate.vst3/Contents/Resources/ + +# move VST2 into subdir +if [ "$(uname -s)" != "Darwin" ]; then + mv ${MESON_INSTALL_DESTDIR_PREFIX}/lib/vst/Syndicate.so ${MESON_INSTALL_DESTDIR_PREFIX}/lib/vst/Syndicate.vst/ +fi