gearmulator

Emulation of classic VA synths of the late 90s/2000s that are based on Motorola 56300 family DSPs
Log | Files | Refs | Submodules | README | LICENSE

commit 7bf0494c14a6a68eeee0d63015b592c3e38ed2d2
parent cd18bb1ea8a9027b37b300b689f48f361662ac56
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Sat, 10 Dec 2022 03:41:55 +0100

Osirus 1.2.22

Diffstat:
MCMakeLists.txt | 3++-
Mdoc/changelog.txt | 23+++++++++++++++++++++++
Msource/jucePlugin/CMakeLists.txt | 4++++
Msource/jucePlugin/PluginEditor.cpp | 253++++++++++++-------------------------------------------------------------------
Msource/jucePlugin/PluginEditor.h | 40++++++++--------------------------------
Asource/jucePlugin/PluginEditorState.cpp | 232+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePlugin/PluginEditorState.h | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePlugin/PluginProcessor.cpp | 22+++++++++++++++++++++-
Msource/jucePlugin/PluginProcessor.h | 10+++++++++-
Msource/jucePlugin/VirusController.cpp | 14+++++++-------
Msource/jucePlugin/VirusParameterBinding.cpp | 198++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msource/jucePlugin/VirusParameterBinding.h | 51++++++++++++++++++++++++++++++++++++---------------
Msource/jucePlugin/parameterDescriptions_C.json | 10++++++++++
Msource/jucePlugin/skins/Hoverland/VirusC_Hoverland.json | 30++++++++++++++++++++++--------
Asource/jucePlugin/ui3/ControllerLinks.cpp | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePlugin/ui3/ControllerLinks.h | 31+++++++++++++++++++++++++++++++
Msource/jucePlugin/ui3/Tabs.h | 2--
Msource/jucePlugin/ui3/VirusEditor.cpp | 64+++++++++++++++++++++++++++++++++++++++++-----------------------
Msource/jucePlugin/ui3/VirusEditor.h | 16+++++++++++-----
Msource/jucePluginLib/CMakeLists.txt | 1+
Msource/jucePluginLib/controller.cpp | 2+-
Msource/jucePluginLib/controller.h | 2++
Msource/jucePluginLib/parameter.cpp | 62+++++++++++++++++++++++++++++++++++++-------------------------
Msource/jucePluginLib/parameter.h | 27++++++++++++++++++++-------
Msource/jucePluginLib/parameterdescriptions.cpp | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msource/jucePluginLib/parameterdescriptions.h | 5+++++
Asource/jucePluginLib/parameterlink.cpp | 0
Asource/jucePluginLib/parameterlink.h | 34++++++++++++++++++++++++++++++++++
Msource/juceUiLib/CMakeLists.txt | 1+
Asource/juceUiLib/controllerlink.cpp | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/juceUiLib/controllerlink.h | 44++++++++++++++++++++++++++++++++++++++++++++
Msource/juceUiLib/editor.cpp | 6++++++
Msource/juceUiLib/editor.h | 1+
Msource/juceUiLib/uiObject.cpp | 45+++++++++++++++++++++++++++++++++++++++++++++
Msource/juceUiLib/uiObject.h | 4++++
Msource/juceUiLib/uiObjectStyle.cpp | 22+++++++++++++---------
Msource/juceUiLib/uiObjectStyle.h | 2++
Msource/virusLib/hdi08TxParser.cpp | 2+-
38 files changed, 1135 insertions(+), 422 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -5,7 +5,7 @@ set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "OS X Architectures") set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9" CACHE STRING "Minimum OS X deployment version") -project(gearmulator VERSION 1.2.20) +project(gearmulator VERSION 1.2.22) include(base.cmake) @@ -39,6 +39,7 @@ endif() # ----------------- Console Programs add_subdirectory(source/virusTestConsole) +add_subdirectory(source/logicAnalysisParser) add_subdirectory(source/virusIntegrationTest) # ----------------- CPack diff --git a/doc/changelog.txt b/doc/changelog.txt @@ -1,5 +1,28 @@ Release Notes +1.2.21 (2022.12.06): + +Osirus: +- [Imp] ROM file will now also be found if put next to the binary in the component folder (VST3 and AU builds) +- [Imp] Prev/Next buttons now select preset browser patches instead of ROM patches if a preset browser patch was selected last +- [Imp] More save options have been added, either save one Single, or the current arrangement (if in Multi mode) or a whole bank +- [Imp] All save options now support saving as either .syx or as .mid + +- [Fix] Plugin may not emit any sound after the plugin state was restored (if part of a previously saved project) +- [Fix] Some UI elements didn't update their state correctly +- [Fix] UI may send incorrect control changes back to DSP after changing patches +- [Fix] Loading multiple Singles via bank load (file with more than one Single) didn't load all Singles but only the last one +- [Fix] GUI related crash after switching skins +- [Fix] Latency setting was not applied before editor opened + +Test Console: +- Fix demo song couldn't be loaded from ROM + +DSP: +- Performance improvements (5-10%) +- Stability improvemens +- Large speedup of code (re)generation. Results in faster startup time, reduces hiccups in Multi mode and when changing patches + 1.2.20 (2022.07.29): - [Imp] Performance has improved by 10% - 20% depending on the use case diff --git a/source/jucePlugin/CMakeLists.txt b/source/jucePlugin/CMakeLists.txt @@ -17,6 +17,8 @@ set(SOURCES ParameterNames.h PluginEditor.cpp PluginEditor.h + PluginEditorState.cpp + PluginEditorState.h PluginProcessor.cpp PluginProcessor.h VirusController.cpp @@ -27,6 +29,8 @@ set(SOURCES ) set(SOURCES_UI3 + ui3/ControllerLinks.cpp + ui3/ControllerLinks.h ui3/FxPage.cpp ui3/FxPage.h ui3/MidiPorts.cpp diff --git a/source/jucePlugin/PluginEditor.cpp b/source/jucePlugin/PluginEditor.cpp @@ -1,249 +1,69 @@ -#include "PluginProcessor.h" #include "PluginEditor.h" -#include "VirusController.h" - -#include "ui3/VirusEditor.h" +#include "PluginEditorState.h" +#include "PluginProcessor.h" -#include "../synthLib/os.h" +#include "VirusController.h" //============================================================================== -AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor(AudioPluginAudioProcessor &p) : - AudioProcessorEditor(&p), processorRef(p), m_parameterBinding(p) +AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor(AudioPluginAudioProcessor &p, PluginEditorState& s) : + AudioProcessorEditor(&p), processorRef(p), m_state(s) { - m_includedSkins.push_back({"Hoverland", "VirusC_Hoverland.json", ""}); - m_includedSkins.push_back({"Trancy", "VirusC_Trancy.json", ""}); - m_includedSkins.push_back({"Galaxpel", "VirusC_Galaxpel.json", ""}); - addMouseListener(this, true); - const auto config = processorRef.getController().getConfig(); - const auto scale = config->getIntValue("scale", 100); - - Skin skin = readSkinFromConfig(); - - if(skin.jsonFilename.empty()) - { - skin = m_includedSkins[0]; - } - - loadSkin(skin); - setGuiScale(scale); - - const auto latencyBlocks = config->getIntValue("latencyBlocks", static_cast<int>(processorRef.getPlugin().getLatencyBlocks())); - setLatencyBlocks(latencyBlocks); -} - -void AudioPluginAudioProcessorEditor::loadSkin(const Skin& _skin) -{ - if(m_currentSkin == _skin) - return; - - m_currentSkin = _skin; - writeSkinToConfig(_skin); - - if (m_virusEditor) - { - m_parameterBinding.clearBindings(); - - if(getIndexOfChildComponent(m_virusEditor.get()) > -1) - removeChildComponent(m_virusEditor.get()); - m_virusEditor.reset(); - } - - m_rootScale = 1.0f; - - try + m_state.evSkinLoaded = [&](juce::Component* _component) { - auto* editor = new genericVirusUI::VirusEditor(m_parameterBinding, processorRef, _skin.jsonFilename, _skin.folder, [this] { openMenu(); }); - m_virusEditor.reset(editor); - setSize(m_virusEditor->getWidth(), m_virusEditor->getHeight()); - m_rootScale = editor->getScale(); + setUiRoot(_component); + }; - m_virusEditor->setTopLeftPosition(0, 0); - addAndMakeVisible(m_virusEditor.get()); - } - catch(const std::runtime_error& _err) + m_state.evSetGuiScale = [&](const int _scale) { - LOG("ERROR: Failed to create editor: " << _err.what()); + if(getNumChildComponents() > 0) + setGuiScale(getChildComponent(0), _scale); + }; - juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Skin load failed", _err.what(), "OK"); - m_virusEditor.reset(); + m_state.enableBindings(); - loadSkin(m_includedSkins[0]); - } + setUiRoot(m_state.getUiRoot()); } -void AudioPluginAudioProcessorEditor::setGuiScale(int percent) -{ - setScaleFactor(static_cast<float>(percent)/100.0f * m_rootScale); - auto* config = processorRef.getController().getConfig(); - config->setValue("scale", percent); - config->saveIfNeeded(); -} - -void AudioPluginAudioProcessorEditor::openMenu() +AudioPluginAudioProcessorEditor::~AudioPluginAudioProcessorEditor() { - const auto config = processorRef.getController().getConfig(); - const auto scale = config->getIntValue("scale", 100); - - juce::PopupMenu menu; - - juce::PopupMenu skinMenu; - - auto addSkinEntry = [this, &skinMenu](const Skin& _skin) - { - skinMenu.addItem(_skin.displayName, true, _skin == m_currentSkin,[this, _skin] {loadSkin(_skin);}); - }; + m_state.evSetGuiScale = [&](int){}; + m_state.evSkinLoaded = [&](juce::Component*){}; - for (const auto & skin : m_includedSkins) - addSkinEntry(skin); - - bool haveSkinsOnDisk = false; - - // find more skins on disk - const auto modulePath = synthLib::getModulePath(); - - std::vector<std::string> entries; - synthLib::getDirectoryEntries(entries, modulePath + "skins"); - - for (const auto& entry : entries) - { - std::vector<std::string> files; - synthLib::getDirectoryEntries(files, entry); + m_state.disableBindings(); - for (const auto& file : files) - { - if(synthLib::hasExtension(file, ".json")) - { - if(!haveSkinsOnDisk) - { - haveSkinsOnDisk = true; - skinMenu.addSeparator(); - } - - const auto relativePath = entry.substr(modulePath.size()); - auto jsonName = file; - const auto pathEndPos = jsonName.find_last_of("/\\"); - if(pathEndPos != std::string::npos) - jsonName = file.substr(pathEndPos+1); - const Skin skin{jsonName + " (" + relativePath + ")", jsonName, relativePath}; - addSkinEntry(skin); - } - } - } - - if(m_virusEditor && m_currentSkin.folder.empty()) - { - auto* editor = dynamic_cast<genericVirusUI::VirusEditor*>(m_virusEditor.get()); - if(editor) - { - skinMenu.addSeparator(); - skinMenu.addItem("Export current skin to 'skins' folder on disk", true, false, [this]{exportCurrentSkin();}); - } - } - - juce::PopupMenu scaleMenu; - scaleMenu.addItem("50%", true, scale == 50, [this] { setGuiScale(50); }); - scaleMenu.addItem("75%", true, scale == 75, [this] { setGuiScale(75); }); - scaleMenu.addItem("100%", true, scale == 100, [this] { setGuiScale(100); }); - scaleMenu.addItem("125%", true, scale == 125, [this] { setGuiScale(125); }); - scaleMenu.addItem("150%", true, scale == 150, [this] { setGuiScale(150); }); - scaleMenu.addItem("200%", true, scale == 200, [this] { setGuiScale(200); }); - scaleMenu.addItem("300%", true, scale == 300, [this] { setGuiScale(300); }); - - const auto latency = processorRef.getPlugin().getLatencyBlocks(); - juce::PopupMenu latencyMenu; - latencyMenu.addItem("0 (DAW will report proper CPU usage)", true, latency == 0, [this] { setLatencyBlocks(0); }); - latencyMenu.addItem("1 (default)", true, latency == 1, [this] { setLatencyBlocks(1); }); - latencyMenu.addItem("2", true, latency == 2, [this] { setLatencyBlocks(2); }); - latencyMenu.addItem("4", true, latency == 4, [this] { setLatencyBlocks(4); }); - latencyMenu.addItem("8", true, latency == 8, [this] { setLatencyBlocks(8); }); - - menu.addSubMenu("GUI Skin", skinMenu); - menu.addSubMenu("GUI Scale", scaleMenu); - menu.addSubMenu("Latency (blocks)", latencyMenu); - - menu.showMenuAsync(juce::PopupMenu::Options()); + setUiRoot(nullptr); } -void AudioPluginAudioProcessorEditor::exportCurrentSkin() const +void AudioPluginAudioProcessorEditor::setGuiScale(juce::Component* _comp, int percent) { - if(!m_virusEditor) - return; - - auto* editor = dynamic_cast<genericVirusUI::VirusEditor*>(m_virusEditor.get()); - - if(!editor) + if(!_comp) return; - const auto res = editor->exportToFolder(synthLib::getModulePath() + "skins/"); + const auto s = static_cast<float>(percent)/100.0f * m_state.getRootScale(); + _comp->setTransform(juce::AffineTransform::scale(s,s)); - if(!res.empty()) - { - juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Export failed", "Failed to export skin:\n\n" + res, "OK", m_virusEditor.get()); - } - else - { - juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::InfoIcon, "Export finished", "Skin successfully exported"); - } -} + setSize(static_cast<int>(m_state.getWidth() * s), static_cast<int>(m_state.getHeight() * s)); -AudioPluginAudioProcessorEditor::Skin AudioPluginAudioProcessorEditor::readSkinFromConfig() const -{ - const auto* config = processorRef.getController().getConfig(); - - Skin skin; - skin.displayName = config->getValue("skinDisplayName", "").toStdString(); - skin.jsonFilename = config->getValue("skinFile", "").toStdString(); - skin.folder = config->getValue("skinFolder", "").toStdString(); - return skin; -} - -void AudioPluginAudioProcessorEditor::writeSkinToConfig(const Skin& _skin) const -{ auto* config = processorRef.getController().getConfig(); - - config->setValue("skinDisplayName", _skin.displayName.c_str()); - config->setValue("skinFile", _skin.jsonFilename.c_str()); - config->setValue("skinFolder", _skin.folder.c_str()); -} - -void AudioPluginAudioProcessorEditor::setLatencyBlocks(uint32_t _blocks) const -{ - auto& p = processorRef.getPlugin(); - - if(p.setLatencyBlocks(_blocks)) - { - processorRef.updateLatencySamples(); - - auto* config = processorRef.getController().getConfig(); - config->setValue("latencyBlocks", static_cast<int>(_blocks)); - config->saveIfNeeded(); - } + config->setValue("scale", percent); + config->saveIfNeeded(); } -AudioPluginAudioProcessorEditor::~AudioPluginAudioProcessorEditor() +void AudioPluginAudioProcessorEditor::setUiRoot(juce::Component* _component) { - m_virusEditor.reset(); -} + removeAllChildren(); -//============================================================================== -void AudioPluginAudioProcessorEditor::paint (juce::Graphics& g) -{ -} + if(!_component) + return; -void AudioPluginAudioProcessorEditor::timerCallback() -{ - // ugly (polling!) way for refreshing presets names as this is temporary ui -} + const auto& config = processorRef.getController().getConfig(); + const auto scale = config->getIntValue("scale", 100); -void AudioPluginAudioProcessorEditor::resized() -{ - // This is generally where you'll want to lay out the positions of any - // subcomponents in your editor.. - auto area = getLocalBounds(); - area.removeFromTop(35); + setGuiScale(_component, scale); + addAndMakeVisible(_component); } void AudioPluginAudioProcessorEditor::mouseDown(const juce::MouseEvent& event) @@ -258,5 +78,8 @@ void AudioPluginAudioProcessorEditor::mouseDown(const juce::MouseEvent& event) if(event.eventComponent && event.eventComponent->findParentComponentOfClass<juce::FileBrowserComponent>()) return; - openMenu(); + if(dynamic_cast<juce::TextEditor*>(event.eventComponent)) + return; + + m_state.openMenu(); } diff --git a/source/jucePlugin/PluginEditor.h b/source/jucePlugin/PluginEditor.h @@ -2,52 +2,28 @@ #include "VirusParameterBinding.h" -#include "PluginProcessor.h" +class PluginEditorState; //============================================================================== -class AudioPluginAudioProcessorEditor : public juce::AudioProcessorEditor, private juce::Timer +class AudioPluginAudioProcessorEditor : public juce::AudioProcessorEditor { public: - explicit AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor&); + explicit AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor&, PluginEditorState&); ~AudioPluginAudioProcessorEditor() override; - void paint (juce::Graphics&) override; - void resized() override; void mouseDown(const juce::MouseEvent& event) override; + void paint(juce::Graphics& g) override {} + private: - struct Skin - { - std::string displayName; - std::string jsonFilename; - std::string folder; - - bool operator == (const Skin& _other) const - { - return displayName == _other.displayName && jsonFilename == _other.jsonFilename && folder == _other.folder; - } - }; - - void timerCallback() override; - void loadSkin(const Skin& _skin); - void setGuiScale(int percent); - void openMenu(); - void exportCurrentSkin() const; - Skin readSkinFromConfig() const; - void writeSkinToConfig(const Skin& _skin) const; - void setLatencyBlocks(uint32_t _blocks) const; + void setGuiScale(juce::Component* _component, int percent); + void setUiRoot(juce::Component* _component); // This reference is provided as a quick way for your editor to // access the processor object that created it. AudioPluginAudioProcessor& processorRef; - VirusParameterBinding m_parameterBinding; - - std::unique_ptr<juce::Component> m_virusEditor; - Skin m_currentSkin; - float m_rootScale = 1.0f; - - std::vector<Skin> m_includedSkins; + PluginEditorState& m_state; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessorEditor) }; diff --git a/source/jucePlugin/PluginEditorState.cpp b/source/jucePlugin/PluginEditorState.cpp @@ -0,0 +1,232 @@ +#include "PluginEditorState.h" + +#include "PluginProcessor.h" + +#include "ui3/VirusEditor.h" + +#include "../synthLib/os.h" + +const std::vector<PluginEditorState::Skin> m_includedSkins = +{ + {"Hoverland", "VirusC_Hoverland.json", ""}, + {"Trancy", "VirusC_Trancy.json", ""}, + {"Galaxpel", "VirusC_Galaxpel.json", ""} +}; + +PluginEditorState::PluginEditorState(AudioPluginAudioProcessor& _processor) : m_processor(_processor), m_parameterBinding(_processor.getController()) +{ + Skin skin = readSkinFromConfig(); + + if(skin.jsonFilename.empty()) + { + skin = m_includedSkins[0]; + } + + loadSkin(skin); +} + +int PluginEditorState::getWidth() const +{ + return m_virusEditor ? m_virusEditor->getWidth() : 0; +} + +int PluginEditorState::getHeight() const +{ + return m_virusEditor ? m_virusEditor->getHeight() : 0; +} + +const std::vector<PluginEditorState::Skin>& PluginEditorState::getIncludedSkins() +{ + return m_includedSkins; +} + +juce::Component* PluginEditorState::getUiRoot() const +{ + return m_virusEditor.get(); +} + +void PluginEditorState::disableBindings() +{ + m_parameterBinding.disableBindings(); +} + +void PluginEditorState::enableBindings() +{ + m_parameterBinding.enableBindings(); +} + +void PluginEditorState::loadSkin(const Skin& _skin) +{ + if(m_currentSkin == _skin) + return; + + m_currentSkin = _skin; + writeSkinToConfig(_skin); + + if (m_virusEditor) + { + m_parameterBinding.clearBindings(); + + auto* parent = m_virusEditor->getParentComponent(); + + if(parent && parent->getIndexOfChildComponent(m_virusEditor.get()) > -1) + parent->removeChildComponent(m_virusEditor.get()); + m_virusEditor.reset(); + } + + m_rootScale = 1.0f; + + try + { + auto* editor = new genericVirusUI::VirusEditor(m_parameterBinding, m_processor, _skin.jsonFilename, _skin.folder, [this] { openMenu(); }); + m_virusEditor.reset(editor); + m_rootScale = editor->getScale(); + + m_virusEditor->setTopLeftPosition(0, 0); + + if(evSkinLoaded) + evSkinLoaded(m_virusEditor.get()); + } + catch(const std::runtime_error& _err) + { + LOG("ERROR: Failed to create editor: " << _err.what()); + + juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Skin load failed", _err.what(), "OK"); + m_virusEditor.reset(); + + loadSkin(m_includedSkins[0]); + } +} + +void PluginEditorState::setGuiScale(int _scale) const +{ + if(evSetGuiScale) + evSetGuiScale(_scale); +} + +void PluginEditorState::openMenu() +{ + const auto config = m_processor.getController().getConfig(); + const auto scale = config->getIntValue("scale", 100); + + juce::PopupMenu menu; + + juce::PopupMenu skinMenu; + + auto addSkinEntry = [this, &skinMenu](const PluginEditorState::Skin& _skin) + { + skinMenu.addItem(_skin.displayName, true, _skin == getCurrentSkin(),[this, _skin] {loadSkin(_skin);}); + }; + + for (const auto & skin : PluginEditorState::getIncludedSkins()) + addSkinEntry(skin); + + bool haveSkinsOnDisk = false; + + // find more skins on disk + const auto modulePath = synthLib::getModulePath(); + + std::vector<std::string> entries; + synthLib::getDirectoryEntries(entries, modulePath + "skins"); + + for (const auto& entry : entries) + { + std::vector<std::string> files; + synthLib::getDirectoryEntries(files, entry); + + for (const auto& file : files) + { + if(synthLib::hasExtension(file, ".json")) + { + if(!haveSkinsOnDisk) + { + haveSkinsOnDisk = true; + skinMenu.addSeparator(); + } + + const auto relativePath = entry.substr(modulePath.size()); + auto jsonName = file; + const auto pathEndPos = jsonName.find_last_of("/\\"); + if(pathEndPos != std::string::npos) + jsonName = file.substr(pathEndPos+1); + const Skin skin{jsonName + " (" + relativePath + ")", jsonName, relativePath}; + addSkinEntry(skin); + } + } + } + + if(m_virusEditor && m_currentSkin.folder.empty()) + { + auto* editor = dynamic_cast<genericVirusUI::VirusEditor*>(m_virusEditor.get()); + if(editor) + { + skinMenu.addSeparator(); + skinMenu.addItem("Export current skin to 'skins' folder on disk", true, false, [this]{exportCurrentSkin();}); + } + } + + juce::PopupMenu scaleMenu; + scaleMenu.addItem("50%", true, scale == 50, [this] { setGuiScale(50); }); + scaleMenu.addItem("75%", true, scale == 75, [this] { setGuiScale(75); }); + scaleMenu.addItem("100%", true, scale == 100, [this] { setGuiScale(100); }); + scaleMenu.addItem("125%", true, scale == 125, [this] { setGuiScale(125); }); + scaleMenu.addItem("150%", true, scale == 150, [this] { setGuiScale(150); }); + scaleMenu.addItem("200%", true, scale == 200, [this] { setGuiScale(200); }); + scaleMenu.addItem("300%", true, scale == 300, [this] { setGuiScale(300); }); + + const auto latency = m_processor.getPlugin().getLatencyBlocks(); + juce::PopupMenu latencyMenu; + latencyMenu.addItem("0 (DAW will report proper CPU usage)", true, latency == 0, [this] { m_processor.setLatencyBlocks(0); }); + latencyMenu.addItem("1 (default)", true, latency == 1, [this] { m_processor.setLatencyBlocks(1); }); + latencyMenu.addItem("2", true, latency == 2, [this] { m_processor.setLatencyBlocks(2); }); + latencyMenu.addItem("4", true, latency == 4, [this] { m_processor.setLatencyBlocks(4); }); + latencyMenu.addItem("8", true, latency == 8, [this] { m_processor.setLatencyBlocks(8); }); + + menu.addSubMenu("GUI Skin", skinMenu); + menu.addSubMenu("GUI Scale", scaleMenu); + menu.addSubMenu("Latency (blocks)", latencyMenu); + + menu.showMenuAsync(juce::PopupMenu::Options()); +} + +void PluginEditorState::exportCurrentSkin() const +{ + if(!m_virusEditor) + return; + + auto* editor = dynamic_cast<genericVirusUI::VirusEditor*>(m_virusEditor.get()); + + if(!editor) + return; + + const auto res = editor->exportToFolder(synthLib::getModulePath() + "skins/"); + + if(!res.empty()) + { + juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Export failed", "Failed to export skin:\n\n" + res, "OK", m_virusEditor.get()); + } + else + { + juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::InfoIcon, "Export finished", "Skin successfully exported"); + } +} + +PluginEditorState::Skin PluginEditorState::readSkinFromConfig() const +{ + const auto* config = m_processor.getController().getConfig(); + + Skin skin; + skin.displayName = config->getValue("skinDisplayName", "").toStdString(); + skin.jsonFilename = config->getValue("skinFile", "").toStdString(); + skin.folder = config->getValue("skinFolder", "").toStdString(); + return skin; +} + +void PluginEditorState::writeSkinToConfig(const Skin& _skin) const +{ + auto* config = m_processor.getController().getConfig(); + + config->setValue("skinDisplayName", _skin.displayName.c_str()); + config->setValue("skinFile", _skin.jsonFilename.c_str()); + config->setValue("skinFolder", _skin.folder.c_str()); +} diff --git a/source/jucePlugin/PluginEditorState.h b/source/jucePlugin/PluginEditorState.h @@ -0,0 +1,59 @@ +#pragma once + +#include <string> + +#include "VirusParameterBinding.h" + +class AudioPluginAudioProcessor; + +class PluginEditorState +{ +public: + struct Skin + { + std::string displayName; + std::string jsonFilename; + std::string folder; + + bool operator == (const Skin& _other) const + { + return displayName == _other.displayName && jsonFilename == _other.jsonFilename && folder == _other.folder; + } + }; + + explicit PluginEditorState(AudioPluginAudioProcessor& _processor); + + void exportCurrentSkin() const; + Skin readSkinFromConfig() const; + void writeSkinToConfig(const Skin& _skin) const; + + float getRootScale() const { return m_rootScale; } + + int getWidth() const; + int getHeight() const; + + const Skin& getCurrentSkin() { return m_currentSkin; } + static const std::vector<Skin>& getIncludedSkins(); + + void openMenu(); + + std::function<void(int)> evSetGuiScale; + std::function<void(juce::Component*)> evSkinLoaded; + + juce::Component* getUiRoot() const; + + void disableBindings(); + void enableBindings(); + +private: + void loadSkin(const Skin& _skin); + void setGuiScale(int _scale) const; + + AudioPluginAudioProcessor& m_processor; + + VirusParameterBinding m_parameterBinding; + + std::unique_ptr<juce::Component> m_virusEditor; + Skin m_currentSkin; + float m_rootScale = 1.0f; +}; diff --git a/source/jucePlugin/PluginProcessor.cpp b/source/jucePlugin/PluginProcessor.cpp @@ -1,5 +1,6 @@ #include "PluginProcessor.h" #include "PluginEditor.h" +#include "PluginEditorState.h" #include "ParameterNames.h" #include <juce_audio_processors/juce_audio_processors.h> @@ -24,6 +25,9 @@ AudioPluginAudioProcessor::AudioPluginAudioProcessor() : { getController(); // init controller m_clockTempoParam = getController().getParameterIndexByName(Virus::g_paramClockTempo); + + const auto latencyBlocks = getController().getConfig()->getIntValue("latencyBlocks", static_cast<int>(getPlugin().getLatencyBlocks())); + setLatencyBlocks(latencyBlocks); } AudioPluginAudioProcessor::~AudioPluginAudioProcessor() = default; @@ -266,7 +270,9 @@ bool AudioPluginAudioProcessor::hasEditor() const juce::AudioProcessorEditor* AudioPluginAudioProcessor::createEditor() { - return new AudioPluginAudioProcessorEditor (*this); + if(!m_editorState) + m_editorState.reset(new PluginEditorState(*this)); + return new AudioPluginAudioProcessorEditor (*this, *m_editorState); } //============================================================================== @@ -413,6 +419,20 @@ void AudioPluginAudioProcessor::updateLatencySamples() setLatencySamples(m_plugin.getLatencyInputToOutput()); } +void AudioPluginAudioProcessor::setLatencyBlocks(uint32_t _blocks) +{ + auto& p = getPlugin(); + + if(p.setLatencyBlocks(_blocks)) + { + updateLatencySamples(); + + auto* config = getController().getConfig(); + config->setValue("latencyBlocks", static_cast<int>(_blocks)); + config->saveIfNeeded(); + } +} + Virus::Controller &AudioPluginAudioProcessor::getController() { if (m_controller == nullptr) diff --git a/source/jucePlugin/PluginProcessor.h b/source/jucePlugin/PluginProcessor.h @@ -2,10 +2,14 @@ #include <juce_audio_processors/juce_audio_processors.h> #include <juce_audio_devices/juce_audio_devices.h> + #include "../synthLib/plugin.h" #include "../virusLib/device.h" + #include "VirusController.h" +class PluginEditorState; + //============================================================================== class AudioPluginAudioProcessor : public juce::AudioProcessor, juce::MidiInputCallback { @@ -60,7 +64,8 @@ public: juce::MidiInput* getMidiInput() const; void handleIncomingMidiMessage(juce::MidiInput *source, const juce::MidiMessage &message) override; - std::string getRomName() { + std::string getRomName() const + { return juce::File(juce::String(m_romName)).getFileNameWithoutExtension().toStdString(); } virusLib::ROMFile::Model getModel() const @@ -73,6 +78,8 @@ public: } void updateLatencySamples(); + void setLatencyBlocks(uint32_t _blocks); + // _____________ // private: @@ -90,4 +97,5 @@ private: synthLib::Plugin m_plugin; std::vector<synthLib::SMidiEvent> m_midiOut; uint32_t m_clockTempoParam = 0xffffffff; + std::unique_ptr<PluginEditorState> m_editorState; }; diff --git a/source/jucePlugin/VirusController.cpp b/source/jucePlugin/VirusController.cpp @@ -148,10 +148,10 @@ namespace Virus } } for (const auto& param : globalParams) - param->setValueFromSynth(value, true); + param->setValueFromSynth(value, true, pluginLib::Parameter::ChangedBy::ControlChange); } for (const auto& param : partParams) - param->setValueFromSynth(value, true); + param->setValueFromSynth(value, true, pluginLib::Parameter::ChangedBy::ControlChange); // TODO: /** If a @@ -389,10 +389,10 @@ namespace Virus for(auto it = _parameterValues.begin(); it != _parameterValues.end(); ++it) { auto* p = getParameter(it->first.second, ch); - p->setValueFromSynth(it->second, true); + p->setValueFromSynth(it->second, true, pluginLib::Parameter::ChangedBy::PresetChange); - for (const auto& linkedParam : p->getLinkedParameters()) - linkedParam->setValueFromSynth(it->second, true); + for (const auto& derivedParam : p->getDerivedParameters()) + derivedParam->setValueFromSynth(it->second, true, pluginLib::Parameter::ChangedBy::PresetChange); } if(m_currentPresetSource[ch] != PresetSource::Browser) @@ -465,7 +465,7 @@ namespace Virus if(desc.page != virusLib::PAGE_C) continue; - param->setValueFromSynth(value); + param->setValueFromSynth(value, true, pluginLib::Parameter::ChangedBy::PresetChange); } } } @@ -487,7 +487,7 @@ namespace Virus DBG(juce::String::formatted("Set part: %d bank: %s param: %d value: %d", part, page == 0 ? "A" : "B", m.b, m.c)); const auto& params = findSynthParam(part, page, m.b); for (const auto & p : params) - p->setValueFromSynth(m.c, true); + p->setValueFromSynth(m.c, true, pluginLib::Parameter::ChangedBy::ControlChange); } void Controller::printMessage(const SysEx &msg) diff --git a/source/jucePlugin/VirusParameterBinding.cpp b/source/jucePlugin/VirusParameterBinding.cpp @@ -9,73 +9,13 @@ VirusParameterBinding::~VirusParameterBinding() clearBindings(); } -void VirusParameterBinding::clearBindings() -{ - for (const auto b : m_bindings) - { - auto* slider = dynamic_cast<juce::Slider*>(b.component); - - if(slider != nullptr) - removeMouseListener(*slider); - - if(b.onChangeListenerId) - b.parameter->removeListener(b.onChangeListenerId); - } - - m_bindings.clear(); -} - -void VirusParameterBinding::setPart(uint8_t _part) -{ - m_processor.getController().setCurrentPart(_part); - - std::vector<BoundParameter> bindings; - bindings.swap(m_bindings); - - for(size_t i=0; i<bindings.size(); ++i) - { - auto& b = bindings[i]; - - if(b.part != CurrentPart) - { - m_bindings.push_back(b); - continue; - } - - const auto& desc = b.parameter->getDescription(); - const bool isNonPartExclusive = desc.isNonPartSensitive(); - - if(isNonPartExclusive) - continue; - - auto* slider = dynamic_cast<juce::Slider*>(b.component); - if(slider) - { - bind(*slider, b.type); - continue; - } - auto* button = dynamic_cast<juce::DrawableButton*>(b.component); - if(button) - { - bind(*button, b.type); - continue; - } - auto* comboBox = dynamic_cast<juce::ComboBox*>(b.component); - if(comboBox) - { - bind(*comboBox, b.type); - continue; - } - assert(false && "unknown component type"); - } -} void VirusParameterBinding::bind(juce::Slider &_slider, uint32_t _param) { bind(_slider, _param, CurrentPart); } void VirusParameterBinding::bind(juce::Slider &_slider, uint32_t _param, const uint8_t _part) { - const auto v = m_processor.getController().getParameter(_param, _part == CurrentPart ? m_processor.getController().getCurrentPart() : _part); + const auto v = m_controller.getParameter(_param, _part == CurrentPart ? m_controller.getCurrentPart() : _part); if (!v) { @@ -99,7 +39,7 @@ void VirusParameterBinding::bind(juce::Slider &_slider, uint32_t _param, const u _slider.getProperties().set("bipolar", true); } const BoundParameter p{v, &_slider, _param, _part}; - m_bindings.emplace_back(p); + addBinding(p); } void VirusParameterBinding::bind(juce::ComboBox& _combo, uint32_t _param) @@ -109,7 +49,7 @@ void VirusParameterBinding::bind(juce::ComboBox& _combo, uint32_t _param) void VirusParameterBinding::bind(juce::ComboBox& _combo, const uint32_t _param, uint8_t _part) { - const auto v = m_processor.getController().getParameter(_param, _part == CurrentPart ? m_processor.getController().getCurrentPart() : _part); + const auto v = m_controller.getParameter(_param, _part == CurrentPart ? m_controller.getCurrentPart() : _part); if (!v) { assert(false && "Failed to find parameter"); @@ -137,15 +77,26 @@ void VirusParameterBinding::bind(juce::ComboBox& _combo, const uint32_t _param, } //_combo.addItemList(v->getAllValueStrings(), 1); _combo.setSelectedId(static_cast<int>(v->getValueObject().getValueSource().getValue()) + 1, juce::dontSendNotification); + _combo.onChange = [this, &_combo, v]() { + const auto id = _combo.getSelectedId(); + + if(id == 0) + return; + + const int current = v->getValueObject().getValueSource().getValue(); + + if((id - 1) == current) + return; + if(v->getDescription().isPublic) { v->beginChangeGesture(); - v->setValueNotifyingHost(v->convertTo0to1(static_cast<float>(_combo.getSelectedId() - 1))); + v->setValueNotifyingHost(v->convertTo0to1(static_cast<float>(id - 1))); v->endChangeGesture(); } - v->getValueObject().getValueSource().setValue((int)_combo.getSelectedId() - 1); + v->getValueObject().setValue(id - 1); }; const auto listenerId = m_nextListenerId++; @@ -157,12 +108,12 @@ void VirusParameterBinding::bind(juce::ComboBox& _combo, const uint32_t _param, })); const BoundParameter p{v, &_combo, _param, _part, listenerId}; - m_bindings.emplace_back(p); + addBinding(p); } void VirusParameterBinding::bind(juce::Button &_btn, const uint32_t _param) { - const auto v = m_processor.getController().getParameter(_param, m_processor.getController().getCurrentPart()); + const auto v = m_controller.getParameter(_param, m_controller.getCurrentPart()); if (!v) { assert(false && "Failed to find parameter"); @@ -170,7 +121,15 @@ void VirusParameterBinding::bind(juce::Button &_btn, const uint32_t _param) } _btn.getToggleStateValue().referTo(v->getValueObject()); const BoundParameter p{v, &_btn, _param, CurrentPart}; - m_bindings.emplace_back(p); + addBinding(p); +} + +juce::Component* VirusParameterBinding::getBoundComponent(const pluginLib::Parameter* _parameter) +{ + const auto it = m_boundParameters.find(_parameter); + if(it == m_boundParameters.end()) + return nullptr; + return it->second; } void VirusParameterBinding::removeMouseListener(juce::Slider& _slider) @@ -184,3 +143,106 @@ void VirusParameterBinding::removeMouseListener(juce::Slider& _slider) m_sliderMouseListeners.erase(it); } } + +void VirusParameterBinding::bind(const std::vector<BoundParameter>& _bindings, const bool _currentPartOnly) +{ + for (const auto& b : _bindings) + { + if(_currentPartOnly && b.part != CurrentPart) + { + addBinding(b); + continue; + } + + const auto& desc = b.parameter->getDescription(); + const bool isNonPartExclusive = desc.isNonPartSensitive(); + + if(isNonPartExclusive) + continue; + + auto* slider = dynamic_cast<juce::Slider*>(b.component); + if(slider) + { + bind(*slider, b.type); + continue; + } + auto* button = dynamic_cast<juce::DrawableButton*>(b.component); + if(button) + { + bind(*button, b.type); + continue; + } + auto* comboBox = dynamic_cast<juce::ComboBox*>(b.component); + if(comboBox) + { + bind(*comboBox, b.type); + continue; + } + assert(false && "unknown component type"); + } +} + +void VirusParameterBinding::addBinding(const BoundParameter& _boundParameter) +{ + m_bindings.emplace_back(_boundParameter); + + m_boundComponents.erase(_boundParameter.component); + m_boundParameters.erase(_boundParameter.parameter); + + m_boundParameters.insert(std::make_pair(_boundParameter.parameter, _boundParameter.component)); + m_boundComponents.insert(std::make_pair(_boundParameter.component, _boundParameter.parameter)); +} + +void VirusParameterBinding::disableBinding(const BoundParameter& _b) +{ + m_boundParameters.erase(_b.parameter); + m_boundComponents.erase(_b.component); + + auto* slider = dynamic_cast<juce::Slider*>(_b.component); + + if(slider != nullptr) + removeMouseListener(*slider); + + auto* combo = dynamic_cast<juce::ComboBox*>(_b.component); + if(combo != nullptr) + combo->onChange = nullptr; + + if(_b.onChangeListenerId) + _b.parameter->removeListener(_b.onChangeListenerId); +} + +void VirusParameterBinding::clearBindings() +{ + for (const auto& b : m_bindings) + disableBinding(b); + + m_bindings.clear(); + m_boundParameters.clear(); + m_boundComponents.clear(); +} + +void VirusParameterBinding::setPart(uint8_t _part) +{ + const std::vector<BoundParameter> bindings = m_bindings; + + clearBindings(); + + m_controller.setCurrentPart(_part); + + bind(bindings, true); +} + +void VirusParameterBinding::disableBindings() +{ + m_disabledBindings.clear(); + std::swap(m_bindings, m_disabledBindings); + + for (const auto& b : m_disabledBindings) + disableBinding(b); +} + +void VirusParameterBinding::enableBindings() +{ + bind(m_disabledBindings, false); + m_disabledBindings.clear(); +} diff --git a/source/jucePlugin/VirusParameterBinding.h b/source/jucePlugin/VirusParameterBinding.h @@ -2,7 +2,13 @@ #include "../jucePluginLib/parameter.h" -namespace juce { +namespace Virus +{ + class Controller; +} + +namespace juce +{ class Value; } @@ -23,36 +29,51 @@ public: class VirusParameterBinding final : juce::MouseListener { public: - VirusParameterBinding(AudioPluginAudioProcessor &_processor) : m_processor(_processor) + static constexpr uint8_t CurrentPart = 0xff; + + struct BoundParameter { + pluginLib::Parameter* parameter = nullptr; + juce::Component* component = nullptr; + uint32_t type = 0xffffffff; + uint8_t part = CurrentPart; + uint32_t onChangeListenerId = 0; + }; + VirusParameterBinding(Virus::Controller& _controller) : m_controller(_controller) + { } ~VirusParameterBinding() override; - void clearBindings(); - void setPart(uint8_t _part); + void bind(juce::Slider& _control, uint32_t _param); void bind(juce::Slider& _control, uint32_t _param, uint8_t _part); void bind(juce::ComboBox &_control, uint32_t _param); void bind(juce::ComboBox &_control, uint32_t _param, uint8_t _part); void bind(juce::Button &_control, uint32_t _param); + void clearBindings(); + void setPart(uint8_t _part); + + void disableBindings(); + void enableBindings(); + + const auto& getBindings() const { return m_bindings; } + juce::Component* getBoundComponent(const pluginLib::Parameter* _parameter); + private: void removeMouseListener(juce::Slider& _slider); - AudioPluginAudioProcessor& m_processor; + void addBinding(const BoundParameter& _boundParameter); + void disableBinding(const BoundParameter& _b); - static constexpr uint8_t CurrentPart = 0xff; + Virus::Controller& m_controller; - struct BoundParameter - { - pluginLib::Parameter* parameter = nullptr; - juce::Component* component = nullptr; - uint32_t type = 0xffffffff; - uint8_t part = CurrentPart; - uint32_t onChangeListenerId = 0; - }; + void bind(const std::vector<BoundParameter>& _bindings, bool _currentPartOnly); std::vector<BoundParameter> m_bindings; + std::vector<BoundParameter> m_disabledBindings; + std::map<const pluginLib::Parameter*, juce::Component*> m_boundParameters; + std::map<const juce::Component*, pluginLib::Parameter*> m_boundComponents; std::map<juce::Slider*, MouseListener*> m_sliderMouseListeners; - uint32_t m_nextListenerId = 1; + uint32_t m_nextListenerId = 100000; }; diff --git a/source/jucePlugin/parameterDescriptions_C.json b/source/jucePlugin/parameterDescriptions_C.json @@ -375,6 +375,16 @@ {"page":114, "class":"Global", "index":126, "name":"LCD Contrast", "min":0, "max":127, "isPublic":false, "isDiscrete":false, "isBool":false}, {"page":114, "class":"Global", "index":127, "name":"Master Volume", "min":0, "max":127, "isPublic":false, "isDiscrete":false, "isBool":false} ], + "parameterlinks": + [ + { + "source": "Cutoff", + "dest": "Cutoff2", + "conditionParameter": "Filter2 Cutoff Link", + "conditionValues": [1], + "link": "relative" + } + ], "valuelists": { "unsignedZero": diff --git a/source/jucePlugin/skins/Hoverland/VirusC_Hoverland.json b/source/jucePlugin/skins/Hoverland/VirusC_Hoverland.json @@ -2483,7 +2483,7 @@ } }, { - "name" : "EnvVel", + "name" : "EnvVel1", "parameterAttachment" : { "parameter" : "Flt1 EnvAmt Velocity" }, @@ -2621,7 +2621,7 @@ } }, { - "name" : "EnvVel", + "name" : "EnvVel2", "parameterAttachment" : { "parameter" : "Flt2 EnvAmt Velocity" }, @@ -2773,7 +2773,13 @@ "texture" : "link_horizon_48x72_x2", "tileSizeX" : "24", "tileSizeY" : "72" - } + }, + "controllerlinks": [ + { "source": "VelFlt1Freq", "dest": "VelFlt2Freq", "condition":"LinkEnv"}, + { "source": "VelFlt2Freq", "dest": "VelFlt1Freq", "condition":"LinkEnv"}, + { "source": "EnvVel1", "dest": "EnvVel2", "condition":"LinkEnv"}, + { "source": "EnvVel2", "dest": "EnvVel1", "condition":"LinkEnv"} + ] }, { "name" : "Attack", @@ -3230,7 +3236,11 @@ "texture" : "link_vert_72x48_x2", "tileSizeX" : "72", "tileSizeY" : "24" - } + }, + "controllerlinks": [ + { "source": "LfoOsc1Pitch", "dest": "LfoOsc2Pitch", "condition":"Lfo1Link"}, + { "source": "LfoOsc2Pitch", "dest": "LfoOsc1Pitch", "condition":"Lfo1Link"} + ] }, { "name" : "LfoShape", @@ -3373,7 +3383,7 @@ } }, { - "name" : "LfoOsc1Pitch", + "name" : "Cutoff1Lfo2Amount", "parameterAttachment" : { "parameter" : "Cutoff1 Lfo2 Amount" }, @@ -3390,7 +3400,7 @@ } }, { - "name" : "LfoOsc2Pitch", + "name" : "Cutoff2Lfo2Amount", "parameterAttachment" : { "parameter" : "Cutoff2 Lfo2 Amount" }, @@ -3491,7 +3501,7 @@ } }, { - "name" : "Lfo1Link", + "name" : "Lfo2Link", "button" : { "isToggle" : "1", "normalImage" : "0", @@ -3505,7 +3515,11 @@ "texture" : "link_vert_72x48_x2", "tileSizeX" : "72", "tileSizeY" : "24" - } + }, + "controllerlinks": [ + { "source": "Cutoff1Lfo2Amount", "dest": "Cutoff2Lfo2Amount", "condition":"Lfo2Link"}, + { "source": "Cutoff2Lfo2Amount", "dest": "Cutoff1Lfo2Amount", "condition":"Lfo2Link"} + ] }, { "name" : "LfoShape", diff --git a/source/jucePlugin/ui3/ControllerLinks.cpp b/source/jucePlugin/ui3/ControllerLinks.cpp @@ -0,0 +1,62 @@ +#include "ControllerLinks.h" + +#include "VirusEditor.h" + +namespace genericVirusUI +{ + // The only purpose of this class is to provide backwards-compatibility with old skins that do not have their links expressed in the json description file + ControllerLinks::ControllerLinks(const VirusEditor& _editor) + { + std::vector<juce::Slider*> lfoSliderL, lfoSliderR, envVel; + std::vector<juce::Button*> lfoToggles; + + _editor.findComponents(lfoSliderL, "LfoOsc1Pitch"); + _editor.findComponents(lfoSliderR, "LfoOsc2Pitch"); + + _editor.findComponents(lfoToggles, "Lfo1Link"); + if(lfoToggles.empty()) + _editor.findComponents(lfoToggles, "LfoOscPitchLink"); + + _editor.findComponents(envVel, "EnvVel"); + if(envVel.empty()) + _editor.findComponents(envVel, "EnvVelo"); + + auto* linkEnv = _editor.findComponentT<juce::Button>("LinkEnv", false); + + auto* flt1Vel = _editor.findComponentT<juce::Slider>("VelFlt1Freq", false); + auto* flt2Vel = _editor.findComponentT<juce::Slider>("VelFlt2Freq", false); + + if(lfoSliderL.size() == 2 && lfoSliderR.size() == 2 && lfoToggles.size() == 2) + { + for(size_t i=0; i<2; ++i) + createLink(lfoSliderL[i], lfoSliderR[i], lfoToggles[i]); + } + + if(linkEnv) + { + if(envVel.size() == 2) + createLink(envVel[0], envVel[1], linkEnv); + + if(flt1Vel && flt2Vel) + createLink(flt1Vel, flt2Vel, linkEnv); + } + + auto* vocModQ = _editor.findComponentT<juce::Slider>("VocModQ", false); + auto* vocCarrSpread = _editor.findComponentT<juce::Slider>("VocCarrSpread", false); + auto* vocLink = _editor.findComponentT<juce::Button>("VocoderLink", false); + + if(vocModQ && vocCarrSpread && vocLink) + createLink(vocModQ, vocCarrSpread, vocLink); + } + + void ControllerLinks::createLink(juce::Slider* _a, juce::Slider* _b, juce::Button* _cond) + { + auto* link = new genericUI::ControllerLink(); + link->create(_a, _b, _cond); + m_links.emplace_back(link); + + link = new genericUI::ControllerLink(); + link->create(_b, _a, _cond); + m_links.emplace_back(link); + } +} diff --git a/source/jucePlugin/ui3/ControllerLinks.h b/source/jucePlugin/ui3/ControllerLinks.h @@ -0,0 +1,31 @@ +#pragma once + +#include <memory> +#include <vector> + +namespace juce +{ + class Button; + class Slider; +} + +namespace genericUI +{ + class ControllerLink; +} + +namespace genericVirusUI +{ + class VirusEditor; + + class ControllerLinks + { + public: + ControllerLinks(const VirusEditor& _editor); + + private: + void createLink(juce::Slider* _a, juce::Slider* _b, juce::Button* _cond); + + std::vector<std::unique_ptr<genericUI::ControllerLink>> m_links; + }; +} diff --git a/source/jucePlugin/ui3/Tabs.h b/source/jucePlugin/ui3/Tabs.h @@ -1,7 +1,5 @@ #pragma once -#include <vector> - #include "../../juceUiLib/tabgroup.h" namespace juce diff --git a/source/jucePlugin/ui3/VirusEditor.cpp b/source/jucePlugin/ui3/VirusEditor.cpp @@ -28,6 +28,10 @@ namespace genericVirusUI if(getTabGroupCount() == 0) m_tabs.reset(new Tabs(*this)); + // be backwards compatible with old skins + if(getControllerLinkCountRecursive() == 0) + m_controllerLinks.reset(new ControllerLinks(*this)); + m_midiPorts.reset(new MidiPorts(*this)); // be backwards compatible with old skins @@ -119,10 +123,30 @@ namespace genericVirusUI updatePresetName(); updatePlayModeButtons(); updateControlLabel(nullptr); + + for (auto& params : getController().getExposedParameters()) + { + for (const auto& param : params.second) + { + m_boundParameters.push_back(param); + + param->onValueChanged.emplace_back(1, [this, param]() + { + if(param->getChangeOrigin() == pluginLib::Parameter::ChangedBy::PresetChange) + return; + auto* comp = m_parameterBinding.getBoundComponent(param); + if(comp) + updateControlLabel(comp); + }); + } + } } VirusEditor::~VirusEditor() { + for (auto* p : m_boundParameters) + p->removeListener(1); + m_parameterBinding.clearBindings(); getController().onProgramChange = nullptr; @@ -257,33 +281,21 @@ namespace genericVirusUI updatePresetName(); } - void VirusEditor::mouseDrag(const juce::MouseEvent & event) - { - updateControlLabel(event.eventComponent); - } - void VirusEditor::mouseEnter(const juce::MouseEvent& event) { - if (event.mouseWasDraggedSinceMouseDown()) - return; - updateControlLabel(event.eventComponent); - } - void VirusEditor::mouseExit(const juce::MouseEvent& event) - { - if (event.mouseWasDraggedSinceMouseDown()) - return; - updateControlLabel(nullptr); + if(event.eventComponent && event.eventComponent->getProperties().contains("parameter")) + updateControlLabel(event.eventComponent); } - void VirusEditor::mouseUp(const juce::MouseEvent& event) + void VirusEditor::timerCallback() { - if (event.mouseWasDraggedSinceMouseDown()) - return; - updateControlLabel(event.eventComponent); + updateControlLabel(nullptr); } - void VirusEditor::updateControlLabel(juce::Component* _component) const + void VirusEditor::updateControlLabel(juce::Component* _component) { + stopTimer(); + if(_component) { // combo boxes report the child label as event source, try the parent in this case @@ -326,7 +338,7 @@ namespace genericVirusUI m_focusedParameterName->setVisible(true); m_focusedParameterValue->setVisible(true); - if(m_focusedParameterTooltip && dynamic_cast<juce::Slider*>(_component)) + if(m_focusedParameterTooltip && dynamic_cast<juce::Slider*>(_component) && _component->isShowing()) { int x = _component->getX(); int y = _component->getY(); @@ -362,6 +374,12 @@ namespace genericVirusUI m_focusedParameterTooltip->setVisible(true); m_focusedParameterTooltip->toFront(false); } + else if(m_focusedParameterTooltip) + { + m_focusedParameterTooltip->setVisible(false); + } + + startTimer(3000); } void VirusEditor::updatePresetName() const @@ -433,14 +451,14 @@ namespace genericVirusUI } juce::PopupMenu banksMenu; - for(auto b=0; b<getController().getBankCount(); ++b) + for(uint8_t b=0; b<static_cast<uint8_t>(getController().getBankCount()); ++b) { std::stringstream bankName; bankName << "Bank " << static_cast<char>('A' + b); addEntry(banksMenu, bankName.str(), [this, b](const FileType _type) { - savePresets(SaveType::Bank, _type, static_cast<uint8_t>(b)); + savePresets(SaveType::Bank, _type, b); }); } @@ -501,7 +519,7 @@ namespace genericVirusUI { const auto playMode = getController().getParameterIndexByName(Virus::g_paramPlayMode); - getController().getParameter(playMode)->setValue(_playMode); + getController().getParameter(playMode)->setValue(_playMode, pluginLib::Parameter::ChangedBy::Ui); if (_playMode == virusLib::PlayModeSingle && getController().getCurrentPart() != 0) m_parameterBinding.setPart(0); diff --git a/source/jucePlugin/ui3/VirusEditor.h b/source/jucePlugin/ui3/VirusEditor.h @@ -7,13 +7,19 @@ #include "FxPage.h" #include "MidiPorts.h" #include "PatchBrowser.h" +#include "ControllerLinks.h" + +namespace pluginLib +{ + class Parameter; +} class VirusParameterBinding; class AudioPluginAudioProcessor; namespace genericVirusUI { - class VirusEditor : public genericUI::EditorInterface, public genericUI::Editor + class VirusEditor : public genericUI::EditorInterface, public genericUI::Editor, juce::Timer { public: enum class FileType @@ -56,12 +62,10 @@ namespace genericVirusUI void onPlayModeChanged(); void onCurrentPartChanged(); - void mouseDrag(const juce::MouseEvent& event) override; void mouseEnter(const juce::MouseEvent& event) override; - void mouseExit(const juce::MouseEvent& event) override; - void mouseUp(const juce::MouseEvent& event) override; + void timerCallback() override; - void updateControlLabel(juce::Component* _component) const; + void updateControlLabel(juce::Component* _component); void updatePresetName() const; void updatePlayModeButtons() const; @@ -85,6 +89,7 @@ namespace genericVirusUI std::unique_ptr<MidiPorts> m_midiPorts; std::unique_ptr<FxPage> m_fxPage; std::unique_ptr<PatchBrowser> m_patchBrowser; + std::unique_ptr<ControllerLinks> m_controllerLinks; juce::Label* m_presetName = nullptr; juce::Label* m_focusedParameterName = nullptr; @@ -103,6 +108,7 @@ namespace genericVirusUI std::unique_ptr<juce::FileChooser> m_fileChooser; juce::String m_previousPath; std::function<void()> m_openMenuCallback; + std::vector<pluginLib::Parameter*> m_boundParameters; std::map<std::string, std::vector<char>> m_fileCache; }; diff --git a/source/jucePluginLib/CMakeLists.txt b/source/jucePluginLib/CMakeLists.txt @@ -7,6 +7,7 @@ set(SOURCES parameter.cpp parameter.h parameterdescription.cpp parameterdescription.h parameterdescriptions.cpp parameterdescriptions.h + parameterlink.cpp parameterlink.h ) add_library(jucePluginLib STATIC) diff --git a/source/jucePluginLib/controller.cpp b/source/jucePluginLib/controller.cpp @@ -44,7 +44,7 @@ namespace pluginLib const auto& existingParams = findSynthParam(idx); for (auto& existingParam : existingParams) - existingParam->addLinkedParameter(p.get()); + existingParam->addDerivedParameter(p.get()); } const bool isNonPartExclusive = desc.isNonPartSensitive(); diff --git a/source/jucePluginLib/controller.h b/source/jucePluginLib/controller.h @@ -30,6 +30,8 @@ namespace pluginLib bool parseMidiPacket(const std::string& _name, MidiPacket::Data& _data, MidiPacket::ParamValues& _parameterValues, const std::vector<uint8_t>& _src) const; bool parseMidiPacket(std::string& _name, MidiPacket::Data& _data, MidiPacket::ParamValues& _parameterValues, const std::vector<uint8_t>& _src) const; + const auto& getExposedParameters() { return m_synthParams; } + protected: virtual Parameter* createParameter(Controller& _controller, const Description& _desc, uint8_t _part, int _uid); void registerParams(juce::AudioProcessor& _processor); diff --git a/source/jucePluginLib/parameter.cpp b/source/jucePluginLib/parameter.cpp @@ -30,7 +30,7 @@ namespace pluginLib func.second(); } - void Parameter::setLinkedValue(const int _value) + void Parameter::setDerivedValue(const int _value, ChangedBy _origin) { const int newValue = juce::roundToInt(m_range.getRange().clipValue(static_cast<float>(_value))); @@ -38,11 +38,14 @@ namespace pluginLib return; m_lastValue = newValue; + m_lastValueOrigin = _origin; if(getDescription().isPublic) { beginChangeGesture(); - setValueNotifyingHost(convertTo0to1(static_cast<float>(newValue))); + const float v = convertTo0to1(static_cast<float>(newValue)); + setValue(v, _origin); + sendValueChangedMessageToListeners(v); endChangeGesture(); } else @@ -53,28 +56,34 @@ namespace pluginLib bool Parameter::isMetaParameter() const { - return !m_linkedParameters.empty(); + return !m_derivedParameters.empty(); } - void Parameter::setValue(float newValue) + void Parameter::setValue(const float _newValue) { - if (m_changingLinkedValues) + setValue(_newValue, ChangedBy::HostAutomation); + } + + void Parameter::setValue(const float _newValue, const ChangedBy _origin) + { + if (m_changingDerivedValues) return; - m_value.setValue(convertFrom0to1(newValue)); + m_lastValueOrigin = _origin; + m_value.setValue(convertFrom0to1(_newValue)); - m_changingLinkedValues = true; + m_changingDerivedValues = true; - for (const auto& parameter : m_linkedParameters) + for (const auto& parameter : m_derivedParameters) { - if(!parameter->m_changingLinkedValues) - parameter->setLinkedValue(m_value.getValue()); + if(!parameter->m_changingDerivedValues) + parameter->setDerivedValue(m_value.getValue(), _origin); } - m_changingLinkedValues = false; - } + m_changingDerivedValues = false; + } - void Parameter::setValueFromSynth(int newValue, const bool notifyHost) + void Parameter::setValueFromSynth(int newValue, const bool notifyHost, ChangedBy _origin) { const auto clampedValue = juce::roundToInt(m_range.getRange().clipValue(static_cast<float>(newValue))); @@ -82,11 +91,14 @@ namespace pluginLib return; m_lastValue = clampedValue; + m_lastValueOrigin = _origin; if (notifyHost && getDescription().isPublic) { beginChangeGesture(); - setValueNotifyingHost(convertTo0to1(static_cast<float>(clampedValue))); + const auto v = convertTo0to1(static_cast<float>(clampedValue)); + setValue(v, _origin); + sendValueChangedMessageToListeners(v); endChangeGesture(); } else @@ -94,15 +106,15 @@ namespace pluginLib m_value.setValue(clampedValue); } - if (m_changingLinkedValues) + if (m_changingDerivedValues) return; - m_changingLinkedValues = true; + m_changingDerivedValues = true; - for (const auto& p : m_linkedParameters) - p->setLinkedValue(newValue); + for (const auto& p : m_derivedParameters) + p->setDerivedValue(newValue, _origin); - m_changingLinkedValues = false; + m_changingDerivedValues = false; } bool Parameter::removeListener(const uint32_t _id) @@ -137,18 +149,18 @@ namespace pluginLib return 0; } - void Parameter::addLinkedParameter(Parameter* _param) + void Parameter::addDerivedParameter(Parameter* _param) { if (_param == this) return; - for (auto* p : m_linkedParameters) + for (auto* p : m_derivedParameters) { - _param->m_linkedParameters.insert(p); - p->m_linkedParameters.insert(_param); + _param->m_derivedParameters.insert(p); + p->m_derivedParameters.insert(_param); } - m_linkedParameters.insert(_param); - _param->m_linkedParameters.insert(this); + m_derivedParameters.insert(_param); + _param->m_derivedParameters.insert(this); } } diff --git a/source/jucePluginLib/parameter.h b/source/jucePluginLib/parameter.h @@ -15,6 +15,15 @@ namespace pluginLib class Parameter : juce::Value::Listener, public juce::RangedAudioParameter { public: + enum class ChangedBy + { + Unknown, + PresetChange, + ControlChange, + HostAutomation, + Ui, + }; + Parameter(Controller& _controller, const Description& _desc, uint8_t _partNum, int uniqueId); juce::Value &getValueObject() { return m_value; }; @@ -29,8 +38,9 @@ namespace pluginLib bool isMetaParameter() const override; float getValue() const override { return convertTo0to1(m_value.getValue()); } - void setValue(float newValue) override; - void setValueFromSynth(int newValue, bool notifyHost = true); + void setValue(float _newValue) override; + void setValue(float _newValue, ChangedBy _origin); + void setValueFromSynth(int newValue, bool notifyHost, ChangedBy _origin); bool isDiscrete() const override { return m_desc.isDiscrete; } bool isBoolean() const override { return m_desc.isBool; } @@ -59,18 +69,20 @@ namespace pluginLib // eg. multi/single value change requires triggering more logic. std::list<std::pair<uint32_t, std::function<void()>>> onValueChanged; - void addLinkedParameter(Parameter* _param); + void addDerivedParameter(Parameter* _param); int getUniqueId() const { return m_uniqueId; } - const std::set<Parameter*>& getLinkedParameters() { return m_linkedParameters; } + const std::set<Parameter*>& getDerivedParameters() { return m_derivedParameters; } bool removeListener(uint32_t _id); + ChangedBy getChangeOrigin() const { return m_lastValueOrigin; } + private: static juce::String genId(const Description &d, int part, int uniqueId); void valueChanged(juce::Value &) override; - void setLinkedValue(int _value); + void setDerivedValue(int _value, ChangedBy _origin); Controller &m_ctrl; const Description m_desc; @@ -78,8 +90,9 @@ namespace pluginLib const uint8_t m_partNum; const int m_uniqueId; // 0 for all unique parameters, > 0 if multiple Parameter instances reference a single synth parameter int m_lastValue{-1}; + ChangedBy m_lastValueOrigin = ChangedBy::Unknown; juce::Value m_value; - std::set<Parameter*> m_linkedParameters; - bool m_changingLinkedValues = false; + std::set<Parameter*> m_derivedParameters; + bool m_changingDerivedValues = false; }; } diff --git a/source/jucePluginLib/parameterdescriptions.cpp b/source/jucePluginLib/parameterdescriptions.cpp @@ -317,9 +317,11 @@ namespace pluginLib } const auto midipackets = json["midipackets"].getDynamicObject(); - parseMidiPackets(errors, midipackets); + const auto parameterLinks = json["parameterlinks"].getArray(); + parseParameterLinks(errors, parameterLinks); + auto res = errors.str(); if(!res.empty()) @@ -515,4 +517,91 @@ namespace pluginLib if(!hasErrors) m_midiPackets.insert(std::make_pair(_key, packet)); } + + void ParameterDescriptions::parseParameterLinks(std::stringstream& _errors, juce::Array<juce::var>* _links) + { + if(!_links) + return; + + for(int i=0; i<_links->size(); ++i) + { + const auto& value = (*_links)[i]; + parseParameterLink(_errors, value); + } + } + + void ParameterDescriptions::parseParameterLink(std::stringstream& _errors, const juce::var& _value) + { + const auto source = _value["source"].toString().toStdString(); + const auto dest = _value["dest"].toString().toStdString(); + const auto conditionParam = _value["conditionParameter"].toString().toStdString(); + const auto conditionValues = _value["conditionValues"].getArray(); + const auto linkType = _value["link"].toString().toStdString(); + + ParameterLink link; + + if(!getIndexByName(link.source, source)) + { + _errors << "Source parameter " << source << " not found for parameter link" << std::endl; + return; + } + + if(!getIndexByName(link.dest, dest)) + { + _errors << "Destination parameter " << dest << " not found for parameter link" << std::endl; + return; + } + + if(link.source == link.dest) + { + _errors << "Cannot link source parameter " << source << " to itself" << std::endl; + return; + } + + if(linkType == "relative") + { + link.mode = ParameterLink::LinkMode::Relative; + } + else if(linkType == "absolute") + { + link.mode = ParameterLink::LinkMode::Absolute; + } + else + { + _errors << "Parameter link mode " << linkType << " is not a value value, must be either relative or absolute" << std::endl; + return; + } + + if(conditionParam.empty()) + { + if(conditionValues && conditionValues->size() > 1) + { + _errors << "Condition parameter not specified for conditional parameter link of " << source << " to " << dest << ". Either specify no condition parameter and no values, or specify both" << std::endl; + return; + } + } + + if(!conditionParam.empty()) + { + if(conditionValues == nullptr || conditionValues->isEmpty()) + { + _errors << "Condition parameter list is empty or invalid although the condition parameter " << conditionParam << " has been specified for parameter link of " << source << " to " << dest << ". Either specify no condition parameter and no values, or specify both" << std::endl; + return; + } + + if(!getIndexByName(link.conditionParameter, conditionParam)) + { + _errors << "Link condition parameter " << conditionParam << " not found for parameter link" << std::endl; + return; + } + + for(auto i=0; i<conditionValues->size(); ++i) + { + const int v = (*conditionValues)[i]; + link.conditionValues.insert(static_cast<uint8_t>(v)); + } + } + + m_parameterLinks.push_back(link); + } } diff --git a/source/jucePluginLib/parameterdescriptions.h b/source/jucePluginLib/parameterdescriptions.h @@ -6,6 +6,7 @@ #include "midipacket.h" #include "parameterdescription.h" +#include "parameterlink.h" namespace pluginLib { @@ -32,9 +33,13 @@ namespace pluginLib void parseMidiPackets(std::stringstream& _errors, juce::DynamicObject* _packets); void parseMidiPacket(std::stringstream& _errors, const std::string& _key, const juce::var& _value); + void parseParameterLinks(std::stringstream& _errors, juce::Array<juce::var>* _links); + void parseParameterLink(std::stringstream& _errors, const juce::var& _value); + std::map<std::string, ValueList> m_valueLists; std::vector<Description> m_descriptions; std::map<std::string, uint32_t> m_nameToIndex; std::map<std::string, MidiPacket> m_midiPackets; + std::vector<ParameterLink> m_parameterLinks; }; } diff --git a/source/jucePluginLib/parameterlink.cpp b/source/jucePluginLib/parameterlink.cpp diff --git a/source/jucePluginLib/parameterlink.h b/source/jucePluginLib/parameterlink.h @@ -0,0 +1,34 @@ +#pragma once + +#include <set> +#include <vector> + +namespace pluginLib +{ + struct ParameterLink + { + enum class LinkMode + { + Absolute, + Relative + }; + + bool isValid() const + { + return source != dest; + } + + bool hasConditions() const + { + return !conditionValues.empty(); + } + + uint32_t source = 0; + uint32_t dest = 0; + + uint32_t conditionParameter; + std::set<uint8_t> conditionValues; + + LinkMode mode = LinkMode::Absolute; + }; +} diff --git a/source/juceUiLib/CMakeLists.txt b/source/juceUiLib/CMakeLists.txt @@ -5,6 +5,7 @@ set(SOURCES editor.cpp editor.h editorInterface.h condition.cpp condition.h + controllerlink.cpp controllerlink.h image.cpp image.h rotaryStyle.cpp rotaryStyle.h comboboxStyle.cpp comboboxStyle.h diff --git a/source/juceUiLib/controllerlink.cpp b/source/juceUiLib/controllerlink.cpp @@ -0,0 +1,82 @@ +#include "controllerlink.h" + +#include "editor.h" + +namespace genericUI +{ + ControllerLink::ControllerLink(std::string _source, std::string _dest, std::string _conditionParam) + : m_sourceName(std::move(_source)) + , m_destName(std::move(_dest)) + , m_conditionParam(std::move(_conditionParam)) + { + } + + ControllerLink::~ControllerLink() + { + if(m_source) + { + m_source->removeListener(this); + m_source->removeComponentListener(this); + } + } + + void ControllerLink::create(const Editor& _editor) + { + create( + _editor.findComponentT<juce::Slider>(m_sourceName), + _editor.findComponentT<juce::Slider>(m_destName), + _editor.findComponentT<juce::Button>(m_conditionParam) + ); + } + + void ControllerLink::create(juce::Slider* _source, juce::Slider* _dest, juce::Button* _condition) + { + m_source = _source; + m_dest = _dest; + m_condition = _condition; + + m_lastSourceValue = m_source->getValue(); + m_source->addListener(this); + m_source->addComponentListener(this); + } + + void ControllerLink::sliderValueChanged(juce::Slider* _slider) + { + const auto current = m_source->getValue(); + const auto delta = current - m_lastSourceValue; + m_lastSourceValue = current; + + if(!m_sourceIsBeingDragged) + return; + + if(std::abs(delta) < 0.0001) + return; + + if(m_condition && !m_condition->getToggleState()) + return; + + const auto destValue = m_dest->getValue(); + const auto newDestValue = destValue + delta; + + if(std::abs(newDestValue - destValue) < 0.0001) + return; + + m_dest->setValue(newDestValue); + } + + void ControllerLink::sliderDragStarted(juce::Slider*) + { + m_lastSourceValue = m_source->getValue(); + m_sourceIsBeingDragged = true; + } + + void ControllerLink::sliderDragEnded(juce::Slider*) + { + m_sourceIsBeingDragged = false; + } + + void ControllerLink::componentBeingDeleted(juce::Component& component) + { + m_source = nullptr; + } +} diff --git a/source/juceUiLib/controllerlink.h b/source/juceUiLib/controllerlink.h @@ -0,0 +1,44 @@ +#pragma once + +#include <string> + +#include <juce_audio_processors/juce_audio_processors.h> + +namespace genericUI +{ + class Editor; + + class ControllerLink : juce::Slider::Listener, juce::ComponentListener + { + public: + ControllerLink(ControllerLink&&) = delete; + ControllerLink(const ControllerLink&) = delete; + + ControllerLink(std::string _source = std::string(), std::string _dest = std::string(), std::string _conditionParam = std::string()); + ~ControllerLink() override; + + void create(const Editor& _editor); + + void create(juce::Slider* _source, juce::Slider* _dest, juce::Button* _condition); + + void operator = (const ControllerLink&) = delete; + void operator = (ControllerLink&&) = delete; + + private: + void sliderValueChanged(juce::Slider* _slider) override; + void sliderDragStarted(juce::Slider*) override; + void sliderDragEnded(juce::Slider*) override; + + void componentBeingDeleted(juce::Component& component) override; + + const std::string m_sourceName; + const std::string m_destName; + const std::string m_conditionParam; + + juce::Slider* m_source = nullptr; + juce::Slider* m_dest = nullptr; + juce::Button* m_condition = nullptr; + double m_lastSourceValue = 0; + bool m_sourceIsBeingDragged = false; + }; +} diff --git a/source/juceUiLib/editor.cpp b/source/juceUiLib/editor.cpp @@ -57,6 +57,7 @@ namespace genericUI m_rootObject->createJuceTree(*this); m_rootObject->createTabGroups(*this); + m_rootObject->createControllerLinks(*this); m_scale = m_rootObject->getPropertyFloat("scale", 1.0f); } @@ -207,6 +208,11 @@ namespace genericUI return m_rootObject ? m_rootObject->getConditionCountRecursive() : 0; } + size_t Editor::getControllerLinkCountRecursive() const + { + return m_rootObject ? m_rootObject->getControllerLinkCountRecursive() : 0; + } + void Editor::setEnabled(juce::Component& _component, const bool _enable) { if(_component.getProperties().contains("disabledAlpha")) diff --git a/source/juceUiLib/editor.h b/source/juceUiLib/editor.h @@ -73,6 +73,7 @@ namespace genericUI } size_t getConditionCountRecursive() const; + size_t getControllerLinkCountRecursive() const; static void setEnabled(juce::Component& _component, bool _enable); diff --git a/source/juceUiLib/uiObject.cpp b/source/juceUiLib/uiObject.cpp @@ -64,6 +64,17 @@ namespace genericUI } } + void UiObject::createControllerLinks(Editor& _editor) + { + for (auto& link : m_controllerLinks) + link->create(_editor); + + for (auto& ch : m_children) + { + ch->createControllerLinks(_editor); + } + } + void UiObject::apply(Editor& _editor, juce::Component& _target) { const auto x = getPropertyInt("x"); @@ -252,6 +263,16 @@ namespace genericUI return count; } + size_t UiObject::getControllerLinkCountRecursive() const + { + size_t count = m_controllerLinks.size(); + + for (const auto & c : m_children) + count += c->getControllerLinkCountRecursive(); + + return count; + } + void UiObject::createCondition(Editor& _editor, juce::Component& _target) { if(!hasComponent("condition")) @@ -359,6 +380,30 @@ namespace genericUI m_tabGroup = TabGroup(name, pagesVec, buttonVec); } + else if(key == "controllerlinks") + { + auto* entries = value.getArray(); + + if(entries && !entries->isEmpty()) + { + for(auto j=0; j<entries->size(); ++j) + { + const auto& e = (*entries)[j]; + const auto source = e["source"].toString().toStdString(); + const auto dest = e["dest"].toString().toStdString(); + const auto condition = e["condition"].toString().toStdString(); + + if(source.empty()) + throw std::runtime_error("source for controller link needs to have a name"); + if(dest.empty()) + throw std::runtime_error("destination for controller link needs to have a name"); + if(condition.empty()) + throw std::runtime_error("condition for controller link needs to have a name"); + + m_controllerLinks.emplace_back(new ControllerLink(source, dest, condition)); + } + } + } else { auto* componentDesc = value.getDynamicObject(); diff --git a/source/juceUiLib/uiObject.h b/source/juceUiLib/uiObject.h @@ -7,6 +7,7 @@ #include <vector> #include "condition.h" +#include "controllerlink.h" #include "tabgroup.h" namespace juce @@ -38,6 +39,7 @@ namespace genericUI void createJuceTree(Editor& _editor); void createChildObjects(Editor& _editor, juce::Component& _parent) const; void createTabGroups(Editor& _editor); + void createControllerLinks(Editor& _editor); void apply(Editor& _editor, juce::Component& _target); void apply(Editor& _editor, juce::Slider& _target); @@ -56,6 +58,7 @@ namespace genericUI std::string getProperty(const std::string& _key, const std::string& _default = std::string()) const; size_t getConditionCountRecursive() const; + size_t getControllerLinkCountRecursive() const; private: bool hasComponent(const std::string& _component) const; @@ -82,6 +85,7 @@ namespace genericUI std::unique_ptr<Condition> m_condition; TabGroup m_tabGroup; + std::vector<std::unique_ptr<ControllerLink>> m_controllerLinks; }; inline bool UiObject::hasComponent(const std::string& _component) const diff --git a/source/juceUiLib/uiObjectStyle.cpp b/source/juceUiLib/uiObjectStyle.cpp @@ -122,15 +122,9 @@ namespace genericUI juce::Font UiObjectStyle::getPopupMenuFont() { - if(!m_fontFile.empty()) - { - auto font = juce::Font(m_editor.getFont(m_fontFile).getTypeface()); - applyFontProperties(font); - return font; - } - auto font = LookAndFeel_V4::getPopupMenuFont(); - applyFontProperties(font); - return font; + auto f = LookAndFeel_V4::getPopupMenuFont(); + f.setHeight(f.getHeight() * 2.0f); + return f; } juce::Font UiObjectStyle::getTextButtonFont(juce::TextButton& _textButton, int buttonHeight) @@ -146,6 +140,16 @@ namespace genericUI return font; } + bool UiObjectStyle::shouldPopupMenuScaleWithTargetComponent(const juce::PopupMenu::Options& _options) + { + return true; + } + + juce::PopupMenu::Options UiObjectStyle::getOptionsForComboBoxPopupMenu(juce::ComboBox& _comboBox, juce::Label& _label) + { + return LookAndFeel_V4::getOptionsForComboBoxPopupMenu(_comboBox, _label).withStandardItemHeight(0); + } + void UiObjectStyle::applyFontProperties(juce::Font& _font) const { if(m_textHeight) diff --git a/source/juceUiLib/uiObjectStyle.h b/source/juceUiLib/uiObjectStyle.h @@ -23,6 +23,8 @@ namespace genericUI juce::Font getLabelFont(juce::Label&) override; juce::Font getPopupMenuFont() override; juce::Font getTextButtonFont(juce::TextButton&, int buttonHeight) override; + bool shouldPopupMenuScaleWithTargetComponent (const juce::PopupMenu::Options&) override; + juce::PopupMenu::Options getOptionsForComboBoxPopupMenu(juce::ComboBox&, juce::Label&) override; void applyFontProperties(juce::Font& _font) const; diff --git a/source/virusLib/hdi08TxParser.cpp b/source/virusLib/hdi08TxParser.cpp @@ -13,7 +13,7 @@ namespace virusLib { const std::vector<dsp56k::TWord> g_knownPatterns[] = { - {0xf40000, 0x7f0000, 0x000000, 0xf70000} // sent after DSP has booted + {0xf40000, 0x7f0000} // sent after DSP has booted }; bool Hdi08TxParser::append(const dsp56k::TWord _data)