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 826e2409ccdd38bc81cab6668bfb039bd8979d43
parent e9fe30838e0512fc5de8e4f6930800db51a055ba
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Sat,  7 Jan 2023 23:53:16 +0100

update OSS version

Diffstat:
MCMakeLists.txt | 22+++++++++++++++++++---
Mbuild_win64_vs19.bat | 2+-
Mdoc/VirusEmulator.tdl | 0
Mdoc/changelog.txt | 10++++++++++
Ascripts/deploy.cmake | 20++++++++++++++++++++
Ascripts/pack.cmake | 22++++++++++++++++++++++
Ascripts/rclone.cmake | 30++++++++++++++++++++++++++++++
Msource/jucePlugin/CMakeLists.txt | 9+--------
Msource/jucePlugin/PluginEditor.h | 3++-
Dsource/jucePlugin/PluginEditorSkin2.cpp | 43-------------------------------------------
Dsource/jucePlugin/PluginEditorSkin2.h | 34----------------------------------
Msource/jucePlugin/PluginEditorState.cpp | 218++-----------------------------------------------------------------------------
Msource/jucePlugin/PluginEditorState.h | 54++++--------------------------------------------------
Msource/jucePlugin/PluginProcessor.cpp | 208+++++++++++--------------------------------------------------------------------
Msource/jucePlugin/PluginProcessor.h | 44+++++++++++++-------------------------------
Msource/jucePlugin/VirusController.cpp | 89+++++++++++++++++++++++++++++++++----------------------------------------------
Msource/jucePlugin/VirusController.h | 35+++++++++++++----------------------
Dsource/jucePlugin/VirusParameterBinding.cpp | 248-------------------------------------------------------------------------------
Dsource/jucePlugin/VirusParameterBinding.h | 79-------------------------------------------------------------------------------
Msource/jucePlugin/parameterDescriptions_C.json | 6+++---
Msource/jucePlugin/ui3/FxPage.cpp | 6+++---
Dsource/jucePlugin/ui3/MidiPorts.cpp | 148-------------------------------------------------------------------------------
Dsource/jucePlugin/ui3/MidiPorts.h | 33---------------------------------
Msource/jucePlugin/ui3/Parts.cpp | 7++-----
Msource/jucePlugin/ui3/PatchBrowser.cpp | 555++++++++++++++++++-------------------------------------------------------------
Msource/jucePlugin/ui3/PatchBrowser.h | 88++++++++++++++++---------------------------------------------------------------
Msource/jucePlugin/ui3/VirusEditor.cpp | 130++++++++++++++++++-------------------------------------------------------------
Msource/jucePlugin/ui3/VirusEditor.h | 30++++++++++--------------------
Asource/jucePluginEditorLib/CMakeLists.txt | 20++++++++++++++++++++
Asource/jucePluginEditorLib/midiPorts.cpp | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/midiPorts.h | 37+++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchbrowser.cpp | 371+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchbrowser.h | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/pluginEditor.cpp | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/pluginEditor.h | 35+++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/pluginEditorState.cpp | 234+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/pluginEditorState.h | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/pluginEditorWindow.cpp | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/pluginEditorWindow.h | 31+++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/pluginProcessor.cpp | 20++++++++++++++++++++
Asource/jucePluginEditorLib/pluginProcessor.h | 18++++++++++++++++++
Msource/jucePluginLib/CMakeLists.txt | 3+++
Msource/jucePluginLib/controller.cpp | 47+++++++++++++++++++++++++++++++++++++++++++----
Msource/jucePluginLib/controller.h | 36++++++++++++++++++++++++++++++++----
Msource/jucePluginLib/midipacket.cpp | 32+++++++++++++++++++++++---------
Msource/jucePluginLib/midipacket.h | 15+++++++++++++--
Msource/jucePluginLib/parameter.cpp | 6++++--
Msource/jucePluginLib/parameter.h | 1+
Asource/jucePluginLib/parameterbinding.cpp | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/parameterbinding.h | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePluginLib/parameterdescription.h | 1+
Msource/jucePluginLib/parameterdescriptions.cpp | 17+++++++++++++----
Msource/jucePluginLib/parameterdescriptions.h | 10+++++-----
Asource/jucePluginLib/processor.cpp | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/processor.h | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/juceUiLib/buttonStyle.cpp | 20+++++++++++++++++---
Msource/juceUiLib/condition.cpp | 29+++++++++++++++++++++++++----
Msource/juceUiLib/condition.h | 8++++++--
Msource/juceUiLib/editor.cpp | 5+++++
Msource/juceUiLib/editor.h | 2++
Msource/juceUiLib/editorInterface.h | 2+-
Msource/juceUiLib/rotaryStyle.cpp | 11+++++++++--
Msource/juceUiLib/uiObject.cpp | 27+++++++++++++++++++++------
Msource/juceUiLib/uiObject.h | 2++
Msource/synthLib/os.cpp | 40++++++++++++++++++++++++++++++++++++++++
Msource/synthLib/os.h | 12++++++++++++
Msource/synthLib/resampler.cpp | 5++++-
Msource/synthLib/resampler.h | 6++++--
Msource/virusConsoleLib/consoleApp.cpp | 6+++---
Msource/virusIntegrationTest/CMakeLists.txt | 5+++++
Asource/virusIntegrationTest/runTest.cmake | 14++++++++++++++
Msource/virusLib/CMakeLists.txt | 3+++
Msource/virusLib/device.cpp | 11++++-------
Msource/virusLib/device.h | 1-
Asource/virusLib/hdi08List.cpp | 37+++++++++++++++++++++++++++++++++++++
Asource/virusLib/hdi08List.h | 37+++++++++++++++++++++++++++++++++++++
Asource/virusLib/hdi08MidiQueue.cpp | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/virusLib/hdi08MidiQueue.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/virusLib/hdi08Queue.cpp | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/virusLib/hdi08Queue.h | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/virusLib/hdi08TxParser.cpp | 11++++++++++-
Msource/virusLib/hdi08TxParser.h | 2++
Msource/virusLib/microcontroller.cpp | 182++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msource/virusLib/microcontroller.h | 35++++++++++++++++++++---------------
Msource/virusLib/romfile.h | 1+
85 files changed, 3087 insertions(+), 1927 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -5,9 +5,11 @@ 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.22) +project(gearmulator VERSION 1.2.25) include(base.cmake) +include(CTest) +include(${CMAKE_CURRENT_LIST_DIR}/scripts/rclone.cmake) set(ASMJIT_STATIC TRUE) set(ASMJIT_NO_INSTALL TRUE) @@ -21,8 +23,21 @@ add_subdirectory(source/virusLib) add_subdirectory(source/virusConsoleLib) add_subdirectory(source/libresample) -if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/source/vstsdk2.4.2/public.sdk/source/vst2.x/audioeffect.h) - set(JUCE_GLOBAL_VST2_SDK_PATH ${CMAKE_CURRENT_SOURCE_DIR}/source/vstsdk2.4.2/) +# ----------------- Try to install VST2 SDK + +set(VST2SDK_TESTFILE ${CMAKE_CURRENT_SOURCE_DIR}/source/vstsdk2.4.2/public.sdk/source/vst2.x/audioeffect.h) +set(VST2SDK_FOLDER ${CMAKE_CURRENT_SOURCE_DIR}/source/vstsdk2.4.2/) + +if(NOT EXISTS ${VST2SDK_TESTFILE}) + if(EXISTS ${RCLONE_CONF}) + copyDataFrom("vstsdk2.4.2/" "${CMAKE_CURRENT_LIST_DIR}/source/vstsdk2.4.2/") + else() + message(WARNING "rclone.conf not found, unable to copy VST2 SDK") + endif() +endif() + +if(EXISTS ${VST2SDK_TESTFILE}) + set(JUCE_GLOBAL_VST2_SDK_PATH ${VST2SDK_FOLDER}) endif() # ----------------- Juce based audio plugin @@ -34,6 +49,7 @@ if(${PROJECT_NAME}_BUILD_JUCEPLUGIN) add_subdirectory(source/jucePluginLib) add_subdirectory(source/juceUiLib) add_subdirectory(source/jucePlugin) + add_subdirectory(source/jucePluginEditorLib) endif() # ----------------- Console Programs diff --git a/build_win64_vs19.bat b/build_win64_vs19.bat @@ -10,6 +10,6 @@ IF %ERRORLEVEL% NEQ 0 ( popd exit /B 2 ) -cpack -G ZIP +cmake -P ../../scripts/pack.cmake popd move /y %outdir%*.zip deploy\ \ No newline at end of file diff --git a/doc/VirusEmulator.tdl b/doc/VirusEmulator.tdl Binary files differ. diff --git a/doc/changelog.txt b/doc/changelog.txt @@ -1,5 +1,15 @@ Release Notes +1.2.24 (2022.12.16) + +- [Fix] AU validation failed on MacOS (1.2.23 regression) +- [Fix] Incorrect waveform labels for Oscillator 1 & 2 (Wave 2-63 => Wave 3-64) + +1.2.23 (2022.12.12) + +- [Imp] Faster startup time +- [Fix] Fix delay/reverb and other global or part specific parameters to work incorrectly (1.2.22 regression) + 1.2.22 (2022.12.08): Osirus: diff --git a/scripts/deploy.cmake b/scripts/deploy.cmake @@ -0,0 +1,20 @@ +include(${CMAKE_CURRENT_LIST_DIR}/rclone.cmake) + +if(NOT FOLDER) + message(FATAL_ERROR "no upload folder specified") +endif() + +if(NOT UPLOAD_LOCAL AND NOT UPLOAD_REMOTE) + message(FATAL_ERROR "neither upload to local nor remote is set") +endif() + +if(EXISTS ${RCLONE_CONF}) + if(UPLOAD_LOCAL) + copyArtefacts("dsp56300:deploy" ${FOLDER}) + endif() + if(UPLOAD_REMOTE) + copyArtefacts("dsp56300_ftp:builds" ${FOLDER}) + endif() +else() + message(FATAL_ERROR "rclone.conf not found, unable to deploy/upload") +endif() diff --git a/scripts/pack.cmake b/scripts/pack.cmake @@ -0,0 +1,22 @@ +message(STATUS "Removing old packages") +file(REMOVE *.zip) +file(REMOVE *.deb) +file(REMOVE *.rpm) + +macro(pack GENERATOR) + message(STATUS "Packaging ${GENERATOR}") + set(PACK_RESULT 0) + execute_process(COMMAND cpack -G ${GENERATOR} + COMMAND_ECHO STDOUT + RESULT_VARIABLE PACK_RESULT) + if(PACK_RESULT) + message(FATAL_ERROR "Failed to execute cpack: " ${PACK_RESULT}) + endif() +endmacro() + +pack("ZIP") + +if(UNIX AND NOT APPLE) + pack("DEB") + pack("RPM") +endif() diff --git a/scripts/rclone.cmake b/scripts/rclone.cmake @@ -0,0 +1,30 @@ +if(NOT ROOT_DIR) + set(ROOT_DIR ${CMAKE_BINARY_DIR}) +endif() + +if(NOT RCLONE_CONF) + if(DEFINED ENV{RCLONE_CONF}) + set(RCLONE_CONF $ENV{RCLONE_CONF}) + else() + set(RCLONE_CONF ${ROOT_DIR}/rclone.conf) + endif() +endif() + +macro(copyArtefacts TARGET FOLDER) + set(RCLONE_RESULT 0) + execute_process(COMMAND rclone --config ${RCLONE_CONF} copy + --transfers=1 -v --include *.zip --include *.deb --include *.rpm --min-size 100 --max-depth 1 "${ROOT_DIR}" "${TARGET}/${FOLDER}" + COMMAND_ECHO STDOUT + RESULT_VARIABLE RCLONE_RESULT + WORKING_DIRECTORY ${ROOT_DIR}) + if(RCLONE_RESULT) + message(FATAL_ERROR "Failed to execute rclone: " ${CMD_RESULT}) + endif() +endmacro() + +macro(copyDataFrom FROM TO) + execute_process(COMMAND rclone --config "${RCLONE_CONF}" sync "dsp56300:${FROM}" "${TO}" COMMAND_ECHO STDOUT RESULT_VARIABLE RCLONE_RESULT) + if(RCLONE_RESULT) + message(FATAL_ERROR "Failed to execute rclone: " ${CMD_RESULT}) + endif() +endmacro() diff --git a/source/jucePlugin/CMakeLists.txt b/source/jucePlugin/CMakeLists.txt @@ -15,16 +15,12 @@ endif() set(SOURCES parameterDescriptions_C.json ParameterNames.h - PluginEditor.cpp - PluginEditor.h PluginEditorState.cpp PluginEditorState.h PluginProcessor.cpp PluginProcessor.h VirusController.cpp VirusController.h - VirusParameterBinding.cpp - VirusParameterBinding.h version.h ) @@ -33,8 +29,6 @@ set(SOURCES_UI3 ui3/ControllerLinks.h ui3/FxPage.cpp ui3/FxPage.h - ui3/MidiPorts.cpp - ui3/MidiPorts.h ui3/Parts.cpp ui3/Parts.h ui3/PatchBrowser.cpp @@ -91,8 +85,7 @@ macro(createJucePlugin targetName productName isSynth plugin4CC binaryDataProjec target_link_libraries(${targetName} PRIVATE ${binaryDataProject} - juceUiLib - jucePluginLib + jucePluginEditorLib juce::juce_audio_utils juce::juce_cryptography PUBLIC diff --git a/source/jucePlugin/PluginEditor.h b/source/jucePlugin/PluginEditor.h @@ -1,7 +1,8 @@ #pragma once -#include "VirusParameterBinding.h" +#include <juce_audio_processors/juce_audio_processors.h> +class AudioPluginAudioProcessor; class PluginEditorState; //============================================================================== diff --git a/source/jucePlugin/PluginEditorSkin2.cpp b/source/jucePlugin/PluginEditorSkin2.cpp @@ -1,43 +0,0 @@ -#include "PluginProcessor.h" -#include "PluginEditorSkin2.h" -#include "VirusController.h" -#include "version.h" -// AH TODO: need compiler switch -#include "ui2/VirusEditor.h" - -//============================================================================== -AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor(AudioPluginAudioProcessor &p) : - AudioProcessorEditor(&p), processorRef(p), m_parameterBinding(p), m_virusEditor(new VirusEditor(m_parameterBinding, processorRef)) -{ - const auto config = processorRef.getController().getConfig(); - - setResizable(true,false); - setScaleFactor(config->getDoubleValue("skin_scale_factor", 0.75f)); - setSize(m_virusEditor->iSkinSizeWidth, m_virusEditor->iSkinSizeHeight); - m_virusEditor->m_AudioPlugInEditor = (AudioPluginAudioProcessorEditor*)this; - - addAndMakeVisible(m_virusEditor); -} - -AudioPluginAudioProcessorEditor::~AudioPluginAudioProcessorEditor() -{ - delete m_virusEditor; -} - -//============================================================================== -void AudioPluginAudioProcessorEditor::paint (juce::Graphics& g) -{ -} - -void AudioPluginAudioProcessorEditor::timerCallback() -{ - // ugly (polling!) way for refreshing presets names as this is temporary ui -} - -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); -} diff --git a/source/jucePlugin/PluginEditorSkin2.h b/source/jucePlugin/PluginEditorSkin2.h @@ -1,34 +0,0 @@ -#pragma once - -#include "VirusParameterBinding.h" -#include "ui2/VirusEditor.h" -#include "PluginProcessor.h" -#include <juce_audio_devices/juce_audio_devices.h> - -//============================================================================== -class AudioPluginAudioProcessorEditor : public juce::AudioProcessorEditor, private juce::Timer -{ -public: - explicit AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor&); - ~AudioPluginAudioProcessorEditor() override; - - //============================================================================== - //void handleIncomingMidiMessage(juce::MidiInput *source, const juce::MidiMessage &message) override; - void paint (juce::Graphics&) override; - void resized() override; - -private: - void timerCallback() override; - - // This reference is provided as a quick way for your editor to - // access the processor object that created it. - AudioPluginAudioProcessor& processorRef; - VirusParameterBinding m_parameterBinding; - - // New "real" editor - VirusEditor *m_virusEditor; - juce::ComboBox m_scale; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPluginAudioProcessorEditor) - -}; diff --git a/source/jucePlugin/PluginEditorState.cpp b/source/jucePlugin/PluginEditorState.cpp @@ -6,227 +6,19 @@ #include "../synthLib/os.h" -const std::vector<PluginEditorState::Skin> m_includedSkins = +const std::vector<PluginEditorState::Skin> g_includedSkins = { {"Hoverland", "VirusC_Hoverland.json", ""}, {"Trancy", "VirusC_Trancy.json", ""}, {"Galaxpel", "VirusC_Galaxpel.json", ""} }; -PluginEditorState::PluginEditorState(AudioPluginAudioProcessor& _processor) : m_processor(_processor), m_parameterBinding(_processor.getController()) +PluginEditorState::PluginEditorState(AudioPluginAudioProcessor& _processor, pluginLib::Controller& _controller) : jucePluginEditorLib::PluginEditorState(_processor, _controller, g_includedSkins) { - 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]); - } + loadDefaultSkin(); } -void PluginEditorState::setGuiScale(int _scale) const +genericUI::Editor* PluginEditorState::createEditor(const Skin& _skin, std::function<void()> _openMenuCallback) { - 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()); + return new genericVirusUI::VirusEditor(m_parameterBinding, static_cast<AudioPluginAudioProcessor&>(m_processor), _skin.jsonFilename, _skin.folder, _openMenuCallback); } diff --git a/source/jucePlugin/PluginEditorState.h b/source/jucePlugin/PluginEditorState.h @@ -1,59 +1,13 @@ #pragma once -#include <string> - -#include "VirusParameterBinding.h" +#include "../jucePluginEditorLib/pluginEditorState.h" class AudioPluginAudioProcessor; -class PluginEditorState +class PluginEditorState : public jucePluginEditorLib::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; + explicit PluginEditorState(AudioPluginAudioProcessor& _processor, pluginLib::Controller& _controller); - std::unique_ptr<juce::Component> m_virusEditor; - Skin m_currentSkin; - float m_rootScale = 1.0f; + genericUI::Editor* createEditor(const Skin& _skin, std::function<void()> _openMenuCallback) override; }; diff --git a/source/jucePlugin/PluginProcessor.cpp b/source/jucePlugin/PluginProcessor.cpp @@ -1,32 +1,39 @@ #include "PluginProcessor.h" -#include "PluginEditor.h" #include "PluginEditorState.h" #include "ParameterNames.h" +#include "../jucePluginEditorLib/pluginEditorWindow.h" + #include <juce_audio_processors/juce_audio_processors.h> #include <juce_audio_devices/juce_audio_devices.h> -#include "../synthLib/os.h" +static juce::PropertiesFile::Options getConfigOptions() +{ + juce::PropertiesFile::Options opts; + opts.applicationName = "DSP56300 Emulator"; + opts.filenameSuffix = ".settings"; + opts.folderName = "DSP56300 Emulator"; + opts.osxLibrarySubFolder = "Application Support/DSP56300 Emulator"; + return opts; +} //============================================================================== AudioPluginAudioProcessor::AudioPluginAudioProcessor() : - AudioProcessor(BusesProperties() + jucePluginEditorLib::Processor(BusesProperties() .withInput("Input", juce::AudioChannelSet::stereo(), true) .withOutput("Output", juce::AudioChannelSet::stereo(), true) #if JucePlugin_IsSynth .withOutput("Out 2", juce::AudioChannelSet::stereo(), true) .withOutput("Out 3", juce::AudioChannelSet::stereo(), true) #endif - ), - MidiInputCallback(), + , getConfigOptions()), m_romName(virusLib::ROMFile::findROM()), m_rom(m_romName), m_device(m_rom), m_plugin(&m_device) { - getController(); // init controller m_clockTempoParam = getController().getParameterIndexByName(Virus::g_paramClockTempo); - const auto latencyBlocks = getController().getConfig()->getIntValue("latencyBlocks", static_cast<int>(getPlugin().getLatencyBlocks())); + const auto latencyBlocks = getConfig().getIntValue("latencyBlocks", static_cast<int>(getPlugin().getLatencyBlocks())); setLatencyBlocks(latencyBlocks); } @@ -199,7 +206,7 @@ void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, if(status == synthLib::M_CONTROLCHANGE || status == synthLib::M_POLYPRESSURE) { // forward to UI to react to control input changes that should move knobs - getController().dispatchVirusOut(std::vector<synthLib::SMidiEvent>{ev}); + static_cast<Virus::Controller&>(getController()).addPluginMidiOut(std::vector{ev}); } } @@ -233,7 +240,7 @@ void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, if (!m_midiOut.empty()) { - getController().dispatchVirusOut(m_midiOut); + static_cast<Virus::Controller&>(getController()).addPluginMidiOut(m_midiOut); } for (auto& e : m_midiOut) @@ -271,190 +278,29 @@ bool AudioPluginAudioProcessor::hasEditor() const juce::AudioProcessorEditor* AudioPluginAudioProcessor::createEditor() { if(!m_editorState) - m_editorState.reset(new PluginEditorState(*this)); - return new AudioPluginAudioProcessorEditor (*this, *m_editorState); -} - -//============================================================================== -void AudioPluginAudioProcessor::getStateInformation (juce::MemoryBlock& destData) -{ - // You should use this method to store your parameters in the memory block. - // You could do that either as raw data, or use the XML or ValueTree classes - // as intermediaries to make it easy to save and load complex data. - - std::vector<uint8_t> state; - m_plugin.getState(state, synthLib::StateTypeGlobal); - destData.append(&state[0], state.size()); -} - -void AudioPluginAudioProcessor::setStateInformation (const void* data, int sizeInBytes) -{ - // You should use this method to restore your parameters from this memory block, - // whose contents will have been created by the getStateInformation() call. - setState(data, sizeInBytes); -} - -void AudioPluginAudioProcessor::getCurrentProgramStateInformation(juce::MemoryBlock& destData) -{ - std::vector<uint8_t> state; - m_plugin.getState(state, synthLib::StateTypeCurrentProgram); - destData.append(&state[0], state.size()); -} - -void AudioPluginAudioProcessor::setCurrentProgramStateInformation(const void* data, int sizeInBytes) -{ - setState(data, sizeInBytes); -} - -void AudioPluginAudioProcessor::getLastMidiOut(std::vector<synthLib::SMidiEvent>& dst) -{ - juce::ScopedLock lock(getCallbackLock()); - std::swap(dst, m_midiOut); - m_midiOut.clear(); -} - -void AudioPluginAudioProcessor::addMidiEvent(const synthLib::SMidiEvent& ev) -{ - m_plugin.addMidiEvent(ev); -} - -juce::MidiOutput *AudioPluginAudioProcessor::getMidiOutput() const { return m_midiOutput.get(); } -juce::MidiInput *AudioPluginAudioProcessor::getMidiInput() const { return m_midiInput.get(); } - -bool AudioPluginAudioProcessor::setMidiOutput(const juce::String& _out) -{ - if (m_midiOutput != nullptr && m_midiOutput->isBackgroundThreadRunning()) - { - m_midiOutput->stopBackgroundThread(); - } - m_midiOutput = juce::MidiOutput::openDevice(_out); - if (m_midiOutput != nullptr) - { - m_midiOutput->startBackgroundThread(); - return true; - } - return false; -} - -bool AudioPluginAudioProcessor::setMidiInput(const juce::String& _in) -{ - if (m_midiInput != nullptr) - { - m_midiInput->stop(); - } - m_midiInput = juce::MidiInput::openDevice(_in, this); - if (m_midiInput != nullptr) - { - m_midiInput->start(); - return true; - } - return false; -} - -void AudioPluginAudioProcessor::handleIncomingMidiMessage(juce::MidiInput *source, const juce::MidiMessage &message) -{ - const auto* raw = message.getSysExData(); - if (raw) - { - const auto count = message.getSysExDataSize(); - auto syx = Virus::SysEx(); - syx.push_back(0xf0); - for (int i = 0; i < count; i++) - { - syx.push_back(raw[i]); - } - syx.push_back(0xf7); - synthLib::SMidiEvent sm; - sm.source = synthLib::MidiEventSourcePlugin; - sm.sysex = syx; - getController().parseMessage(syx); - - addMidiEvent(sm); - - if (m_midiOutput) - { - std::vector<synthLib::SMidiEvent> data; - getLastMidiOut(data); - if (!data.empty()) - { - const auto msg = juce::MidiMessage::createSysExMessage(data.data(), static_cast<int>(data.size())); - - m_midiOutput->sendMessageNow(msg); - } - } - } - else - { - const auto count = message.getRawDataSize(); - const auto* rawData = message.getRawData(); - if (count >= 1 && count <= 3) - { - synthLib::SMidiEvent sm; - sm.source = synthLib::MidiEventSourcePlugin; - sm.a = rawData[0]; - sm.b = count > 1 ? rawData[1] : 0; - sm.c = count > 2 ? rawData[2] : 0; - addMidiEvent(sm); - } - else - { - synthLib::SMidiEvent sm; - sm.source = synthLib::MidiEventSourcePlugin; - auto syx = Virus::SysEx(); - for (int i = 0; i < count; i++) - { - syx.push_back(rawData[i]); - } - sm.sysex = syx; - addMidiEvent(sm); - } - } + m_editorState.reset(new PluginEditorState(*this, getController())); + return new jucePluginEditorLib::EditorWindow(*this, *m_editorState, getConfig()); } void AudioPluginAudioProcessor::updateLatencySamples() { if constexpr(JucePlugin_IsSynth) - setLatencySamples(m_plugin.getLatencyMidiToOutput()); + setLatencySamples(getPlugin().getLatencyMidiToOutput()); else - 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(); - } + setLatencySamples(getPlugin().getLatencyInputToOutput()); } -Virus::Controller &AudioPluginAudioProcessor::getController() +bool AudioPluginAudioProcessor::setLatencyBlocks(uint32_t _blocks) { - if (m_controller == nullptr) - { - // initialize controller if not exists. - // assures PluginProcessor is fully constructed! - m_controller = std::make_unique<Virus::Controller>(*this); - } - return *m_controller; + if(!Processor::setLatencyBlocks(_blocks)) + return false; + updateLatencySamples(); + return true; } -void AudioPluginAudioProcessor::setState(const void* _data, size_t _sizeInBytes) +pluginLib::Controller* AudioPluginAudioProcessor::createController() { - if(_sizeInBytes < 1) - return; - - std::vector<uint8_t> state; - state.resize(_sizeInBytes); - memcpy(&state[0], _data, _sizeInBytes); - m_plugin.setState(state); - if (m_controller) - m_controller->onStateLoaded(); + return new Virus::Controller(*this); } //============================================================================== diff --git a/source/jucePlugin/PluginProcessor.h b/source/jucePlugin/PluginProcessor.h @@ -1,17 +1,16 @@ #pragma once -#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" +#include "../jucePluginEditorLib/pluginProcessor.h" + class PluginEditorState; //============================================================================== -class AudioPluginAudioProcessor : public juce::AudioProcessor, juce::MidiInputCallback +class AudioPluginAudioProcessor : public jucePluginEditorLib::Processor { public: //============================================================================== @@ -25,6 +24,7 @@ public: bool isBusesLayoutSupported (const BusesLayout& layouts) const override; void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override; + using AudioProcessor::processBlock; //============================================================================== @@ -46,24 +46,8 @@ public: const juce::String getProgramName (int index) override; void changeProgramName (int index, const juce::String& newName) override; - //============================================================================== - void getStateInformation (juce::MemoryBlock& destData) override; - void setStateInformation (const void* data, int sizeInBytes) override; - void getCurrentProgramStateInformation (juce::MemoryBlock& destData) override; - void setCurrentProgramStateInformation (const void* data, int sizeInBytes) override; - // _____________ // - Virus::Controller &getController(); - bool isPluginValid() const { return m_plugin.isValid(); } - void getLastMidiOut(std::vector<synthLib::SMidiEvent>& dst); - void addMidiEvent(const synthLib::SMidiEvent& ev); - bool setMidiOutput(const juce::String& _out); - juce::MidiOutput* getMidiOutput() const; - bool setMidiInput(const juce::String& _in); - juce::MidiInput* getMidiInput() const; - void handleIncomingMidiMessage(juce::MidiInput *source, const juce::MidiMessage &message) override; - std::string getRomName() const { return juce::File(juce::String(m_romName)).getFileNameWithoutExtension().toStdString(); @@ -72,21 +56,20 @@ public: { return m_rom.getModel(); } - synthLib::Plugin& getPlugin() + + bool setLatencyBlocks(uint32_t _blocks) override; + // _____________ + // +private: + + synthLib::Plugin& getPlugin() override { return m_plugin; } - void updateLatencySamples(); - void setLatencyBlocks(uint32_t _blocks); + pluginLib::Controller* createController() override; - // _____________ - // -private: - std::unique_ptr<Virus::Controller> m_controller; - std::unique_ptr<juce::MidiOutput> m_midiOutput; - std::unique_ptr<juce::MidiInput> m_midiInput; - void setState(const void *_data, size_t _sizeInBytes); + void updateLatencySamples(); //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessor) @@ -95,7 +78,6 @@ private: virusLib::ROMFile m_rom; virusLib::Device m_device; 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 @@ -36,16 +36,17 @@ namespace Virus return g_midiPacketNames[static_cast<uint32_t>(_type)]; } - Controller::Controller(AudioPluginAudioProcessor &p, unsigned char deviceId) : pluginLib::Controller(loadParameterDescriptions()), m_processor(p), m_deviceId(deviceId) + Controller::Controller(AudioPluginAudioProcessor &p, unsigned char deviceId) : pluginLib::Controller(p, loadParameterDescriptions()), m_processor(p), m_deviceId(deviceId) { - registerParams(p); + switch(p.getModel()) + { + default: + case virusLib::ROMFile::Model::ABC: m_singles.resize(8); break; + case virusLib::ROMFile::Model::Snow: m_singles.resize(10); break; + case virusLib::ROMFile::Model::TI: m_singles.resize(26); break; + } - juce::PropertiesFile::Options opts; - opts.applicationName = "DSP56300 Emulator"; - opts.filenameSuffix = ".settings"; - opts.folderName = "DSP56300 Emulator"; - opts.osxLibrarySubFolder = "Application Support/DSP56300 Emulator"; - m_config = new juce::PropertiesFile(opts); + registerParams(p); // add lambda to enforce updating patches when virus switch from/to multi/single. const auto& params = findSynthParam(0, 0x72, 0x7a); @@ -65,7 +66,7 @@ namespace Virus requestTotal(); requestArrangement(); - for(uint8_t i=3; i<=8; ++i) + for(uint8_t i=3; i<=getBankCount(); ++i) requestSingleBank(i); startTimer(5); @@ -74,10 +75,9 @@ namespace Virus Controller::~Controller() { stopTimer(); - delete m_config; } - void Controller::parseMessage(const SysEx& _msg) + void Controller::parseSysexMessage(const pluginLib::SysEx& _msg) { std::string name; pluginLib::MidiPacket::Data data; @@ -130,7 +130,7 @@ namespace Virus const auto& partParams = findSynthParam(part, page, index); - if (partParams.empty() && part != 0) + if (partParams.empty() && part != 0 && part != virusLib::SINGLE) { // ensure it's not global const auto& globalParams = findSynthParam(0, page, index); @@ -213,8 +213,8 @@ namespace Virus return name; } - void Controller::setSinglePresetName(uint8_t _part, const juce::String& _name) - { + void Controller::setSinglePresetName(uint8_t _part, const juce::String& _name) const + { for (int i=0; i<kNameLength; i++) { const std::string paramName = "SingleName" + std::to_string(i); @@ -307,7 +307,7 @@ namespace Virus return m_currentPresetSource[_part]; } - bool Controller::parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::ParamValues& _parameterValues, const SysEx& _msg) const + bool Controller::parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::ParamValues& _parameterValues, const pluginLib::SysEx& _msg) const { const auto packetName = midiPacketName(MidiPacketType::SingleDump); @@ -318,7 +318,7 @@ namespace Virus if(_msg.size() > m->size()) { - SysEx temp; + pluginLib::SysEx temp; temp.insert(temp.begin(), _msg.begin(), _msg.begin() + (m->size()-1)); temp.push_back(0xf7); return parseMidiPacket(*m, _data, _parameterValues, temp); @@ -341,13 +341,13 @@ namespace Virus } uint32_t size; - const auto res = genericVirusUI::VirusEditor::findNamedResourceByFilename(name, size); + const auto res = genericVirusUI::VirusEditor::findEmbeddedResource(name, size); if(res) return {res, size}; return {}; } - void Controller::parseSingle(const SysEx& msg) + void Controller::parseSingle(const pluginLib::SysEx& msg) { pluginLib::MidiPacket::Data data; pluginLib::MidiPacket::ParamValues parameterValues; @@ -358,7 +358,7 @@ namespace Virus parseSingle(msg, data, parameterValues); } - void Controller::parseSingle(const SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues) + void Controller::parseSingle(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues) { SinglePatch patch; @@ -439,7 +439,7 @@ namespace Virus } } - void Controller::parseMulti(const SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues) + void Controller::parseMulti(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues) { const auto bankNumber = _data.find(pluginLib::MidiDataType::Bank)->second; @@ -470,7 +470,7 @@ namespace Virus } } - void Controller::parseControllerDump(synthLib::SMidiEvent &m) + void Controller::parseControllerDump(const synthLib::SMidiEvent& m) { const uint8_t status = m.a & 0xf0; const uint8_t part = m.a & 0x0f; @@ -484,13 +484,12 @@ namespace Virus else return; - 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, pluginLib::Parameter::ChangedBy::ControlChange); } - void Controller::printMessage(const SysEx &msg) + void Controller::printMessage(const pluginLib::SysEx &msg) { std::stringstream ss; ss << "[size " << msg.size() << "] "; @@ -504,15 +503,7 @@ namespace Virus LOG(s); } - void Controller::sendSysEx(const SysEx &msg) const - { - synthLib::SMidiEvent ev; - ev.sysex = msg; - ev.source = synthLib::MidiEventSourceEditor; - m_processor.addMidiEvent(ev); - } - - void Controller::onStateLoaded() const + void Controller::onStateLoaded() { requestTotal(); requestArrangement(); @@ -564,21 +555,16 @@ namespace Virus bool Controller::sendSysEx(MidiPacketType _type, std::map<pluginLib::MidiDataType, uint8_t>& _params) const { - std::vector<uint8_t> sysex; - _params.insert(std::make_pair(pluginLib::MidiDataType::DeviceId, m_deviceId)); - - if(!createMidiDataFromPacket(sysex, midiPacketName(_type), _params, 0)) - return false; - - sendSysEx(sysex); - return true; + return pluginLib::Controller::sendSysEx(midiPacketName(_type), _params); } void Controller::timerCallback() { - const juce::ScopedLock sl(m_eventQueueLock); - for (auto msg : m_virusOut) + std::vector<synthLib::SMidiEvent> virusOut; + getPluginMidiOut(virusOut); + + for (const auto& msg : virusOut) { if (msg.sysex.empty()) { @@ -587,17 +573,9 @@ namespace Virus } else { - parseMessage(msg.sysex); + parseSysexMessage(msg.sysex); } } - m_virusOut.clear(); - } - - void Controller::dispatchVirusOut(const std::vector<synthLib::SMidiEvent> &newData) - { - const juce::ScopedLock sl(m_eventQueueLock); - - m_virusOut.insert(m_virusOut.end(), newData.begin(), newData.end()); } void Controller::sendParameterChange(const pluginLib::Parameter& _parameter, uint8_t _value) @@ -652,7 +630,7 @@ namespace Virus for (const auto& it : _paramValues) { - const auto* p = getParameter(it.first.second); + const auto* p = getParameter(it.first.second, _program == virusLib::SINGLE ? 0 : _program); assert(p); if(!p) return {}; @@ -692,4 +670,11 @@ namespace Virus setCurrentPartPreset(_part, getCurrentPartBank(_part), getCurrentPartProgram(_part) + 1); } } + + std::string Controller::getBankName(uint32_t _index) const + { + char temp[32]{0}; + sprintf(temp, "Bank %c", 'A' + _index); + return temp; + } }; // namespace Virus diff --git a/source/jucePlugin/VirusController.h b/source/jucePlugin/VirusController.h @@ -11,7 +11,6 @@ class AudioPluginAudioProcessor; namespace Virus { - using SysEx = std::vector<uint8_t>; class Controller : public pluginLib::Controller, juce::Timer { public: @@ -29,7 +28,7 @@ namespace Virus struct MultiPatch : Patch {}; - using Singles = std::array<std::array<SinglePatch, 128>, 8>; + using Singles = std::vector<std::array<SinglePatch, 128>>; static constexpr auto kNameLength = 10; @@ -67,17 +66,15 @@ namespace Virus Controller(AudioPluginAudioProcessor &, unsigned char deviceId = 0x00); ~Controller() override; - // this is called by the plug-in on audio thread! - void dispatchVirusOut(const std::vector<synthLib::SMidiEvent> &); - std::vector<uint8_t> createSingleDump(uint8_t _part, uint8_t _bank, uint8_t _program); std::vector<uint8_t> createSingleDump(uint8_t _bank, uint8_t _program, const pluginLib::MidiPacket::ParamValues& _paramValues); std::vector<uint8_t> modifySingleDump(const std::vector<uint8_t>& _sysex, virusLib::BankNumber _newBank, uint8_t _newProgram, bool _modifyBank, bool _modifyProgram); void selectPrevPreset(uint8_t _part); void selectNextPreset(uint8_t _part); + std::string getBankName(uint32_t _index) const; - static void printMessage(const SysEx &); + static void printMessage(const pluginLib::SysEx &); juce::Value* getParamValue(uint8_t ch, uint8_t bank, uint8_t paramIndex); @@ -106,7 +103,7 @@ namespace Virus return m_multiEditBuffer; } - void setSinglePresetName(uint8_t _part, const juce::String& _name); + void setSinglePresetName(uint8_t _part, const juce::String& _name) const; bool isMultiMode() const; // part 0 - 15 (ignored when single! 0x40...) @@ -119,12 +116,8 @@ namespace Virus juce::String getCurrentPartPresetName(uint8_t _part) const; uint32_t getBankCount() const { return static_cast<uint32_t>(m_singles.size()); } - uint8_t getCurrentPart() const { return m_currentPart; } - void setCurrentPart(uint8_t _part) { m_currentPart = _part; } - void parseMessage(const SysEx &); - void sendSysEx(const SysEx &) const; - void onStateLoaded() const; - juce::PropertiesFile* getConfig() { return m_config; } + void parseSysexMessage(const pluginLib::SysEx &) override; + void onStateLoaded() override; std::function<void()> onProgramChange = {}; std::function<void()> onMsgDone = {}; @@ -140,12 +133,14 @@ namespace Virus void sendParameterChange(const pluginLib::Parameter& _parameter, uint8_t _value) override; bool sendParameterChange(uint8_t _page, uint8_t _part, uint8_t _index, uint8_t _value) const; + using pluginLib::Controller::sendSysEx; + bool sendSysEx(MidiPacketType _type) const; bool sendSysEx(MidiPacketType _type, std::map<pluginLib::MidiDataType, uint8_t>& _params) const; uint8_t getDeviceId() const { return m_deviceId; } - bool parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::ParamValues& _parameterValues, const SysEx& _msg) const; + bool parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::ParamValues& _parameterValues, const pluginLib::SysEx& _msg) const; private: static std::string loadParameterDescriptions(); @@ -158,22 +153,18 @@ namespace Virus MultiPatch m_multiEditBuffer; - void parseSingle(const SysEx& _msg); - void parseSingle(const SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues); + void parseSingle(const pluginLib::SysEx& _msg); + void parseSingle(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues); - void parseMulti(const SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues); + void parseMulti(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues); void parseParamChange(const pluginLib::MidiPacket::Data& _data); - void parseControllerDump(synthLib::SMidiEvent &); + void parseControllerDump(const synthLib::SMidiEvent&); AudioPluginAudioProcessor& m_processor; - juce::CriticalSection m_eventQueueLock; - std::vector<synthLib::SMidiEvent> m_virusOut; unsigned char m_deviceId; virusLib::BankNumber m_currentBank[16]{}; uint8_t m_currentProgram[16]{}; PresetSource m_currentPresetSource[16]{PresetSource::Unknown}; - uint8_t m_currentPart = 0; - juce::PropertiesFile *m_config; }; }; // namespace Virus diff --git a/source/jucePlugin/VirusParameterBinding.cpp b/source/jucePlugin/VirusParameterBinding.cpp @@ -1,248 +0,0 @@ -#include "VirusParameterBinding.h" - -#include "PluginProcessor.h" - -class Parameter; - -VirusParameterBinding::~VirusParameterBinding() -{ - clearBindings(); -} - -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_controller.getParameter(_param, _part == CurrentPart ? m_controller.getCurrentPart() : _part); - - if (!v) - { - assert(false && "Failed to find parameter"); - return; - } - - removeMouseListener(_slider); - - auto* listener = new VirusParameterBindingMouseListener(v, _slider); - m_sliderMouseListeners.insert(std::make_pair(&_slider, listener)); - - _slider.addMouseListener(listener, false); - const auto range = v->getNormalisableRange(); - _slider.setRange(range.start, range.end, range.interval); - _slider.setDoubleClickReturnValue(true, v->convertFrom0to1(v->getDefaultValue())); - _slider.getValueObject().referTo(v->getValueObject()); - _slider.getProperties().set("type", "slider"); - _slider.getProperties().set("name", juce::String(v->getDescription().name)); - if (v->isBipolar()) { - _slider.getProperties().set("bipolar", true); - } - const BoundParameter p{v, &_slider, _param, _part}; - addBinding(p); -} - -void VirusParameterBinding::bind(juce::ComboBox& _combo, uint32_t _param) -{ - bind(_combo, _param, CurrentPart); -} - -void VirusParameterBinding::bind(juce::ComboBox& _combo, const uint32_t _param, uint8_t _part) -{ - const auto v = m_controller.getParameter(_param, _part == CurrentPart ? m_controller.getCurrentPart() : _part); - if (!v) - { - assert(false && "Failed to find parameter"); - return; - } - _combo.setTextWhenNothingSelected("-"); - _combo.setScrollWheelEnabled(true); - - _combo.onChange = nullptr; - _combo.clear(); - - int idx = 1; - uint32_t count = 0; - for (const auto& vs : v->getAllValueStrings()) { - if(vs.isNotEmpty()) - { - _combo.addItem(vs, idx); - if(++count == 16) - { - _combo.getRootMenu()->addColumnBreak(); - count = 0; - } - } - idx++; - } - //_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>(id - 1))); - v->endChangeGesture(); - } - v->getValueObject().setValue(id - 1); - }; - - const auto listenerId = m_nextListenerId++; - - v->onValueChanged.emplace_back(std::make_pair(listenerId, [this, &_combo, v]() - { - const auto value = static_cast<int>(v->getValueObject().getValueSource().getValue()); - _combo.setSelectedId(value + 1, juce::dontSendNotification); - })); - - const BoundParameter p{v, &_combo, _param, _part, listenerId}; - addBinding(p); -} - -void VirusParameterBinding::bind(juce::Button &_btn, const uint32_t _param) -{ - const auto v = m_controller.getParameter(_param, m_controller.getCurrentPart()); - if (!v) - { - assert(false && "Failed to find parameter"); - return; - } - _btn.getToggleStateValue().referTo(v->getValueObject()); - const BoundParameter p{v, &_btn, _param, CurrentPart}; - 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) -{ - auto it = m_sliderMouseListeners.find(&_slider); - - if(it != m_sliderMouseListeners.end()) - { - _slider.removeMouseListener(it->second); - delete it->second; - 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 @@ -1,79 +0,0 @@ -#pragma once - -#include "../jucePluginLib/parameter.h" - -namespace Virus -{ - class Controller; -} - -namespace juce -{ - class Value; -} - -class AudioPluginAudioProcessor; -class Parameter; -class VirusParameterBindingMouseListener : public juce::MouseListener -{ -public: - VirusParameterBindingMouseListener(pluginLib::Parameter* _param, juce::Slider &_slider) : m_param(_param), m_slider(&_slider) { - } - pluginLib::Parameter *m_param; - juce::Slider* m_slider; - void mouseDown(const juce::MouseEvent &event) override { m_param->beginChangeGesture(); } - void mouseUp(const juce::MouseEvent &event) override { m_param->endChangeGesture(); } - void mouseDrag(const juce::MouseEvent &event) override { m_param->setValueNotifyingHost(m_param->convertTo0to1(static_cast<float>(m_slider->getValue()))); } -}; - -class VirusParameterBinding final : juce::MouseListener -{ -public: - 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 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); - - void addBinding(const BoundParameter& _boundParameter); - void disableBinding(const BoundParameter& _b); - - Virus::Controller& m_controller; - - 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 = 100000; -}; diff --git a/source/jucePlugin/parameterDescriptions_C.json b/source/jucePlugin/parameterDescriptions_C.json @@ -454,13 +454,13 @@ ], "oscWave": [ - "Sine", "Triangle", "Wave 2", "Wave 3", "Wave 4", "Wave 5", "Wave 6", "Wave 7", "Wave 8", "Wave 9", "Wave 10", "Wave 11", "Wave 12", "Wave 13", "Wave 14", "Wave 15", "Wave 16", "Wave 17", "Wave 18", "Wave 19", + "Sine", "Triangle", "Wave 3", "Wave 4", "Wave 5", "Wave 6", "Wave 7", "Wave 8", "Wave 9", "Wave 10", "Wave 11", "Wave 12", "Wave 13", "Wave 14", "Wave 15", "Wave 16", "Wave 17", "Wave 18", "Wave 19", "Wave 20", "Wave 21", "Wave 22", "Wave 23", "Wave 24", "Wave 25", "Wave 26", "Wave 27", "Wave 28", "Wave 29", "Wave 30", "Wave 31", "Wave 32", "Wave 33", "Wave 34", "Wave 35", "Wave 36", "Wave 37", "Wave 38", "Wave 39", "Wave 40", "Wave 41", "Wave 42", "Wave 43", "Wave 44", "Wave 45", "Wave 46", "Wave 47", "Wave 48", "Wave 49", "Wave 50", "Wave 51", "Wave 52", "Wave 53", "Wave 54", "Wave 55", "Wave 56", "Wave 57", "Wave 58", "Wave 59", "Wave 60", "Wave 61", "Wave 62", "Wave 63", "Wave 64", "Wave 65", "Wave 66", "Wave 67", "Wave 68", "Wave 69", "Wave 70", "Wave 71", "Wave 72", "Wave 73", "Wave 74", "Wave 75", "Wave 76", "Wave 77", "Wave 78", "Wave 79", "Wave 80", "Wave 81", "Wave 82", "Wave 83", "Wave 84", "Wave 85", "Wave 86", "Wave 87", "Wave 88", "Wave 89", "Wave 90", "Wave 91", "Wave 92", "Wave 93", "Wave 94", "Wave 95", "Wave 96", "Wave 97", "Wave 98", "Wave 99", "Wave 100", "Wave 101", "Wave 102", "Wave 103", "Wave 104", "Wave 105", "Wave 106", "Wave 107", "Wave 108", "Wave 109", "Wave 110", "Wave 111", "Wave 112", "Wave 113", "Wave 114", "Wave 115", "Wave 116", "Wave 117", "Wave 118", "Wave 119", - "Wave 120", "Wave 121", "Wave 122", "Wave 123", "Wave 124", "Wave 125", "Wave 126", "Wave 127" + "Wave 120", "Wave 121", "Wave 122", "Wave 123", "Wave 124", "Wave 125", "Wave 126", "Wave 127", "Wave 128" ], "satCurve": [ @@ -702,7 +702,7 @@ "ascii": [ "NUL","SOH","STX","ETX","EOT","ENQ","ACK","BEL","BS","HT","LF","VT","FF","CR","SO","SI","DLE","DC1","DC2","DC3","DC4","NAK","SYN","ETB","CAN","EM","SUB","ESC","FS","GS","RS","US", - ", ","!",'"',"#","$","%","&","'","(",")","*","+",",","-",".","/", + ", ","!","\"","#","$","%","&","'","(",")","*","+",",","-",".","/", "0","1","2","3","4","5","6","7","8","9", ":",";","<","=",">","?","@", "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z", diff --git a/source/jucePlugin/ui3/FxPage.cpp b/source/jucePlugin/ui3/FxPage.cpp @@ -10,7 +10,7 @@ namespace genericVirusUI FxPage::FxPage(const VirusEditor& _editor) { const auto delayReverbMode = _editor.getController().getParameterIndexByName(Virus::g_paramDelayReverbMode); - const auto p = _editor.getController().getParamValueObject(delayReverbMode); + const auto p = _editor.getController().getParamValueObject(delayReverbMode, 0); if(!p) return; @@ -18,8 +18,8 @@ namespace genericVirusUI const auto containerReverb = _editor.findComponent("ContainerReverb"); const auto containerDelay = _editor.findComponent("ContainerDelay"); - m_conditionReverb.reset(new genericUI::Condition(*containerReverb, *p, {2,3,4})); - m_conditionDelay.reset(new genericUI::Condition(*containerDelay, *p, {0,1,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26})); + m_conditionReverb.reset(new genericUI::Condition(*containerReverb, p, 0, {2,3,4})); + m_conditionDelay.reset(new genericUI::Condition(*containerDelay, p, 0, {0,1,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26})); } FxPage::~FxPage() diff --git a/source/jucePlugin/ui3/MidiPorts.cpp b/source/jucePlugin/ui3/MidiPorts.cpp @@ -1,148 +0,0 @@ -#include "MidiPorts.h" - -#include "VirusEditor.h" - -#include "../VirusController.h" -#include "../PluginProcessor.h" - -namespace genericVirusUI -{ - MidiPorts::MidiPorts(VirusEditor& _editor) : m_editor(_editor) - { - auto& processor = _editor.getProcessor(); - - { - const auto properties = _editor.getController().getConfig(); - - const auto midiIn = properties->getValue("midi_input", ""); - const auto midiOut = properties->getValue("midi_output", ""); - - if (!midiIn.isEmpty()) - processor.setMidiInput(midiIn); - - if (!midiOut.isEmpty()) - processor.setMidiOutput(midiOut); - } - - m_midiIn = _editor.findComponentT<juce::ComboBox>("MidiIn"); - m_midiOut = _editor.findComponentT<juce::ComboBox>("MidiOut"); - - m_midiIn->setTextWhenNoChoicesAvailable("-"); - - const auto midiInputs = juce::MidiInput::getAvailableDevices(); - - int inIndex = 0; - - m_midiIn->addItem("<none>", 1); - - for (int i = 0; i < midiInputs.size(); i++) - { - const auto input = midiInputs[i]; - - if (processor.getMidiInput() != nullptr && input.identifier == processor.getMidiInput()->getIdentifier()) - inIndex = i + 1; - - m_midiIn->addItem(input.name, i+2); - } - - m_midiIn->setSelectedItemIndex(inIndex, juce::dontSendNotification); - - m_midiIn->onChange = [this]() { updateMidiInput(m_midiIn->getSelectedItemIndex()); }; - - m_midiOut->setTextWhenNoChoicesAvailable("-"); - - const auto midiOutputs = juce::MidiOutput::getAvailableDevices(); - - auto outIndex = 0; - - m_midiOut->addItem("<none>", 1); - - for (int i = 0; i < midiOutputs.size(); i++) - { - const auto output = midiOutputs[i]; - if (processor.getMidiOutput() != nullptr && - output.identifier == processor.getMidiOutput()->getIdentifier()) - { - outIndex = i + 1; - } - m_midiOut->addItem(output.name, i+2); - } - - m_midiOut->setSelectedItemIndex(outIndex, juce::dontSendNotification); - - m_midiOut->onChange = [this]() { updateMidiOutput(m_midiOut->getSelectedItemIndex()); }; - - deviceManager = new juce::AudioDeviceManager(); - } - - MidiPorts::~MidiPorts() - { - delete deviceManager; - } - - void MidiPorts::updateMidiInput(int index) - { - const auto list = juce::MidiInput::getAvailableDevices(); - - const auto properties = m_editor.getController().getConfig(); - - if (index <= 0) - { - properties->setValue("midi_input", ""); - properties->save(); - m_lastInputIndex = 0; - m_midiIn->setSelectedItemIndex(index, juce::dontSendNotification); - return; - } - - index--; - - const auto newInput = list[index]; - - if (!deviceManager->isMidiInputDeviceEnabled(newInput.identifier)) - deviceManager->setMidiInputDeviceEnabled(newInput.identifier, true); - - if (!m_editor.getProcessor().setMidiInput(newInput.identifier)) - { - m_midiIn->setSelectedItemIndex(0, juce::dontSendNotification); - m_lastInputIndex = 0; - return; - } - - properties->setValue("midi_input", newInput.identifier); - properties->save(); - - m_midiIn->setSelectedItemIndex(index + 1, juce::dontSendNotification); - m_lastInputIndex = index; - } - - void MidiPorts::updateMidiOutput(int index) - { - const auto list = juce::MidiOutput::getAvailableDevices(); - - const auto properties = m_editor.getController().getConfig(); - - if (index == 0) - { - properties->setValue("midi_output", ""); - properties->save(); - m_midiOut->setSelectedItemIndex(index, juce::dontSendNotification); - m_lastOutputIndex = index; - m_editor.getProcessor().setMidiOutput(""); - return; - } - index--; - const auto newOutput = list[index]; - if (!m_editor.getProcessor().setMidiOutput(newOutput.identifier)) - { - m_midiOut->setSelectedItemIndex(0, juce::dontSendNotification); - m_lastOutputIndex = 0; - return; - } - properties->setValue("midi_output", newOutput.identifier); - properties->save(); - - m_midiOut->setSelectedItemIndex(index + 1, juce::dontSendNotification); - m_lastOutputIndex = index; - } -} diff --git a/source/jucePlugin/ui3/MidiPorts.h b/source/jucePlugin/ui3/MidiPorts.h @@ -1,33 +0,0 @@ -#pragma once - -namespace juce -{ - class AudioDeviceManager; - class ComboBox; -} - -namespace genericVirusUI -{ - class VirusEditor; - - class MidiPorts - { - public: - explicit MidiPorts(VirusEditor& _editor); - ~MidiPorts(); - - private: - VirusEditor& m_editor; - - juce::ComboBox* m_midiIn = nullptr; - juce::ComboBox* m_midiOut = nullptr; - - juce::AudioDeviceManager* deviceManager = nullptr; - int m_lastInputIndex = 0; - int m_lastOutputIndex = 0; - - void updateMidiInput(int _index); - void updateMidiOutput(int _index); - - }; -} diff --git a/source/jucePlugin/ui3/Parts.cpp b/source/jucePlugin/ui3/Parts.cpp @@ -3,10 +3,9 @@ #include "VirusEditor.h" #include "../VirusController.h" -#include "../VirusParameterBinding.h" #include "../ParameterNames.h" -#include "dsp56kEmu/logging.h" +#include "../../jucePluginLib/parameterbinding.h" namespace genericVirusUI { @@ -109,9 +108,7 @@ namespace genericVirusUI m_editor.getController().setCurrentPartPreset(pt, bank, j); }); } - std::stringstream bankName; - bankName << "Bank " << static_cast<char>('A' + b); - selector.addSubMenu(std::string(bankName.str()), p); + selector.addSubMenu(m_editor.getController().getBankName(b), p); } selector.showMenuAsync(juce::PopupMenu::Options()); } diff --git a/source/jucePlugin/ui3/PatchBrowser.cpp b/source/jucePlugin/ui3/PatchBrowser.cpp @@ -1,86 +1,46 @@ #include "PatchBrowser.h" #include "VirusEditor.h" +#include "../PluginProcessor.h" #include "../../virusLib/microcontrollerTypes.h" #include "../../virusLib/microcontroller.h" #include "../VirusController.h" -#include "juce_cryptography/hashing/juce_MD5.h" #include "../../synthLib/midiToSysex.h" using namespace juce; -const juce::Array<juce::String> ModelList = {"A","B","C","TI"}; - namespace genericVirusUI { - virusLib::PresetVersion guessVersion(const uint8_t v) - { - return virusLib::Microcontroller::getPresetVersion(v); - } - - static PatchBrowser* s_lastPatchBrowser = nullptr; + enum Columns + { + INDEX = 1, + NAME = 2, + CAT1 = 3, + CAT2 = 4, + ARP = 5, + UNI = 6, + ST = 7, + VER = 8, + }; - PatchBrowser::PatchBrowser(const VirusEditor& _editor) : m_editor(_editor), m_controller(_editor.getController()), - m_fileFilter("*.syx;*.mid;*.midi", "*", "Virus Patch Dumps"), - m_bankList(FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, File::getSpecialLocation(File::SpecialLocationType::currentApplicationFile), &m_fileFilter, nullptr), - m_search("Search Box"), - m_patchList("Patch Browser"), - m_properties(m_controller.getConfig()) + constexpr std::initializer_list<jucePluginEditorLib::PatchBrowser::ColumnDefinition> g_columns = { - const auto bankDir = m_properties->getValue("virus_bank_dir", ""); - - if (bankDir.isNotEmpty() && File(bankDir).isDirectory()) - { - m_bankList.setRoot(bankDir); - - s_lastPatchBrowser = this; - auto callbackPathBrowser = this; - - juce::Timer::callAfterDelay(1000, [&, bankDir, callbackPathBrowser]() - { - if(s_lastPatchBrowser != callbackPathBrowser) - return; - - const auto lastFile = m_properties->getValue("virus_selected_file", ""); - const auto child = File(bankDir).getChildFile(lastFile); - if(child.existsAsFile()) - { - m_bankList.setFileName(child.getFileName()); - m_sendOnSelect = false; - onFileSelected(child); - m_sendOnSelect = true; - } - }); - } - - m_patchList.getHeader().addColumn("#", Columns::INDEX, 32); - m_patchList.getHeader().addColumn("Name", Columns::NAME, 130); - m_patchList.getHeader().addColumn("Category1", Columns::CAT1, 84); - m_patchList.getHeader().addColumn("Category2", Columns::CAT2, 84); - m_patchList.getHeader().addColumn("Arp", Columns::ARP, 32); - m_patchList.getHeader().addColumn("Uni", Columns::UNI, 32); - m_patchList.getHeader().addColumn("ST+-", Columns::ST, 32); - m_patchList.getHeader().addColumn("Ver", Columns::VER, 32); - - fitInParent(m_bankList, "ContainerFileSelector"); - fitInParent(m_patchList, "ContainerPatchList"); - - m_search.setColour(TextEditor::textColourId, Colours::white); - m_search.onTextChange = [this] - { - refreshPatchList(); - }; - m_search.setTextToShowWhenEmpty("Search...", Colours::grey); - - fitInParent(m_search, "ContainerPatchListSearchBox"); - - m_bankList.addListener(this); - m_patchList.setModel(this); + {"#", INDEX, 32}, + {"Name", NAME, 130}, + {"Category1", CAT1, 84}, + {"Category2", CAT2, 84}, + {"Arp", ARP, 32}, + {"Uni", UNI, 32}, + {"ST+-", ST, 32}, + {"Ver", VER, 32} + }; - m_romBankSelect = _editor.findComponentT<juce::ComboBox>("RomBankSelect", false); + PatchBrowser::PatchBrowser(const VirusEditor& _editor) : jucePluginEditorLib::PatchBrowser(_editor, _editor.getController(), _editor.getProcessor().getConfig(), g_columns) + { + const auto& c = _editor.getController(); if(m_romBankSelect) { @@ -88,11 +48,9 @@ namespace genericVirusUI m_romBankSelect->addItem("-", 1); - for(uint32_t i=0; i<m_controller.getBankCount(); ++i) + for(uint32_t i=0; i<c.getBankCount(); ++i) { - std::stringstream ss; - ss << "Bank " << static_cast<char>('A' + i); - m_romBankSelect->addItem(ss.str(), ++id); + m_romBankSelect->addItem(c.getBankName(i), ++id); } m_romBankSelect->onChange = [this] @@ -104,283 +62,9 @@ namespace genericVirusUI } } - PatchBrowser::~PatchBrowser() - { - if(s_lastPatchBrowser == this) - s_lastPatchBrowser = nullptr; - } - - void PatchBrowser::fitInParent(juce::Component& _component, const std::string& _parentName) const - { - auto* parent = m_editor.findComponent(_parentName); - - _component.setTransform(juce::AffineTransform::scale(2.0f)); - - const auto& bounds = parent->getBounds(); - const auto w = bounds.getWidth() >> 1; - const auto h = bounds.getHeight() >> 1; - - _component.setBounds(0,0, w,h); - - parent->addAndMakeVisible(_component); - } - - void PatchBrowser::selectionChanged() {} - - uint32_t PatchBrowser::load(const Virus::Controller& _controller, std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<std::vector<uint8_t>>& _packets) - { - uint32_t count = 0; - for (const auto& packet : _packets) - { - if (load(_controller, _result, _dedupeChecksums, packet)) - ++count; - } - return count; - } - - bool PatchBrowser::load(const Virus::Controller& _controller, std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<uint8_t>& _data) - { - if (_data.size() < 267) - return false; - - Patch patch; - patch.sysex = _data; - if(!initializePatch(_controller, patch)) - return false; - - patch.progNumber = static_cast<int>(_result.size()); - - if (!_dedupeChecksums) - { - _result.push_back(patch); - } - else - { - const auto md5 = std::string(MD5(&_data.front() + 9 + 17, 256 - 17 - 3).toHexString().toRawUTF8()); - - if (_dedupeChecksums->find(md5) == _dedupeChecksums->end()) - { - _dedupeChecksums->insert(md5); - _result.push_back(patch); - } - } - - return true; - } - - uint32_t PatchBrowser::loadBankFile(const Virus::Controller& _controller, std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const File& file) - { - const auto ext = file.getFileExtension().toLowerCase(); - const auto path = file.getParentDirectory().getFullPathName(); - - if (ext == ".syx") - { - MemoryBlock data; - - if (!file.loadFileAsData(data)) - return 0; - - std::vector<uint8_t> d; - d.resize(data.getSize()); - memcpy(&d[0], data.getData(), data.getSize()); - - std::vector<std::vector<uint8_t>> packets; - synthLib::MidiToSysex::splitMultipleSysex(packets, d); - - return load(_controller, _result, _dedupeChecksums, packets); - } - - if (ext == ".mid" || ext == ".midi") - { - std::vector<uint8_t> data; - - synthLib::MidiToSysex::readFile(data, file.getFullPathName().getCharPointer()); - - if (data.empty()) - return 0; - - std::vector<std::vector<uint8_t>> packets; - synthLib::MidiToSysex::splitMultipleSysex(packets, data); - - return load(_controller, _result, _dedupeChecksums, packets); - } - - return 0; - } - - bool PatchBrowser::selectPrevPreset() - { - return selectPrevNextPreset(-1); - } - - bool PatchBrowser::selectNextPreset() - { - return selectPrevNextPreset(1); - } - - void PatchBrowser::fileClicked(const File& file, const MouseEvent& e) - { - const auto ext = file.getFileExtension().toLowerCase(); - const auto path = file.getParentDirectory().getFullPathName(); - if (file.isDirectory() && e.mods.isPopupMenu()) - { - auto p = PopupMenu(); - p.addItem("Add directory contents to patch list", [this, file]() - { - m_patches.clear(); - m_checksums.clear(); - std::set<std::string> dedupeChecksums; - - std::vector<Patch> patches; - - for (const auto& f : RangedDirectoryIterator(file, false, "*.syx;*.mid;*.midi", File::findFiles)) - loadBankFile(m_controller, patches, &dedupeChecksums, f.getFile()); - - fillPatchList(patches); - }); - p.showMenuAsync(PopupMenu::Options()); - - return; - } - m_properties->setValue("virus_bank_dir", path); - onFileSelected(file); - } - - int PatchBrowser::getNumRows() { return m_filteredPatches.size(); } - - void PatchBrowser::paintRowBackground(Graphics& g, int rowNumber, int width, int height, bool rowIsSelected) - { - const auto alternateColour = m_patchList.getLookAndFeel() - .findColour(ListBox::backgroundColourId) - .interpolatedWith(m_patchList.getLookAndFeel().findColour(ListBox::textColourId), 0.03f); - if (rowIsSelected) - g.fillAll(Colours::lightblue); - else if (rowNumber & 1) - g.fillAll(alternateColour); - } - - void PatchBrowser::paintCell(Graphics& g, int rowNumber, int columnId, int width, int height, bool rowIsSelected) { - g.setColour(rowIsSelected ? Colours::darkblue - : m_patchList.getLookAndFeel().findColour(ListBox::textColourId)); // [5] - - if (rowNumber >= getNumRows()) - return; // Juce what are you up to? - - const auto rowElement = m_filteredPatches[rowNumber]; - //auto text = rowElement.name; - String text = ""; - if (columnId == Columns::INDEX) - text = String(rowElement.progNumber); - else if (columnId == Columns::NAME) - text = rowElement.name; - else if (columnId == Columns::CAT1) - text = rowElement.category1; - else if (columnId == Columns::CAT2) - text = rowElement.category2; - else if (columnId == Columns::ARP) - text = rowElement.arpMode != 0 ? "Y" : " "; - else if (columnId == Columns::UNI) - text = rowElement.unison == 0 ? " " : String(rowElement.unison + 1); - else if (columnId == Columns::ST) - text = rowElement.transpose != 64 ? String(rowElement.transpose - 64) : " "; - else if (columnId == Columns::VER) - { - switch (rowElement.model) - { - case virusLib::A: text = "A"; break; - case virusLib::B: text = "B"; break; - case virusLib::C: text = "C"; break; - case virusLib::D: text = "TI"; break; - case virusLib::D2: text = "TI2"; break; - default: text = "?"; break; - } - } - g.drawText(text, 2, 0, width - 4, height, Justification::centredLeft, true); // [6] - g.setColour(m_patchList.getLookAndFeel().findColour(ListBox::backgroundColourId)); - g.fillRect(width - 1, 0, 1, height); // [7] - } - - void PatchBrowser::selectedRowsChanged(int lastRowSelected) - { - if(!m_sendOnSelect) - return; - - const auto idx = m_patchList.getSelectedRow(); - - if (idx == -1) - return; - - // re-pack single, force to edit buffer - const auto program = m_controller.isMultiMode() ? m_controller.getCurrentPart() : static_cast<uint8_t>(virusLib::ProgramType::SINGLE); - - const auto& patch = m_filteredPatches[idx]; - - const auto msg = m_controller.modifySingleDump(patch.sysex, virusLib::BankNumber::EditBuffer, program, true, true); - - if(msg.empty()) - return; - - m_controller.sendSysEx(msg); - m_controller.requestSingle(0x0, program); - - m_controller.setCurrentPartPresetSource(m_controller.getCurrentPart(), Virus::Controller::PresetSource::Browser); - - m_properties->setValue("virus_selected_patch", patch.name); - } - - void PatchBrowser::cellDoubleClicked(int rowNumber, int columnId, const MouseEvent&) - { - if (rowNumber == m_patchList.getSelectedRow()) - selectedRowsChanged(0); - } - - class PatchBrowser::PatchBrowserSorter - { - public: - PatchBrowserSorter(const int attributeToSortBy, const bool forwards) - : m_attributeToSort(attributeToSortBy), - m_direction(forwards ? 1 : -1) - {} - - int compareElements(const Patch& first, const Patch& second) const - { - if (m_attributeToSort == Columns::INDEX) - return m_direction * (first.progNumber - second.progNumber); - if (m_attributeToSort == Columns::NAME) - return m_direction * first.name.compareIgnoreCase(second.name); - if (m_attributeToSort == Columns::CAT1) - return m_direction * first.category1.compare(second.category1); - if (m_attributeToSort == Columns::CAT2) - return m_direction * first.category2.compare(second.category2); - if (m_attributeToSort == Columns::ARP) - return m_direction * (first.arpMode - second.arpMode); - if (m_attributeToSort == Columns::UNI) - return m_direction * (first.unison - second.unison); - if (m_attributeToSort == Columns::VER) - return m_direction * (first.model - second.model); - if (m_attributeToSort == Columns::ST) - return m_direction * (first.transpose - second.transpose); - return m_direction * (first.progNumber - second.progNumber); - } - - private: - const int m_attributeToSort; - const int m_direction; - }; - - void PatchBrowser::sortOrderChanged(int newSortColumnId, bool isForwards) - { - if (newSortColumnId != 0) - { - PatchBrowserSorter sorter(newSortColumnId, isForwards); - m_filteredPatches.sort(sorter); - m_patchList.updateContent(); - } - } - void PatchBrowser::loadRomBank(const uint32_t _bankIndex) { - const auto& singles = m_controller.getSinglePresets(); + const auto& singles = static_cast<Virus::Controller&>(m_controller).getSinglePresets(); if(_bankIndex >= singles.size()) return; @@ -389,117 +73,60 @@ namespace genericVirusUI const auto searchValue = m_search.getText(); - std::vector<Patch> patches; + PatchList patches; for(size_t s=0; s<bank.size(); ++s) { - Patch patch; + auto* patch = createPatch(); - patch.sysex = bank[s].data; + patch->sysex = bank[s].data; + patch->progNumber = static_cast<int>(s); - if(!initializePatch(m_controller, patch)) + if(!initializePatch(*patch)) continue; - patch.progNumber = static_cast<int>(s); - - patches.push_back(patch); + patches.push_back(std::shared_ptr<jucePluginEditorLib::Patch>(patch)); } fillPatchList(patches); } - void PatchBrowser::onFileSelected(const juce::File& file) - { - const auto ext = file.getFileExtension().toLowerCase(); - if (file.existsAsFile() && ext == ".syx" || ext == ".midi" || ext == ".mid") - { - m_properties->setValue("virus_selected_file", file.getFileName()); - - std::vector<Patch> patches; - loadBankFile(m_controller, patches, nullptr, file); - - fillPatchList(patches); - } - - if(m_romBankSelect) - m_romBankSelect->setSelectedItemIndex(0); - } - - void PatchBrowser::fillPatchList(const std::vector<Patch>& _patches) - { - m_patches.clear(); - - for (const auto& patch : _patches) - m_patches.add(patch); - - refreshPatchList(); - } - - void PatchBrowser::refreshPatchList() - { - const auto searchValue = m_search.getText(); - const auto selectedPatchName = m_properties->getValue("virus_selected_patch", ""); - - m_filteredPatches.clear(); - int i=0; - int selectIndex = -1; - - for (const auto& patch : m_patches) - { - if (searchValue.isEmpty() || patch.name.containsIgnoreCase(searchValue)) - { - m_filteredPatches.add(patch); - - if(patch.name == selectedPatchName) - selectIndex = i; - - ++i; - } - } - m_patchList.updateContent(); - m_patchList.deselectAllRows(); - m_patchList.repaint(); - - if(selectIndex != -1) - m_patchList.selectRow(selectIndex); - } - bool PatchBrowser::selectPrevNextPreset(int _dir) { const auto part = m_controller.getCurrentPart(); - if(m_controller.getCurrentPartPresetSource(part) == Virus::Controller::PresetSource::Rom) + const auto& c = static_cast<const Virus::Controller&>(m_controller); + + if(c.getCurrentPartPresetSource(part) == Virus::Controller::PresetSource::Rom) return false; - if(m_filteredPatches.isEmpty()) + if(m_filteredPatches.empty()) return false; const auto idx = m_patchList.getSelectedRow(); if(idx < 0) return false; - const auto name = m_controller.getCurrentPartPresetName(part); + const auto name = c.getCurrentPartPresetName(part); - if(m_filteredPatches[idx].name != name) + if(m_filteredPatches[idx]->name != name) return false; - const auto newIdx = idx + _dir; - - if(newIdx < 0 || newIdx >= m_filteredPatches.size()) - return false; - - m_patchList.selectRow(newIdx); - return true; + return jucePluginEditorLib::PatchBrowser::selectPrevNextPreset(_dir); } - bool PatchBrowser::initializePatch(const Virus::Controller& _controller, Patch& _patch) + bool PatchBrowser::initializePatch(jucePluginEditorLib::Patch& _patch) { - const auto& c = _controller; + if (_patch.sysex.size() < 267) + return false; + + const auto& c = static_cast<const Virus::Controller&>(m_controller); + auto& patch = static_cast<Patch&>(_patch); pluginLib::MidiPacket::Data data; pluginLib::MidiPacket::ParamValues parameterValues; - if(!c.parseSingle(data, parameterValues, _patch.sysex)) + if(!c.parseSingle(data, parameterValues, patch.sysex)) return false; const auto idxVersion = c.getParameterIndexByName("Version"); @@ -509,11 +136,11 @@ namespace genericVirusUI const auto idxTranspose = c.getParameterIndexByName("Transpose"); const auto idxArpMode = c.getParameterIndexByName("Arp Mode"); - _patch.name = c.getSinglePresetName(parameterValues); - _patch.model = guessVersion(parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxVersion))->second); - _patch.unison = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxUnison))->second; - _patch.transpose = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxTranspose))->second; - _patch.arpMode = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxArpMode))->second; + patch.name = c.getSinglePresetName(parameterValues); + patch.model = virusLib::Microcontroller::getPresetVersion(parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxVersion))->second); + patch.unison = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxUnison))->second; + patch.transpose = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxTranspose))->second; + patch.arpMode = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxArpMode))->second; const auto category1 = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxCategory1))->second; const auto category2 = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxCategory2))->second; @@ -521,10 +148,82 @@ namespace genericVirusUI const auto* paramCategory1 = c.getParameter(idxCategory1, 0); const auto* paramCategory2 = c.getParameter(idxCategory2, 0); - _patch.category1 = paramCategory1->getDescription().valueList.valueToText(category1); - _patch.category2 = paramCategory2->getDescription().valueList.valueToText(category2); + patch.category1 = paramCategory1->getDescription().valueList.valueToText(category1); + patch.category2 = paramCategory2->getDescription().valueList.valueToText(category2); return true; } + MD5 PatchBrowser::getChecksum(jucePluginEditorLib::Patch& _patch) + { + return {&_patch.sysex.front() + 9 + 17, 256 - 17 - 3}; + } + + bool PatchBrowser::activatePatch(jucePluginEditorLib::Patch& _patch) + { + auto& c = static_cast<Virus::Controller&>(m_controller); + + // re-pack single, force to edit buffer + const auto program = c.isMultiMode() ? c.getCurrentPart() : static_cast<uint8_t>(virusLib::ProgramType::SINGLE); + + const auto msg = c.modifySingleDump(_patch.sysex, virusLib::BankNumber::EditBuffer, program, true, true); + + if(msg.empty()) + return false; + + c.sendSysEx(msg); + c.requestSingle(0x0, program); + + c.setCurrentPartPresetSource(m_controller.getCurrentPart(), Virus::Controller::PresetSource::Browser); + + return true; + } + + int PatchBrowser::comparePatches(const int _columnId, const jucePluginEditorLib::Patch& _a, const jucePluginEditorLib::Patch& _b) const + { + const auto& a = static_cast<const Patch&>(_a); + const auto& b = static_cast<const Patch&>(_b); + + switch(_columnId) + { + case INDEX: return a.progNumber - b.progNumber; + case NAME: return String(a.name).compareIgnoreCase(b.name); + case CAT1: return a.category1.compare(b.category1); + case CAT2: return a.category2.compare(b.category2); + case ARP: return a.arpMode - b.arpMode; + case UNI: return a.unison - b.unison; + case VER: return a.model - b.model; + case ST: return a.transpose - b.transpose; + default: return a.progNumber - b.progNumber; + } + } + + std::string PatchBrowser::getCellText(const jucePluginEditorLib::Patch& _patch, int columnId) + { + auto& rowElement = static_cast<const Patch&>(_patch); + + switch (columnId) + { + case INDEX: return std::to_string(rowElement.progNumber); + case NAME: return rowElement.name; + case CAT1: return rowElement.category1; + case CAT2: return rowElement.category2; + case ARP: return rowElement.arpMode != 0 ? "Y" : " "; + case UNI: return rowElement.unison == 0 ? " " : std::to_string(rowElement.unison + 1); + case ST: return rowElement.transpose != 64 ? std::to_string(rowElement.transpose - 64) : " "; + case VER: + { + switch (rowElement.model) + { + case virusLib::A: return "A"; + case virusLib::B: return "B"; + case virusLib::C: return "C"; + case virusLib::D: return "TI"; + case virusLib::D2: return "TI2"; + default: return "?"; + } + } + default: return "?"; + } + } } diff --git a/source/jucePlugin/ui3/PatchBrowser.h b/source/jucePlugin/ui3/PatchBrowser.h @@ -1,105 +1,51 @@ #pragma once -#include <set> +#include "PatchBrowser.h" -#include <juce_audio_processors/juce_audio_processors.h> +#include "../../jucePluginEditorLib/patchbrowser.h" + +#include "../../virusLib/microcontrollerTypes.h" namespace Virus { class Controller; } -namespace virusLib -{ - enum PresetVersion : uint8_t; -} - namespace genericVirusUI { class VirusEditor; - struct Patch + struct Patch : jucePluginEditorLib::Patch { - int progNumber = 0; - juce::String name; std::string category1; std::string category2; - std::vector<uint8_t> sysex; - virusLib::PresetVersion model; + virusLib::PresetVersion model = virusLib::PresetVersion::A; uint8_t unison = 0; uint8_t transpose = 0; uint8_t arpMode = 0; }; - class PatchBrowser : public juce::FileBrowserListener, juce::TableListBoxModel + class PatchBrowser : public jucePluginEditorLib::PatchBrowser { public: explicit PatchBrowser(const VirusEditor& _editor); - ~PatchBrowser() override; - - static uint32_t load(const Virus::Controller& _controller, std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<std::vector<uint8_t>>& _packets); - static bool load(const Virus::Controller& _controller, std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const std::vector<uint8_t>& _data); - static uint32_t loadBankFile(const Virus::Controller& _controller, std::vector<Patch>& _result, std::set<std::string>* _dedupeChecksums, const juce::File& file); - - bool selectPrevPreset(); - bool selectNextPreset(); private: - static bool initializePatch(const Virus::Controller& _controller, Patch& _patch); - - juce::FileBrowserComponent& getBankList() {return m_bankList; } - juce::TableListBox& getPatchList() {return m_patchList; } - juce::TextEditor& getSearchBox() {return m_search; } - - void fitInParent(juce::Component& _component, const std::string& _parentName) const; - - // Inherited via FileBrowserListener - void selectionChanged() override; - void fileClicked(const juce::File &file, const juce::MouseEvent &e) override; - void fileDoubleClicked(const juce::File &file) override {} - void browserRootChanged(const juce::File &newRoot) override {} + jucePluginEditorLib::Patch* createPatch() override + { + return new Patch(); + } + bool initializePatch(jucePluginEditorLib::Patch& patch) override; + juce::MD5 getChecksum(jucePluginEditorLib::Patch& _patch) override; + bool activatePatch(jucePluginEditorLib::Patch& _patch) override; + int comparePatches(int _columnId, const jucePluginEditorLib::Patch& a, const jucePluginEditorLib::Patch& b) const override; - // Inherited via TableListBoxModel - int getNumRows() override; - void paintRowBackground(juce::Graphics &, int rowNumber, int width, int height, bool rowIsSelected) override; - void paintCell(juce::Graphics &, int rowNumber, int columnId, int width, int height, bool rowIsSelected) override; - void selectedRowsChanged(int lastRowSelected) override; - void cellDoubleClicked (int rowNumber, int columnId, const juce::MouseEvent &) override; - void sortOrderChanged(int newSortColumnId, bool isForwards) override; + std::string getCellText(const jucePluginEditorLib::Patch& _patch, int _columnId) override; void loadRomBank(uint32_t _bankIndex); - void onFileSelected(const juce::File& file); - void fillPatchList(const std::vector<Patch>& _patches); - void refreshPatchList(); - - bool selectPrevNextPreset(int _dir); + bool selectPrevNextPreset(int _dir) override; class PatchBrowserSorter; - - enum Columns - { - INDEX = 1, - NAME = 2, - CAT1 = 3, - CAT2 = 4, - ARP = 5, - UNI = 6, - ST = 7, - VER = 8, - }; - - const VirusEditor& m_editor; - Virus::Controller& m_controller; - juce::WildcardFileFilter m_fileFilter; - juce::FileBrowserComponent m_bankList; - juce::TextEditor m_search; - juce::TableListBox m_patchList; - juce::Array<Patch> m_patches; - juce::Array<Patch> m_filteredPatches; - juce::PropertiesFile *m_properties; - juce::ComboBox* m_romBankSelect; - juce::HashMap<juce::String, bool> m_checksums; - bool m_sendOnSelect = true; }; } diff --git a/source/jucePlugin/ui3/VirusEditor.cpp b/source/jucePlugin/ui3/VirusEditor.cpp @@ -5,19 +5,19 @@ #include "../ParameterNames.h" #include "../PluginProcessor.h" #include "../VirusController.h" -#include "../VirusParameterBinding.h" #include "../version.h" +#include "../../jucePluginLib/parameterbinding.h" + #include "../../synthLib/os.h" #include "../../synthLib/sysexToMidi.h" namespace genericVirusUI { - VirusEditor::VirusEditor(VirusParameterBinding& _binding, AudioPluginAudioProcessor &_processorRef, const std::string& _jsonFilename, std::string _skinFolder, std::function<void()> _openMenuCallback) : - Editor(static_cast<EditorInterface&>(*this)), + VirusEditor::VirusEditor(pluginLib::ParameterBinding& _binding, AudioPluginAudioProcessor& _processorRef, const std::string& _jsonFilename, std::string _skinFolder, std::function<void()> _openMenuCallback) : + Editor(_processorRef, _binding, std::move(_skinFolder)), m_processor(_processorRef), m_parameterBinding(_binding), - m_skinFolder(std::move(_skinFolder)), m_openMenuCallback(std::move(_openMenuCallback)) { create(_jsonFilename); @@ -32,7 +32,7 @@ namespace genericVirusUI if(getControllerLinkCountRecursive() == 0) m_controllerLinks.reset(new ControllerLinks(*this)); - m_midiPorts.reset(new MidiPorts(*this)); + m_midiPorts.reset(new jucePluginEditorLib::MidiPorts(*this, getProcessor())); // be backwards compatible with old skins if(!getConditionCountRecursive()) @@ -132,7 +132,8 @@ namespace genericVirusUI param->onValueChanged.emplace_back(1, [this, param]() { - if(param->getChangeOrigin() == pluginLib::Parameter::ChangedBy::PresetChange) + if (param->getChangeOrigin() == pluginLib::Parameter::ChangedBy::PresetChange || + param->getChangeOrigin() == pluginLib::Parameter::ChangedBy::Derived) return; auto* comp = m_parameterBinding.getBoundComponent(param); if(comp) @@ -154,10 +155,10 @@ namespace genericVirusUI Virus::Controller& VirusEditor::getController() const { - return m_processor.getController(); + return static_cast<Virus::Controller&>(m_processor.getController()); } - const char* VirusEditor::findNamedResourceByFilename(const std::string& _filename, uint32_t& _size) + const char* VirusEditor::findEmbeddedResource(const std::string& _filename, uint32_t& _size) { for(size_t i=0; i<BinaryData::namedResourceListSize; ++i) { @@ -172,92 +173,14 @@ namespace genericVirusUI return nullptr; } - PatchBrowser* VirusEditor::getPatchBrowser() - { - return m_patchBrowser.get(); - } - - const char* VirusEditor::getResourceByFilename(const std::string& _name, uint32_t& _dataSize) - { - if(!m_skinFolder.empty()) - { - auto readFromCache = [this, &_name, &_dataSize]() - { - const auto it = m_fileCache.find(_name); - if(it == m_fileCache.end()) - { - _dataSize = 0; - return static_cast<char*>(nullptr); - } - _dataSize = static_cast<uint32_t>(it->second.size()); - return &it->second.front(); - }; - - auto* res = readFromCache(); - - if(res) - return res; - - const auto modulePath = synthLib::getModulePath(); - const auto folder = synthLib::validatePath(m_skinFolder.find(modulePath) == 0 ? m_skinFolder : modulePath + m_skinFolder); - - // try to load from disk first - FILE* hFile = fopen((folder + _name).c_str(), "rb"); - if(hFile) - { - fseek(hFile, 0, SEEK_END); - _dataSize = ftell(hFile); - fseek(hFile, 0, SEEK_SET); - - std::vector<char> data; - data.resize(_dataSize); - const auto readCount = fread(&data.front(), 1, _dataSize, hFile); - fclose(hFile); - - if(readCount == _dataSize) - m_fileCache.insert(std::make_pair(_name, std::move(data))); - - res = readFromCache(); - - if(res) - return res; - } - } - - uint32_t size = 0; - const auto res = findNamedResourceByFilename(_name, size); - if(!res) - throw std::runtime_error("Failed to find file named " + _name); - _dataSize = size; - return res; - } - - int VirusEditor::getParameterIndexByName(const std::string& _name) - { - return getController().getParameterIndexByName(_name); - } - - bool VirusEditor::bindParameter(juce::Button& _target, int _parameterIndex) - { - m_parameterBinding.bind(_target, _parameterIndex); - return true; - } - - bool VirusEditor::bindParameter(juce::ComboBox& _target, int _parameterIndex) + const char* VirusEditor::findResourceByFilename(const std::string& _filename, uint32_t& _size) { - m_parameterBinding.bind(_target, _parameterIndex); - return true; - } - - bool VirusEditor::bindParameter(juce::Slider& _target, int _parameterIndex) - { - m_parameterBinding.bind(_target, _parameterIndex); - return true; + return findEmbeddedResource(_filename, _size); } - juce::Value* VirusEditor::getParameterValue(int _parameterIndex) + PatchBrowser* VirusEditor::getPatchBrowser() { - return getController().getParamValueObject(_parameterIndex); + return m_patchBrowser.get(); } void VirusEditor::onProgramChange() @@ -453,10 +376,7 @@ namespace genericVirusUI juce::PopupMenu banksMenu; 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) + addEntry(banksMenu, getController().getBankName(b), [this, b](const FileType _type) { savePresets(SaveType::Bank, _type, b); }); @@ -487,8 +407,9 @@ namespace genericVirusUI m_previousPath = result.getParentDirectory().getFullPathName(); const auto ext = result.getFileExtension().toLowerCase(); - std::vector<Patch> patches; - PatchBrowser::loadBankFile(getController(), patches, nullptr, result); + PatchBrowser::PatchList patches; + + m_patchBrowser->loadBankFile(patches, nullptr, result); if (patches.empty()) return; @@ -496,7 +417,7 @@ namespace genericVirusUI if (patches.size() == 1) { // load to edit buffer of current part - const auto data = getController().modifySingleDump(patches.front().sysex, virusLib::BankNumber::EditBuffer, + const auto data = getController().modifySingleDump(patches.front()->sysex, virusLib::BankNumber::EditBuffer, getController().isMultiMode() ? getController().getCurrentPart() : virusLib::SINGLE, true, true); getController().sendSysEx(data); } @@ -505,7 +426,7 @@ namespace genericVirusUI // load to bank A for(uint8_t i=0; i<static_cast<uint8_t>(patches.size()); ++i) { - const auto data = getController().modifySingleDump(patches[i].sysex, virusLib::BankNumber::A, i, true, false); + const auto data = getController().modifySingleDump(patches[i]->sysex, virusLib::BankNumber::A, i, true, false); getController().sendSysEx(data); } } @@ -519,17 +440,23 @@ namespace genericVirusUI { const auto playMode = getController().getParameterIndexByName(Virus::g_paramPlayMode); - getController().getParameter(playMode)->setValue(_playMode, pluginLib::Parameter::ChangedBy::Ui); + auto* param = getController().getParameter(playMode); + param->setValue(_playMode, pluginLib::Parameter::ChangedBy::Ui); + + // we send this directly here as we request a new arrangement below, we don't want to wait on juce to inform the knob to have changed + getController().sendParameterChange(*param, _playMode); if (_playMode == virusLib::PlayModeSingle && getController().getCurrentPart() != 0) - m_parameterBinding.setPart(0); + setPart(0); onPlayModeChanged(); + + getController().requestArrangement(); } void VirusEditor::savePresets(SaveType _saveType, FileType _fileType, uint8_t _bankNumber/* = 0*/) { - const auto path = getController().getConfig()->getValue("virus_bank_dir", ""); + const auto path = m_processor.getConfig().getValue("virus_bank_dir", ""); m_fileChooser = std::make_unique<juce::FileChooser>( "Save preset(s) as syx or mid", m_previousPath.isEmpty() @@ -625,5 +552,6 @@ namespace genericVirusUI { m_parameterBinding.setPart(static_cast<uint8_t>(_part)); onCurrentPartChanged(); + setCurrentPart(static_cast<uint8_t>(_part)); } } diff --git a/source/jucePlugin/ui3/VirusEditor.h b/source/jucePlugin/ui3/VirusEditor.h @@ -1,25 +1,25 @@ #pragma once -#include "../../juceUiLib/editor.h" +#include "../../jucePluginEditorLib/midiPorts.h" +#include "../../jucePluginEditorLib/pluginEditor.h" #include "Parts.h" #include "Tabs.h" #include "FxPage.h" -#include "MidiPorts.h" #include "PatchBrowser.h" #include "ControllerLinks.h" namespace pluginLib { class Parameter; + class ParameterBinding; } -class VirusParameterBinding; class AudioPluginAudioProcessor; namespace genericVirusUI { - class VirusEditor : public genericUI::EditorInterface, public genericUI::Editor, juce::Timer + class VirusEditor : public jucePluginEditorLib::Editor, juce::Timer { public: enum class FileType @@ -35,29 +35,23 @@ namespace genericVirusUI Arrangement }; - VirusEditor(VirusParameterBinding& _binding, AudioPluginAudioProcessor &_processorRef, const std::string& _jsonFilename, + VirusEditor(pluginLib::ParameterBinding& _binding, AudioPluginAudioProcessor& _processorRef, const std::string& _jsonFilename, std::string _skinFolder, std::function<void()> _openMenuCallback); ~VirusEditor() override; void setPart(size_t _part); AudioPluginAudioProcessor& getProcessor() const { return m_processor; } - VirusParameterBinding& getParameterBinding() const { return m_parameterBinding; } + pluginLib::ParameterBinding& getParameterBinding() const { return m_parameterBinding; } Virus::Controller& getController() const; - static const char* findNamedResourceByFilename(const std::string& _filename, uint32_t& _size); + static const char* findEmbeddedResource(const std::string& _filename, uint32_t& _size); + const char* findResourceByFilename(const std::string& _filename, uint32_t& _size) override; PatchBrowser* getPatchBrowser(); private: - const char* getResourceByFilename(const std::string& _name, uint32_t& _dataSize) override; - int getParameterIndexByName(const std::string& _name) override; - bool bindParameter(juce::Button& _target, int _parameterIndex) override; - bool bindParameter(juce::ComboBox& _target, int _parameterIndex) override; - bool bindParameter(juce::Slider& _target, int _parameterIndex) override; - juce::Value* getParameterValue(int _parameterIndex) override; - void onProgramChange(); void onPlayModeChanged(); void onCurrentPartChanged(); @@ -80,13 +74,11 @@ namespace genericVirusUI bool savePresets(const std::string& _pathName, SaveType _saveType, FileType _fileType, uint8_t _bankNumber = 0) const; AudioPluginAudioProcessor& m_processor; - VirusParameterBinding& m_parameterBinding; - - const std::string m_skinFolder; + pluginLib::ParameterBinding& m_parameterBinding; std::unique_ptr<Parts> m_parts; std::unique_ptr<Tabs> m_tabs; - std::unique_ptr<MidiPorts> m_midiPorts; + std::unique_ptr<jucePluginEditorLib::MidiPorts> m_midiPorts; std::unique_ptr<FxPage> m_fxPage; std::unique_ptr<PatchBrowser> m_patchBrowser; std::unique_ptr<ControllerLinks> m_controllerLinks; @@ -109,7 +101,5 @@ namespace genericVirusUI 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/jucePluginEditorLib/CMakeLists.txt b/source/jucePluginEditorLib/CMakeLists.txt @@ -0,0 +1,20 @@ +cmake_minimum_required(VERSION 3.15) +project(jucePluginEditorLib VERSION ${CMAKE_PROJECT_VERSION}) + +set(SOURCES + midiPorts.cpp midiPorts.h + patchbrowser.cpp patchbrowser.h + pluginEditor.cpp pluginEditor.h + pluginEditorWindow.cpp pluginEditorWindow.h + pluginEditorState.cpp pluginEditorState.h + pluginProcessor.cpp pluginProcessor.h +) + +add_library(jucePluginEditorLib STATIC) + +target_sources(jucePluginEditorLib PRIVATE ${SOURCES}) +source_group("source" FILES ${SOURCES}) + +target_link_libraries(jucePluginEditorLib PUBLIC jucePluginLib juceUiLib) +target_include_directories(jucePluginEditorLib PUBLIC ../JUCE/modules) +target_compile_definitions(jucePluginEditorLib PRIVATE JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1) diff --git a/source/jucePluginEditorLib/midiPorts.cpp b/source/jucePluginEditorLib/midiPorts.cpp @@ -0,0 +1,143 @@ +#include "midiPorts.h" + +#include "pluginProcessor.h" + +#include "../../juceUiLib/editor.h" + +namespace jucePluginEditorLib +{ + MidiPorts::MidiPorts(const genericUI::Editor& _editor, Processor& _processor) : m_processor(_processor) + { + const auto& properties = m_processor.getConfig(); + + const auto midiIn = properties.getValue("midi_input", ""); + const auto midiOut = properties.getValue("midi_output", ""); + + if (!midiIn.isEmpty()) + m_processor.setMidiInput(midiIn); + + if (!midiOut.isEmpty()) + m_processor.setMidiOutput(midiOut); + + m_midiIn = _editor.findComponentT<juce::ComboBox>("MidiIn"); + m_midiOut = _editor.findComponentT<juce::ComboBox>("MidiOut"); + + m_midiIn->setTextWhenNoChoicesAvailable("-"); + + const auto midiInputs = juce::MidiInput::getAvailableDevices(); + + int inIndex = 0; + + m_midiIn->addItem("<none>", 1); + + for (int i = 0; i < midiInputs.size(); i++) + { + const auto input = midiInputs[i]; + + if (m_processor.getMidiInput() != nullptr && input.identifier == m_processor.getMidiInput()->getIdentifier()) + inIndex = i + 1; + + m_midiIn->addItem(input.name, i+2); + } + + m_midiIn->setSelectedItemIndex(inIndex, juce::dontSendNotification); + + m_midiIn->onChange = [this]() { updateMidiInput(m_midiIn->getSelectedItemIndex()); }; + + m_midiOut->setTextWhenNoChoicesAvailable("-"); + + const auto midiOutputs = juce::MidiOutput::getAvailableDevices(); + + auto outIndex = 0; + + m_midiOut->addItem("<none>", 1); + + for (int i = 0; i < midiOutputs.size(); i++) + { + const auto output = midiOutputs[i]; + if (m_processor.getMidiOutput() != nullptr && + output.identifier == m_processor.getMidiOutput()->getIdentifier()) + { + outIndex = i + 1; + } + m_midiOut->addItem(output.name, i+2); + } + + m_midiOut->setSelectedItemIndex(outIndex, juce::dontSendNotification); + + m_midiOut->onChange = [this]() { updateMidiOutput(m_midiOut->getSelectedItemIndex()); }; + + deviceManager = new juce::AudioDeviceManager(); + } + + MidiPorts::~MidiPorts() + { + delete deviceManager; + } + + void MidiPorts::updateMidiInput(int index) + { + const auto list = juce::MidiInput::getAvailableDevices(); + + auto& properties = m_processor.getConfig(); + + if (index <= 0) + { + properties.setValue("midi_input", ""); + properties.save(); + m_lastInputIndex = 0; + m_midiIn->setSelectedItemIndex(index, juce::dontSendNotification); + return; + } + + index--; + + const auto newInput = list[index]; + + if (!deviceManager->isMidiInputDeviceEnabled(newInput.identifier)) + deviceManager->setMidiInputDeviceEnabled(newInput.identifier, true); + + if (!m_processor.setMidiInput(newInput.identifier)) + { + m_midiIn->setSelectedItemIndex(0, juce::dontSendNotification); + m_lastInputIndex = 0; + return; + } + + properties.setValue("midi_input", newInput.identifier); + properties.save(); + + m_midiIn->setSelectedItemIndex(index + 1, juce::dontSendNotification); + m_lastInputIndex = index; + } + + void MidiPorts::updateMidiOutput(int index) + { + const auto list = juce::MidiOutput::getAvailableDevices(); + + auto& properties = m_processor.getConfig(); + + if (index == 0) + { + properties.setValue("midi_output", ""); + properties.save(); + m_midiOut->setSelectedItemIndex(index, juce::dontSendNotification); + m_lastOutputIndex = index; + m_processor.setMidiOutput(""); + return; + } + index--; + const auto newOutput = list[index]; + if (!m_processor.setMidiOutput(newOutput.identifier)) + { + m_midiOut->setSelectedItemIndex(0, juce::dontSendNotification); + m_lastOutputIndex = 0; + return; + } + properties.setValue("midi_output", newOutput.identifier); + properties.save(); + + m_midiOut->setSelectedItemIndex(index + 1, juce::dontSendNotification); + m_lastOutputIndex = index; + } +} diff --git a/source/jucePluginEditorLib/midiPorts.h b/source/jucePluginEditorLib/midiPorts.h @@ -0,0 +1,37 @@ +#pragma once + +namespace genericUI +{ + class Editor; +} + +namespace juce +{ + class AudioDeviceManager; + class ComboBox; +} + +namespace jucePluginEditorLib +{ + class Processor; + + class MidiPorts + { + public: + explicit MidiPorts(const genericUI::Editor& _editor, Processor& _processor); + ~MidiPorts(); + + private: + Processor& m_processor; + + juce::ComboBox* m_midiIn = nullptr; + juce::ComboBox* m_midiOut = nullptr; + + juce::AudioDeviceManager* deviceManager = nullptr; + int m_lastInputIndex = 0; + int m_lastOutputIndex = 0; + + void updateMidiInput(int _index); + void updateMidiOutput(int _index); + }; +} diff --git a/source/jucePluginEditorLib/patchbrowser.cpp b/source/jucePluginEditorLib/patchbrowser.cpp @@ -0,0 +1,371 @@ +#include "patchbrowser.h" + +#include "pluginEditor.h" + +#include "../synthLib/midiToSysex.h" + +using namespace juce; + +namespace jucePluginEditorLib +{ + static PatchBrowser* s_lastPatchBrowser = nullptr; + + PatchBrowser::PatchBrowser(const Editor& _editor, pluginLib::Controller& _controller, juce::PropertiesFile& _config, const std::initializer_list<ColumnDefinition>& _columns) + : m_editor(_editor), m_controller(_controller) + , m_properties(_config) + , m_fileFilter("*.syx;*.mid;*.midi", "*", "Patch Dumps") + , m_bankList(FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles, File::getSpecialLocation(File::SpecialLocationType::currentApplicationFile), &m_fileFilter, nullptr) + , m_search("Search Box") + , m_patchList("Patch Browser") + { + for (const auto& column : _columns) + m_patchList.getHeader().addColumn(column.name, column.id, column.width); + + const auto bankDir = m_properties.getValue("virus_bank_dir", ""); + + if (bankDir.isNotEmpty() && File(bankDir).isDirectory()) + { + m_bankList.setRoot(bankDir); + + s_lastPatchBrowser = this; + auto callbackPathBrowser = this; + + juce::Timer::callAfterDelay(1000, [&, bankDir, callbackPathBrowser]() + { + if(s_lastPatchBrowser != callbackPathBrowser) + return; + + const auto lastFile = m_properties.getValue("virus_selected_file", ""); + const auto child = File(bankDir).getChildFile(lastFile); + if(child.existsAsFile()) + { + m_bankList.setFileName(child.getFileName()); + m_sendOnSelect = false; + onFileSelected(child); + m_sendOnSelect = true; + } + }); + } + + fitInParent(m_bankList, "ContainerFileSelector"); + fitInParent(m_patchList, "ContainerPatchList"); + + m_search.setColour(TextEditor::textColourId, Colours::white); + m_search.onTextChange = [this] + { + refreshPatchList(); + }; + m_search.setTextToShowWhenEmpty("Search...", Colours::grey); + + fitInParent(m_search, "ContainerPatchListSearchBox"); + + m_bankList.addListener(this); + m_patchList.setModel(this); + + m_romBankSelect = _editor.findComponentT<juce::ComboBox>("RomBankSelect", false); + } + + PatchBrowser::~PatchBrowser() + { + if(s_lastPatchBrowser == this) + s_lastPatchBrowser = nullptr; + } + + bool PatchBrowser::selectPrevPreset() + { + return selectPrevNextPreset(-1); + } + + bool PatchBrowser::selectNextPreset() + { + return selectPrevNextPreset(1); + } + + uint32_t PatchBrowser::load(PatchList& _result, std::set<std::string>* _dedupeChecksums, const std::vector<std::vector<uint8_t>>& _packets) + { + uint32_t count = 0; + for (const auto& packet : _packets) + { + if (load(_result, _dedupeChecksums, packet)) + ++count; + } + return count; + } + + bool PatchBrowser::load(PatchList& _result, std::set<std::string>* _dedupeChecksums, const std::vector<uint8_t>& _data) + { + auto* patch = createPatch(); + patch->sysex = _data; + patch->progNumber = static_cast<int>(_result.size()); + + if(!initializePatch(*patch)) + return false; + + if (!_dedupeChecksums) + { + _result.push_back(std::shared_ptr<Patch>(patch)); + } + else + { + const auto md5 = std::string(getChecksum(*patch).toHexString().toRawUTF8()); + + if (_dedupeChecksums->find(md5) == _dedupeChecksums->end()) + { + _dedupeChecksums->insert(md5); + _result.push_back(std::shared_ptr<Patch>(patch)); + } + } + + return true; + } + + uint32_t PatchBrowser::loadBankFile(PatchList& _result, std::set<std::string>* _dedupeChecksums, const File& file) + { + const auto ext = file.getFileExtension().toLowerCase(); + const auto path = file.getParentDirectory().getFullPathName(); + + if (ext == ".syx") + { + MemoryBlock data; + + if (!file.loadFileAsData(data)) + return 0; + + std::vector<uint8_t> d; + d.resize(data.getSize()); + memcpy(&d[0], data.getData(), data.getSize()); + + std::vector<std::vector<uint8_t>> packets; + synthLib::MidiToSysex::splitMultipleSysex(packets, d); + + return load(_result, _dedupeChecksums, packets); + } + + if (ext == ".mid" || ext == ".midi") + { + std::vector<uint8_t> data; + + synthLib::MidiToSysex::readFile(data, file.getFullPathName().getCharPointer()); + + if (data.empty()) + return 0; + + std::vector<std::vector<uint8_t>> packets; + synthLib::MidiToSysex::splitMultipleSysex(packets, data); + + return load(_result, _dedupeChecksums, packets); + } + + return 0; + } + + bool PatchBrowser::selectPrevNextPreset(int _dir) + { + if(m_filteredPatches.empty()) + return false; + + const auto idx = m_patchList.getSelectedRow(); + + if(idx < 0) + return false; + + const auto newIdx = idx + _dir; + + if(newIdx < 0 || newIdx >= static_cast<int>(m_filteredPatches.size())) + return false; + + m_patchList.selectRow(newIdx); + return true; + } + + void PatchBrowser::fillPatchList(const std::vector<std::shared_ptr<Patch>>& _patches) + { + m_patches.clear(); + + for (const auto& patch : _patches) + m_patches.push_back(patch); + + refreshPatchList(); + } + + void PatchBrowser::refreshPatchList() + { + const auto searchValue = m_search.getText(); + const auto selectedPatchName = m_properties.getValue("virus_selected_patch", ""); + + m_filteredPatches.clear(); + int i=0; + int selectIndex = -1; + + for (const auto& patch : m_patches) + { + if (searchValue.isEmpty() || juce::String(patch->name).containsIgnoreCase(searchValue)) + { + m_filteredPatches.push_back(patch); + + if(patch->name == selectedPatchName) + selectIndex = i; + + ++i; + } + } + m_patchList.updateContent(); + m_patchList.deselectAllRows(); + m_patchList.repaint(); + + if(selectIndex != -1) + m_patchList.selectRow(selectIndex); + } + + void PatchBrowser::onFileSelected(const juce::File& file) + { + const auto ext = file.getFileExtension().toLowerCase(); + + if (file.existsAsFile() && ext == ".syx" || ext == ".midi" || ext == ".mid") + { + m_properties.setValue("virus_selected_file", file.getFileName()); + + std::vector<std::shared_ptr<Patch>> patches; + loadBankFile(patches, nullptr, file); + + fillPatchList(patches); + } + + if(m_romBankSelect) + m_romBankSelect->setSelectedItemIndex(0); + } + + void PatchBrowser::fitInParent(juce::Component& _component, const std::string& _parentName) const + { + auto* parent = m_editor.findComponent(_parentName); + + _component.setTransform(juce::AffineTransform::scale(2.0f)); + + const auto& bounds = parent->getBounds(); + const auto w = bounds.getWidth() >> 1; + const auto h = bounds.getHeight() >> 1; + + _component.setBounds(0,0, w,h); + + parent->addAndMakeVisible(_component); + } + + void PatchBrowser::fileClicked(const juce::File& file, const juce::MouseEvent& e) + { + const auto ext = file.getFileExtension().toLowerCase(); + const auto path = file.getParentDirectory().getFullPathName(); + if (file.isDirectory() && e.mods.isPopupMenu()) + { + auto p = PopupMenu(); + p.addItem("Add directory contents to patch list", [this, file]() + { + m_patches.clear(); + m_checksums.clear(); + std::set<std::string> dedupeChecksums; + + PatchList patches; + + for (const auto& f : RangedDirectoryIterator(file, false, "*.syx;*.mid;*.midi", File::findFiles)) + loadBankFile(patches, &dedupeChecksums, f.getFile()); + + fillPatchList(patches); + }); + p.showMenuAsync(PopupMenu::Options()); + + return; + } + m_properties.setValue("virus_bank_dir", path); + onFileSelected(file); + } + + int PatchBrowser::getNumRows() + { + return static_cast<int>(m_filteredPatches.size()); + } + + void PatchBrowser::paintRowBackground(Graphics& g, int rowNumber, int width, int height, bool rowIsSelected) + { + const auto alternateColour = m_patchList.getLookAndFeel() + .findColour(ListBox::backgroundColourId) + .interpolatedWith(m_patchList.getLookAndFeel().findColour(ListBox::textColourId), 0.03f); + if (rowIsSelected) + g.fillAll(Colours::lightblue); + else if (rowNumber & 1) + g.fillAll(alternateColour); + } + + void PatchBrowser::paintCell(Graphics& g, int rowNumber, int columnId, int width, int height, bool rowIsSelected) + { + if (rowNumber >= getNumRows()) + return; // Juce what are you up to? + + g.setColour(rowIsSelected ? Colours::darkblue : m_patchList.getLookAndFeel().findColour(ListBox::textColourId)); + + const auto& rowElement = m_filteredPatches[rowNumber]; + + //auto text = rowElement.name; + const String text = getCellText(*rowElement, columnId); + + g.drawText(text, 2, 0, width - 4, height, Justification::centredLeft, true); + g.setColour(m_patchList.getLookAndFeel().findColour(ListBox::backgroundColourId)); + g.fillRect(width - 1, 0, 1, height); + } + + void PatchBrowser::selectedRowsChanged(int lastRowSelected) + { + if(!m_sendOnSelect) + return; + + const auto idx = m_patchList.getSelectedRow(); + + if (idx == -1) + return; + + const auto& patch = m_filteredPatches[idx]; + + if(!activatePatch(*patch)) + return; + + m_properties.setValue("virus_selected_patch", juce::String(patch->name)); + } + + void PatchBrowser::cellDoubleClicked(int rowNumber, int columnId, const juce::MouseEvent& _mouseEvent) + { + if (rowNumber == m_patchList.getSelectedRow()) + selectedRowsChanged(0); + } + + + class PatchBrowserSorter + { + public: + PatchBrowserSorter(PatchBrowser& _browser, const int _attributeToSortBy, const bool _forward) : m_browser(_browser), m_attributeToSort(_attributeToSortBy), m_forward(_forward) + { + } + + bool operator()(const std::shared_ptr<Patch>& _a, const std::shared_ptr<Patch>& _b) const + { + return (compareElements(*_a, *_b) < 0) == m_forward; + } + + private: + int compareElements(const Patch& _a, const Patch& _b) const + { + return m_browser.comparePatches(m_attributeToSort, _a, _b); + } + + PatchBrowser& m_browser; + const int m_attributeToSort; + const bool m_forward; + }; + + void PatchBrowser::sortOrderChanged(int newSortColumnId, bool isForwards) + { + if (newSortColumnId != 0) + { + const PatchBrowserSorter sorter(*this, newSortColumnId, isForwards); + std::sort(m_filteredPatches.begin(), m_filteredPatches.end(), sorter); + m_patchList.updateContent(); + } + } +} diff --git a/source/jucePluginEditorLib/patchbrowser.h b/source/jucePluginEditorLib/patchbrowser.h @@ -0,0 +1,98 @@ +#pragma once + +#include <juce_audio_processors/juce_audio_processors.h> + +#include <set> + +#include "juce_cryptography/hashing/juce_MD5.h" + +namespace pluginLib +{ + class Controller; +} + +namespace jucePluginEditorLib +{ + class Editor; + + struct Patch + { + virtual ~Patch() = default; + + int progNumber = 0; + std::string name; + std::vector<uint8_t> sysex; + }; + + class PatchBrowser : public juce::FileBrowserListener, juce::TableListBoxModel + { + public: + struct ColumnDefinition + { + const char* name = nullptr; + int id = 0; + int width = 0; + }; + + using PatchList = std::vector<std::shared_ptr<Patch>>; + + PatchBrowser(const Editor& _editor, pluginLib::Controller& _controller, juce::PropertiesFile& _config, const std::initializer_list<ColumnDefinition>& _columns); + ~PatchBrowser() override; + + bool selectPrevPreset(); + bool selectNextPreset(); + + uint32_t load(PatchList& _result, std::set<std::string>* _dedupeChecksums, const std::vector<std::vector<uint8_t>>& _packets); + bool load(PatchList& _result, std::set<std::string>* _dedupeChecksums, const std::vector<uint8_t>& _data); + uint32_t loadBankFile(PatchList& _result, std::set<std::string>* _dedupeChecksums, const juce::File& file); + + protected: + virtual Patch* createPatch() = 0; + virtual bool initializePatch(Patch& _patch) = 0; + virtual juce::MD5 getChecksum(Patch& _patch) = 0; + virtual bool activatePatch(Patch& _patch) = 0; + public: + virtual int comparePatches(int _columnId, const Patch& _a, const Patch& _b) const = 0; + protected: + virtual std::string getCellText(const Patch& _patch, int _columnId) = 0; + virtual bool selectPrevNextPreset(int _dir); + + void fillPatchList(const PatchList& _patches); + void refreshPatchList(); + void onFileSelected(const juce::File& file); + + void fitInParent(juce::Component& _component, const std::string& _parentName) const; + + private: + // Inherited via FileBrowserListener + void selectionChanged() override {} + void fileClicked(const juce::File &file, const juce::MouseEvent &e) override; + void fileDoubleClicked(const juce::File &file) override {} + void browserRootChanged(const juce::File &newRoot) override {} + + // Inherited via TableListBoxModel + int getNumRows() override; + void paintRowBackground(juce::Graphics &, int rowNumber, int width, int height, bool rowIsSelected) override; + void paintCell(juce::Graphics &, int rowNumber, int columnId, int width, int height, bool rowIsSelected) override; + void selectedRowsChanged(int lastRowSelected) override; + void cellDoubleClicked (int rowNumber, int columnId, const juce::MouseEvent &) override; + void sortOrderChanged(int newSortColumnId, bool isForwards) override; + + protected: + const Editor& m_editor; + pluginLib::Controller& m_controller; + juce::PropertiesFile& m_properties; + + juce::WildcardFileFilter m_fileFilter; + juce::FileBrowserComponent m_bankList; + juce::TextEditor m_search; + juce::TableListBox m_patchList; + juce::ComboBox* m_romBankSelect = nullptr; + + PatchList m_patches; + PatchList m_filteredPatches; + + juce::HashMap<juce::String, bool> m_checksums; + bool m_sendOnSelect = true; + }; +} diff --git a/source/jucePluginEditorLib/pluginEditor.cpp b/source/jucePluginEditorLib/pluginEditor.cpp @@ -0,0 +1,100 @@ +#include "pluginEditor.h" + +#include "pluginProcessor.h" +#include "../jucePluginLib/parameterbinding.h" + +#include "../synthLib/os.h" + +namespace jucePluginEditorLib +{ + Editor::Editor(pluginLib::Processor& _processor, pluginLib::ParameterBinding& _binding, std::string _skinFolder) + : genericUI::Editor(static_cast<EditorInterface&>(*this)) + , m_processor(_processor) + , m_binding(_binding) + , m_skinFolder(std::move(_skinFolder)) + { + } + + const char* Editor::getResourceByFilename(const std::string& _name, uint32_t& _dataSize) + { + if(!m_skinFolder.empty()) + { + auto readFromCache = [this, &_name, &_dataSize]() + { + const auto it = m_fileCache.find(_name); + if(it == m_fileCache.end()) + { + _dataSize = 0; + return static_cast<char*>(nullptr); + } + _dataSize = static_cast<uint32_t>(it->second.size()); + return &it->second.front(); + }; + + auto* res = readFromCache(); + + if(res) + return res; + + const auto modulePath = synthLib::getModulePath(); + const auto folder = synthLib::validatePath(m_skinFolder.find(modulePath) == 0 ? m_skinFolder : modulePath + m_skinFolder); + + // try to load from disk first + FILE* hFile = fopen((folder + _name).c_str(), "rb"); + if(hFile) + { + fseek(hFile, 0, SEEK_END); + _dataSize = ftell(hFile); + fseek(hFile, 0, SEEK_SET); + + std::vector<char> data; + data.resize(_dataSize); + const auto readCount = fread(&data.front(), 1, _dataSize, hFile); + fclose(hFile); + + if(readCount == _dataSize) + m_fileCache.insert(std::make_pair(_name, std::move(data))); + + res = readFromCache(); + + if(res) + return res; + } + } + + uint32_t size = 0; + const auto res = findResourceByFilename(_name, size); + if(!res) + throw std::runtime_error("Failed to find file named " + _name); + _dataSize = size; + return res; + } + + int Editor::getParameterIndexByName(const std::string& _name) + { + return static_cast<int>(m_processor.getController().getParameterIndexByName(_name)); + } + + bool Editor::bindParameter(juce::Button& _target, int _parameterIndex) + { + m_binding.bind(_target, _parameterIndex); + return true; + } + + bool Editor::bindParameter(juce::ComboBox& _target, int _parameterIndex) + { + m_binding.bind(_target, _parameterIndex); + return true; + } + + bool Editor::bindParameter(juce::Slider& _target, int _parameterIndex) + { + m_binding.bind(_target, _parameterIndex); + return true; + } + + juce::Value* Editor::getParameterValue(int _parameterIndex, uint8_t _part) + { + return m_processor.getController().getParamValueObject(_parameterIndex, _part); + } +} diff --git a/source/jucePluginEditorLib/pluginEditor.h b/source/jucePluginEditorLib/pluginEditor.h @@ -0,0 +1,35 @@ +#pragma once + +#include "../juceUiLib/editor.h" + +namespace pluginLib +{ + class Processor; + class ParameterBinding; +} + +namespace jucePluginEditorLib +{ + class Editor : public genericUI::Editor, genericUI::EditorInterface + { + public: + Editor(pluginLib::Processor& _processor, pluginLib::ParameterBinding& _binding, std::string _skinFolder); + + virtual const char* findResourceByFilename(const std::string& _filename, uint32_t& _size) = 0; + + private: + const char* getResourceByFilename(const std::string& _name, uint32_t& _dataSize) override; + int getParameterIndexByName(const std::string& _name) override; + bool bindParameter(juce::Button& _target, int _parameterIndex) override; + bool bindParameter(juce::ComboBox& _target, int _parameterIndex) override; + bool bindParameter(juce::Slider& _target, int _parameterIndex) override; + juce::Value* getParameterValue(int _parameterIndex, uint8_t _part) override; + + pluginLib::Processor& m_processor; + pluginLib::ParameterBinding& m_binding; + + const std::string m_skinFolder; + + std::map<std::string, std::vector<char>> m_fileCache; + }; +} diff --git a/source/jucePluginEditorLib/pluginEditorState.cpp b/source/jucePluginEditorLib/pluginEditorState.cpp @@ -0,0 +1,234 @@ +#include "pluginEditorState.h" + +#include "pluginProcessor.h" +#include "../synthLib/os.h" +#include "dsp56kEmu/logging.h" + +#include "../juceUiLib/editor.h" + +namespace jucePluginEditorLib +{ +PluginEditorState::PluginEditorState(Processor& _processor, pluginLib::Controller& _controller, std::vector<Skin> _includedSkins) + : m_processor(_processor), m_parameterBinding(_controller), m_includedSkins(std::move(_includedSkins)) +{ +} + +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::loadDefaultSkin() +{ + Skin skin = readSkinFromConfig(); + + if(skin.jsonFilename.empty()) + { + skin = m_includedSkins[0]; + } + + loadSkin(skin); +} + +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 = createEditor(_skin, [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.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 = 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<genericUI::Editor*>(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.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.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/jucePluginEditorLib/pluginEditorState.h b/source/jucePluginEditorLib/pluginEditorState.h @@ -0,0 +1,84 @@ +#pragma once + +#include <functional> +#include <memory> +#include <string> +#include <vector> + +#include "../jucePluginLib/parameterbinding.h" + +namespace genericUI +{ + class Editor; +} + +namespace juce +{ + class Component; +} + +class AudioPluginAudioProcessor; + +namespace jucePluginEditorLib +{ + class Processor; + + 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(Processor& _processor, pluginLib::Controller& _controller, + std::vector<Skin> _includedSkins); + virtual ~PluginEditorState() = default; + + 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; } + 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(); + + void loadDefaultSkin(); + + protected: + virtual genericUI::Editor* createEditor(const Skin& _skin, std::function<void()> _openMenuCallback) = 0; + + Processor& m_processor; + pluginLib::ParameterBinding m_parameterBinding; + + private: + void loadSkin(const Skin& _skin); + void setGuiScale(int _scale) const; + + std::unique_ptr<juce::Component> m_virusEditor; + Skin m_currentSkin; + float m_rootScale = 1.0f; + std::vector<Skin> m_includedSkins; + }; +} diff --git a/source/jucePluginEditorLib/pluginEditorWindow.cpp b/source/jucePluginEditorLib/pluginEditorWindow.cpp @@ -0,0 +1,84 @@ +#include "pluginEditorWindow.h" +#include "pluginEditorState.h" + +namespace jucePluginEditorLib +{ + +//============================================================================== +EditorWindow::EditorWindow(juce::AudioProcessor& _p, PluginEditorState& _s, juce::PropertiesFile& _config) + : AudioProcessorEditor(&_p), m_state(_s), m_config(_config) +{ + addMouseListener(this, true); + + m_state.evSkinLoaded = [&](juce::Component* _component) + { + setUiRoot(_component); + }; + + m_state.evSetGuiScale = [&](const int _scale) + { + if(getNumChildComponents() > 0) + setGuiScale(getChildComponent(0), _scale); + }; + + m_state.enableBindings(); + + setUiRoot(m_state.getUiRoot()); +} + +EditorWindow::~EditorWindow() +{ + m_state.evSetGuiScale = [&](int){}; + m_state.evSkinLoaded = [&](juce::Component*){}; + + m_state.disableBindings(); + + setUiRoot(nullptr); +} + +void EditorWindow::setGuiScale(juce::Component* _comp, int percent) +{ + if(!_comp) + return; + + const auto s = static_cast<float>(percent)/100.0f * m_state.getRootScale(); + _comp->setTransform(juce::AffineTransform::scale(s,s)); + + setSize(static_cast<int>(m_state.getWidth() * s), static_cast<int>(m_state.getHeight() * s)); + + m_config.setValue("scale", percent); + m_config.saveIfNeeded(); +} + +void EditorWindow::setUiRoot(juce::Component* _component) +{ + removeAllChildren(); + + if(!_component) + return; + + const auto scale = m_config.getIntValue("scale", 100); + + setGuiScale(_component, scale); + addAndMakeVisible(_component); +} + +void EditorWindow::mouseDown(const juce::MouseEvent& event) +{ + if(!event.mods.isPopupMenu()) + { + AudioProcessorEditor::mouseDown(event); + return; + } + + // file browsers have their own menu, do not display two menus at once + if(event.eventComponent && event.eventComponent->findParentComponentOfClass<juce::FileBrowserComponent>()) + return; + + if(dynamic_cast<juce::TextEditor*>(event.eventComponent)) + return; + + m_state.openMenu(); +} + +} +\ No newline at end of file diff --git a/source/jucePluginEditorLib/pluginEditorWindow.h b/source/jucePluginEditorLib/pluginEditorWindow.h @@ -0,0 +1,31 @@ +#pragma once + +#include <juce_audio_processors/juce_audio_processors.h> + +class AudioPluginAudioProcessor; + +namespace jucePluginEditorLib +{ + class PluginEditorState; + + //============================================================================== + class EditorWindow : public juce::AudioProcessorEditor + { + public: + explicit EditorWindow (juce::AudioProcessor& _p, PluginEditorState& _s, juce::PropertiesFile& _config); + ~EditorWindow() override; + + void mouseDown(const juce::MouseEvent& event) override; + + void paint(juce::Graphics& g) override {} + + private: + void setGuiScale(juce::Component* _component, int percent); + void setUiRoot(juce::Component* _component); + + PluginEditorState& m_state; + juce::PropertiesFile& m_config; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EditorWindow) + }; +} diff --git a/source/jucePluginEditorLib/pluginProcessor.cpp b/source/jucePluginEditorLib/pluginProcessor.cpp @@ -0,0 +1,20 @@ +#include "pluginProcessor.h" + +namespace jucePluginEditorLib +{ + Processor::Processor(const BusesProperties& _busesProperties, const juce::PropertiesFile::Options& _configOptions) + : pluginLib::Processor(_busesProperties) + , m_config(_configOptions) + { + } + + bool Processor::setLatencyBlocks(const uint32_t _blocks) + { + if(!pluginLib::Processor::setLatencyBlocks(_blocks)) + return false; + + getConfig().setValue("latencyBlocks", static_cast<int>(_blocks)); + getConfig().saveIfNeeded(); + return true; + } +} diff --git a/source/jucePluginEditorLib/pluginProcessor.h b/source/jucePluginEditorLib/pluginProcessor.h @@ -0,0 +1,18 @@ +#pragma once + +#include "../jucePluginLib/processor.h" + +namespace jucePluginEditorLib +{ + class Processor : public pluginLib::Processor + { + public: + Processor(const BusesProperties& _busesProperties, const juce::PropertiesFile::Options& _configOptions); + + juce::PropertiesFile& getConfig() { return m_config; } + + bool setLatencyBlocks(uint32_t _blocks) override; + private: + juce::PropertiesFile m_config; + }; +} diff --git a/source/jucePluginLib/CMakeLists.txt b/source/jucePluginLib/CMakeLists.txt @@ -5,9 +5,11 @@ set(SOURCES controller.cpp controller.h midipacket.cpp midipacket.h parameter.cpp parameter.h + parameterbinding.cpp parameterbinding.h parameterdescription.cpp parameterdescription.h parameterdescriptions.cpp parameterdescriptions.h parameterlink.cpp parameterlink.h + processor.cpp processor.h ) add_library(jucePluginLib STATIC) @@ -15,5 +17,6 @@ add_library(jucePluginLib STATIC) target_sources(jucePluginLib PRIVATE ${SOURCES}) source_group("source" FILES ${SOURCES}) +target_link_libraries(jucePluginLib PUBLIC juceUiLib synthLib) target_include_directories(jucePluginLib PUBLIC ../JUCE/modules) target_compile_definitions(jucePluginLib PRIVATE JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1) diff --git a/source/jucePluginLib/controller.cpp b/source/jucePluginLib/controller.cpp @@ -3,10 +3,11 @@ #include <cassert> #include "parameter.h" +#include "processor.h" namespace pluginLib { - Controller::Controller(const std::string& _parameterDescJson) : m_descriptions(_parameterDescJson) + Controller::Controller(pluginLib::Processor& _processor, const std::string& _parameterDescJson) : m_processor(_processor), m_descriptions(_parameterDescJson) { } @@ -105,6 +106,31 @@ namespace pluginLib _processor.addParameterGroup(std::move(globalParams)); } + void Controller::sendSysEx(const pluginLib::SysEx& msg) const + { + synthLib::SMidiEvent ev; + ev.sysex = msg; + ev.source = synthLib::MidiEventSourceEditor; + m_processor.addMidiEvent(ev); + } + + bool Controller::sendSysEx(const std::string& _packetName) const + { + const std::map<pluginLib::MidiDataType, uint8_t> params; + return sendSysEx(_packetName, params); + } + + bool Controller::sendSysEx(const std::string& _packetName, const std::map<pluginLib::MidiDataType, uint8_t>& _params) const + { + std::vector<uint8_t> sysex; + + if(!createMidiDataFromPacket(sysex, _packetName, _params, 0)) + return false; + + sendSysEx(sysex); + return true; + } + const Controller::ParameterList& Controller::findSynthParam(const uint8_t _part, const uint8_t _page, const uint8_t _paramIndex) { const ParamIndex paramIndex{ _page, _part, _paramIndex }; @@ -130,9 +156,9 @@ namespace pluginLib return iti->second; } - juce::Value* Controller::getParamValueObject(const uint32_t _index) + juce::Value* Controller::getParamValueObject(const uint32_t _index, uint8_t _part) { - const auto res = getParameter(_index); + const auto res = getParameter(_index, _part); return res ? &res->getValueObject() : nullptr; } @@ -215,7 +241,7 @@ namespace pluginLib bool Controller::parseMidiPacket(std::string& _name, MidiPacket::Data& _data, MidiPacket::ParamValues& _parameterValues, const std::vector<uint8_t>& _src) const { - const auto packets = m_descriptions.getMidiPackets(); + const auto& packets = m_descriptions.getMidiPackets(); for (const auto& packet : packets) { @@ -228,6 +254,19 @@ namespace pluginLib return false; } + void Controller::addPluginMidiOut(const std::vector<synthLib::SMidiEvent>& _events) + { + const std::lock_guard l(m_pluginMidiOutLock); + m_pluginMidiOut.insert(m_pluginMidiOut.end(), _events.begin(), _events.end()); + } + + void Controller::getPluginMidiOut(std::vector<synthLib::SMidiEvent>& _events) + { + const std::lock_guard l(m_pluginMidiOutLock); + std::swap(m_pluginMidiOut, _events); + m_pluginMidiOut.clear(); + } + Parameter* Controller::createParameter(Controller& _controller, const Description& _desc, uint8_t _part, int _uid) { return new Parameter(_controller, _desc, _part, _uid); diff --git a/source/jucePluginLib/controller.h b/source/jucePluginLib/controller.h @@ -5,18 +5,27 @@ #include <string> +namespace synthLib +{ + struct SMidiEvent; +} + namespace pluginLib { + class Processor; + using SysEx = std::vector<uint8_t>; + class Controller { public: static constexpr uint32_t InvalidParameterIndex = 0xffffffff; - explicit Controller(const std::string& _parameterDescJson); + explicit Controller(pluginLib::Processor& _processor, const std::string& _parameterDescJson); + virtual ~Controller() = default; virtual void sendParameterChange(const Parameter& _parameter, uint8_t _value) = 0; - juce::Value* getParamValueObject(uint32_t _index); + juce::Value* getParamValueObject(uint32_t _index, uint8_t _part); Parameter* getParameter(uint32_t _index) const; Parameter* getParameter(uint32_t _index, uint8_t _part) const; @@ -32,12 +41,26 @@ namespace pluginLib const auto& getExposedParameters() { return m_synthParams; } + uint8_t getCurrentPart() const { return m_currentPart; } + void setCurrentPart(const uint8_t _part) { m_currentPart = _part; } + + virtual void parseSysexMessage(const SysEx&) = 0; + virtual void onStateLoaded() = 0; + + // this is called by the plug-in on audio thread! + void addPluginMidiOut(const std::vector<synthLib::SMidiEvent>&); + void getPluginMidiOut(std::vector<synthLib::SMidiEvent>&); + protected: virtual Parameter* createParameter(Controller& _controller, const Description& _desc, uint8_t _part, int _uid); void registerParams(juce::AudioProcessor& _processor); - private: + void sendSysEx(const pluginLib::SysEx &) const; + bool sendSysEx(const std::string& _packetName) const; + bool sendSysEx(const std::string& _packetName, const std::map<pluginLib::MidiDataType, uint8_t>& _params) const; + private: + Processor& m_processor; ParameterDescriptions m_descriptions; struct ParamIndex @@ -59,8 +82,13 @@ namespace pluginLib using ParameterList = std::vector<Parameter*>; - // tries to find synth param in both internal and host + uint8_t m_currentPart = 0; + + std::mutex m_pluginMidiOutLock; + std::vector<synthLib::SMidiEvent> m_pluginMidiOut; + protected: + // tries to find synth param in both internal and host const ParameterList& findSynthParam(uint8_t _part, uint8_t _page, uint8_t _paramIndex); const ParameterList& findSynthParam(const ParamIndex& _paramIndex); diff --git a/source/jucePluginLib/midipacket.cpp b/source/jucePluginLib/midipacket.cpp @@ -13,6 +13,8 @@ namespace pluginLib uint8_t usedMask = 0; uint32_t byteIndex = 0; + m_byteToDefinitionIndex.reserve(m_definitions.size()); + for(uint32_t i=0; i<m_definitions.size(); ++i) { const auto& d = m_definitions[i]; @@ -30,7 +32,11 @@ namespace pluginLib } m_definitionToByteIndex.insert(std::make_pair(i, byteIndex)); - m_byteToDefinitionIndex.insert(std::make_pair(byteIndex, i)); + + if(byteIndex >= m_byteToDefinitionIndex.size()) + m_byteToDefinitionIndex.push_back({}); + + m_byteToDefinitionIndex[byteIndex].push_back(i); usedMask |= masked; } @@ -46,11 +52,14 @@ namespace pluginLib for(size_t i=0; i<size(); ++i) { - const auto range = m_byteToDefinitionIndex.equal_range(static_cast<uint32_t>(i)); + if(i >= m_byteToDefinitionIndex.size()) + continue; - for(auto itRange = range.first; itRange != range.second; ++itRange) + const auto& range = m_byteToDefinitionIndex[i]; + + for(auto itRange = range.begin(); itRange != range.end(); ++itRange) { - const auto& d = m_definitions[itRange->second]; + const auto& d = m_definitions[*itRange]; switch (d.type) { @@ -72,7 +81,7 @@ namespace pluginLib } break; case MidiDataType::Checksum: - pendingChecksums.insert(std::make_pair(static_cast<uint32_t>(i), itRange->second)); + pendingChecksums.insert(std::make_pair(static_cast<uint32_t>(i), *itRange)); break; default: { @@ -90,7 +99,7 @@ namespace pluginLib } } - for (auto& pendingChecksum : pendingChecksums) + for (const auto& pendingChecksum : pendingChecksums) { const auto byteIndex = pendingChecksum.first; const auto descIndex = pendingChecksum.second; @@ -111,15 +120,20 @@ namespace pluginLib if(_src.size() != size()) return false; + _parameterValues.reserve(_src.size() << 1); + for(size_t i=0; i<_src.size(); ++i) { const auto s = _src[i]; - const auto range = m_byteToDefinitionIndex.equal_range(static_cast<uint32_t>(i)); + if(i >= m_byteToDefinitionIndex.size()) + continue; + + const auto& range = m_byteToDefinitionIndex[i]; - for(auto it = range.first; it != range.second; ++it) + for(auto it = range.begin(); it != range.end(); ++it) { - const auto& d = m_definitions[it->second]; + const auto& d = m_definitions[*it]; switch (d.type) { diff --git a/source/jucePluginLib/midipacket.h b/source/jucePluginLib/midipacket.h @@ -4,6 +4,7 @@ #include <map> #include <set> #include <string> +#include <unordered_map> #include <vector> namespace pluginLib @@ -49,8 +50,18 @@ namespace pluginLib using Data = std::map<MidiDataType, uint8_t>; using ParamIndex = std::pair<uint8_t,uint32_t>; + + struct ParamIndexHash + { + std::size_t operator () (const ParamIndex& p) const + { + static_assert(sizeof(std::size_t) > sizeof(uint32_t) + sizeof(uint8_t), "need a 64 bit compiler"); + return (static_cast<std::size_t>(p.first) << 32) | p.second; + } + }; + using ParamIndices = std::set<ParamIndex>; - using ParamValues = std::map<ParamIndex, uint8_t>; // part, index => value + using ParamValues = std::unordered_map<ParamIndex, uint8_t, ParamIndexHash>; // part, index => value using NamedParamValues = std::map<std::pair<uint8_t,std::string>, uint8_t>; // part, name => value using Sysex = std::vector<uint8_t>; @@ -74,7 +85,7 @@ namespace pluginLib const std::string m_name; std::vector<MidiDataDefinition> m_definitions; std::map<uint32_t, uint32_t> m_definitionToByteIndex; - std::multimap<uint32_t, uint32_t> m_byteToDefinitionIndex; + std::vector<std::vector<uint32_t>> m_byteToDefinitionIndex; uint32_t m_byteSize = 0; bool m_hasParameters = false; }; diff --git a/source/jucePluginLib/parameter.cpp b/source/jucePluginLib/parameter.cpp @@ -10,7 +10,7 @@ namespace pluginLib { m_range.start = static_cast<float>(m_desc.range.getStart()); m_range.end = static_cast<float>(m_desc.range.getEnd()); - m_range.interval = m_desc.isDiscrete || m_desc.isBool ? 1.0f : 0.0f; + m_range.interval = m_desc.step ? m_desc.step : (m_desc.isDiscrete || m_desc.isBool ? 1.0f : 0.0f); m_value.addListener(this); } @@ -37,6 +37,8 @@ namespace pluginLib if (newValue == m_lastValue) return; + _origin = ChangedBy::Derived; + m_lastValue = newValue; m_lastValueOrigin = _origin; @@ -162,5 +164,5 @@ namespace pluginLib m_derivedParameters.insert(_param); _param->m_derivedParameters.insert(this); - } + } } diff --git a/source/jucePluginLib/parameter.h b/source/jucePluginLib/parameter.h @@ -22,6 +22,7 @@ namespace pluginLib ControlChange, HostAutomation, Ui, + Derived }; Parameter(Controller& _controller, const Description& _desc, uint8_t _partNum, int uniqueId); diff --git a/source/jucePluginLib/parameterbinding.cpp b/source/jucePluginLib/parameterbinding.cpp @@ -0,0 +1,278 @@ +#include "parameterbinding.h" + +#include <cassert> + +#include "parameter.h" +#include "controller.h" + +namespace pluginLib +{ + ParameterBinding::MouseListener::MouseListener(pluginLib::Parameter* _param, juce::Slider& _slider) + : m_param(_param), m_slider(&_slider) + { + } + + void ParameterBinding::MouseListener::mouseDown(const juce::MouseEvent& event) + { + m_param->beginChangeGesture(); + } + + void ParameterBinding::MouseListener::mouseUp(const juce::MouseEvent& event) + { + m_param->endChangeGesture(); + } + + void ParameterBinding::MouseListener::mouseDrag(const juce::MouseEvent& event) + { + m_param->setValueNotifyingHost(m_param->convertTo0to1(static_cast<float>(m_slider->getValue()))); + } + + ParameterBinding::~ParameterBinding() + { + clearBindings(); + } + + void ParameterBinding::bind(juce::Slider &_slider, uint32_t _param) + { + bind(_slider, _param, CurrentPart); + } + void ParameterBinding::bind(juce::Slider &_slider, uint32_t _param, const uint8_t _part) + { + const auto v = m_controller.getParameter(_param, _part == CurrentPart ? m_controller.getCurrentPart() : _part); + + if (!v) + { + assert(false && "Failed to find parameter"); + return; + } + + removeMouseListener(_slider); + + auto* listener = new MouseListener(v, _slider); + m_sliderMouseListeners.insert(std::make_pair(&_slider, listener)); + + _slider.addMouseListener(listener, false); + + const auto& range = v->getNormalisableRange(); + + _slider.setRange(range.start, range.end, range.interval); + _slider.setDoubleClickReturnValue(true, v->convertFrom0to1(v->getDefaultValue())); + _slider.getValueObject().referTo(v->getValueObject()); + _slider.getProperties().set("type", "slider"); + _slider.getProperties().set("name", juce::String(v->getDescription().name)); + + if (v->isBipolar()) + _slider.getProperties().set("bipolar", true); + + const BoundParameter p{v, &_slider, _param, _part}; + addBinding(p); + } + + void ParameterBinding::bind(juce::ComboBox& _combo, uint32_t _param) + { + bind(_combo, _param, CurrentPart); + } + + void ParameterBinding::bind(juce::ComboBox& _combo, const uint32_t _param, uint8_t _part) + { + const auto v = m_controller.getParameter(_param, _part == CurrentPart ? m_controller.getCurrentPart() : _part); + + if (!v) + { + assert(false && "Failed to find parameter"); + return; + } + + _combo.setTextWhenNothingSelected("-"); + _combo.setScrollWheelEnabled(true); + + _combo.onChange = nullptr; + _combo.clear(); + + int idx = 1; + uint32_t count = 0; + for (const auto& vs : v->getAllValueStrings()) + { + if(vs.isNotEmpty()) + { + _combo.addItem(vs, idx); + if(++count == 16) + { + _combo.getRootMenu()->addColumnBreak(); + count = 0; + } + } + idx++; + } + + _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>(id - 1))); + v->endChangeGesture(); + } + v->getValueObject().setValue(id - 1); + }; + + const auto listenerId = m_nextListenerId++; + + v->onValueChanged.emplace_back(std::make_pair(listenerId, [this, &_combo, v]() + { + const auto value = static_cast<int>(v->getValueObject().getValueSource().getValue()); + _combo.setSelectedId(value + 1, juce::dontSendNotification); + })); + + const BoundParameter p{v, &_combo, _param, _part, listenerId}; + addBinding(p); + } + + void ParameterBinding::bind(juce::Button &_btn, const uint32_t _param) + { + bind(_btn, _param, CurrentPart); + } + + void ParameterBinding::bind(juce::Button& _control, uint32_t _param, uint8_t _part) + { + const auto v = m_controller.getParameter(_param, _part == CurrentPart ? m_controller.getCurrentPart() : _part); + if (!v) + { + assert(false && "Failed to find parameter"); + return; + } + _control.getToggleStateValue().referTo(v->getValueObject()); + const BoundParameter p{v, &_control, _param, CurrentPart}; + addBinding(p); + } + + juce::Component* ParameterBinding::getBoundComponent(const pluginLib::Parameter* _parameter) + { + const auto it = m_boundParameters.find(_parameter); + if(it == m_boundParameters.end()) + return nullptr; + return it->second; + } + + void ParameterBinding::removeMouseListener(juce::Slider& _slider) + { + const auto it = m_sliderMouseListeners.find(&_slider); + + if(it != m_sliderMouseListeners.end()) + { + _slider.removeMouseListener(it->second); + delete it->second; + m_sliderMouseListeners.erase(it); + } + } + + void ParameterBinding::bind(const std::vector<BoundParameter>& _bindings, const bool _currentPartOnly) + { + for (const auto& b : _bindings) + { + auto* slider = dynamic_cast<juce::Slider*>(b.component); + if(slider) + { + bind(*slider, b.type, b.part); + continue; + } + auto* button = dynamic_cast<juce::DrawableButton*>(b.component); + if(button) + { + bind(*button, b.type, b.part); + continue; + } + auto* comboBox = dynamic_cast<juce::ComboBox*>(b.component); + if(comboBox) + { + bind(*comboBox, b.type, b.part); + continue; + } + assert(false && "unknown component type"); + } + } + + void ParameterBinding::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 ParameterBinding::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); + slider->getValueObject().referTo(juce::Value()); + } + + auto* combo = dynamic_cast<juce::ComboBox*>(_b.component); + if(combo != nullptr) + combo->onChange = nullptr; + + auto* button = dynamic_cast<juce::Button*>(_b.component); + if(button != nullptr) + button->getToggleStateValue().referTo(juce::Value()); + + if(_b.onChangeListenerId) + _b.parameter->removeListener(_b.onChangeListenerId); + } + + void ParameterBinding::clearBindings() + { + for (const auto& b : m_bindings) + disableBinding(b); + + m_bindings.clear(); + m_boundParameters.clear(); + m_boundComponents.clear(); + } + + void ParameterBinding::setPart(uint8_t _part) + { + const std::vector<BoundParameter> bindings = m_bindings; + + clearBindings(); + + m_controller.setCurrentPart(_part); + + bind(bindings, true); + } + + void ParameterBinding::disableBindings() + { + m_disabledBindings.clear(); + std::swap(m_bindings, m_disabledBindings); + + for (const auto& b : m_disabledBindings) + disableBinding(b); + } + + void ParameterBinding::enableBindings() + { + bind(m_disabledBindings, false); + m_disabledBindings.clear(); + } +} diff --git a/source/jucePluginLib/parameterbinding.h b/source/jucePluginLib/parameterbinding.h @@ -0,0 +1,80 @@ +#pragma once + +#include <juce_audio_processors/juce_audio_processors.h> + +namespace juce +{ + class MouseEvent; + class Slider; +} + +namespace pluginLib +{ + class Parameter; + class Controller; + + class ParameterBinding + { + class MouseListener : public juce::MouseListener + { + public: + MouseListener(pluginLib::Parameter* _param, juce::Slider& _slider); + void mouseDown(const juce::MouseEvent& event) override; + void mouseUp(const juce::MouseEvent& event) override; + void mouseDrag(const juce::MouseEvent& event) override; + + private: + pluginLib::Parameter *m_param; + juce::Slider* m_slider; + }; + public: + 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; + }; + + ParameterBinding(Controller& _controller) : m_controller(_controller) + { + } + ~ParameterBinding(); + + 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 bind(juce::Button &_control, uint32_t _param, uint8_t _part); + + 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); + + void addBinding(const BoundParameter& _boundParameter); + void disableBinding(const BoundParameter& _b); + + Controller& m_controller; + + 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 = 100000; + }; +} diff --git a/source/jucePluginLib/parameterdescription.h b/source/jucePluginLib/parameterdescription.h @@ -50,6 +50,7 @@ namespace pluginLib bool isDiscrete; bool isBool; bool isBipolar; + int step = 0; std::string toText; bool isNonPartSensitive() const; diff --git a/source/jucePluginLib/parameterdescriptions.cpp b/source/jucePluginLib/parameterdescriptions.cpp @@ -242,9 +242,20 @@ namespace pluginLib d.isDiscrete = readPropertyBool("isDiscrete"); d.isBool = readPropertyBool("isBool"); d.isBipolar = readPropertyBool("isBipolar"); + d.step = readPropertyIntWithDefault("step", 0); d.toText = valueList; - d.index = static_cast<uint8_t>(readPropertyInt("index")); + + d.page = static_cast<uint8_t>(readPropertyInt("page")); + + auto index = readPropertyInt("index"); + + while(index >= 128) + { + index -= 128; + ++d.page; + } + d.index = static_cast<uint8_t>(index); d.range.setStart(minValue); d.range.setEnd(maxValue); @@ -299,8 +310,6 @@ namespace pluginLib } } - d.page = static_cast<uint8_t>(readPropertyInt("page")); - m_descriptions.push_back(d); } @@ -498,7 +507,7 @@ namespace pluginLib { if(p.checksumFirstIndex >= (packet.size()-1) || p.checksumLastIndex >= (packet.size()-1)) { - _errors << "specified checksum range " << p.checksumFirstIndex << "-" << p.checksumLastIndex << " is out of range 0-" << packet.size() << i << std::endl; + _errors << "specified checksum range " << p.checksumFirstIndex << "-" << p.checksumLastIndex << " is out of range 0-" << packet.size() << std::endl; return; } } diff --git a/source/jucePluginLib/parameterdescriptions.h b/source/jucePluginLib/parameterdescriptions.h @@ -1,6 +1,6 @@ #pragma once -#include <map> +#include <unordered_map> #include <string> #include <vector> @@ -26,7 +26,7 @@ namespace pluginLib bool getIndexByName(uint32_t& _index, const std::string& _name) const; - const std::map<std::string, MidiPacket>& getMidiPackets() const { return m_midiPackets; } + const std::unordered_map<std::string, MidiPacket>& getMidiPackets() const { return m_midiPackets; } private: std::string loadJson(const std::string& _jsonString); @@ -36,10 +36,10 @@ namespace pluginLib 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::unordered_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::unordered_map<std::string, uint32_t> m_nameToIndex; + std::unordered_map<std::string, MidiPacket> m_midiPackets; std::vector<ParameterLink> m_parameterLinks; }; } diff --git a/source/jucePluginLib/processor.cpp b/source/jucePluginLib/processor.cpp @@ -0,0 +1,175 @@ +#include "processor.h" + +namespace pluginLib +{ + Processor::Processor(const BusesProperties& _busesProperties) : juce::AudioProcessor(_busesProperties) + { + } + + Processor::~Processor() + { + m_controller.reset(); + } + + void Processor::getLastMidiOut(std::vector<synthLib::SMidiEvent>& dst) + { + juce::ScopedLock lock(getCallbackLock()); + std::swap(dst, m_midiOut); + m_midiOut.clear(); + } + + void Processor::addMidiEvent(const synthLib::SMidiEvent& ev) + { + getPlugin().addMidiEvent(ev); + } + + juce::MidiOutput *Processor::getMidiOutput() const { return m_midiOutput.get(); } + juce::MidiInput *Processor::getMidiInput() const { return m_midiInput.get(); } + + bool Processor::setMidiOutput(const juce::String& _out) + { + if (m_midiOutput != nullptr && m_midiOutput->isBackgroundThreadRunning()) + { + m_midiOutput->stopBackgroundThread(); + } + m_midiOutput = juce::MidiOutput::openDevice(_out); + if (m_midiOutput != nullptr) + { + m_midiOutput->startBackgroundThread(); + return true; + } + return false; + } + + bool Processor::setMidiInput(const juce::String& _in) + { + if (m_midiInput != nullptr) + { + m_midiInput->stop(); + } + m_midiInput = juce::MidiInput::openDevice(_in, this); + if (m_midiInput != nullptr) + { + m_midiInput->start(); + return true; + } + return false; + } + + void Processor::handleIncomingMidiMessage(juce::MidiInput *source, const juce::MidiMessage &message) + { + const auto* raw = message.getSysExData(); + if (raw) + { + const auto count = message.getSysExDataSize(); + auto syx = pluginLib::SysEx(); + syx.push_back(0xf0); + for (int i = 0; i < count; i++) + { + syx.push_back(raw[i]); + } + syx.push_back(0xf7); + synthLib::SMidiEvent sm; + sm.source = synthLib::MidiEventSourcePlugin; + sm.sysex = syx; + getController().parseSysexMessage(syx); + + addMidiEvent(sm); + + if (m_midiOutput) + { + std::vector<synthLib::SMidiEvent> data; + getLastMidiOut(data); + if (!data.empty()) + { + const auto msg = juce::MidiMessage::createSysExMessage(data.data(), static_cast<int>(data.size())); + + m_midiOutput->sendMessageNow(msg); + } + } + } + else + { + const auto count = message.getRawDataSize(); + const auto* rawData = message.getRawData(); + if (count >= 1 && count <= 3) + { + synthLib::SMidiEvent sm; + sm.source = synthLib::MidiEventSourcePlugin; + sm.a = rawData[0]; + sm.b = count > 1 ? rawData[1] : 0; + sm.c = count > 2 ? rawData[2] : 0; + addMidiEvent(sm); + } + else + { + synthLib::SMidiEvent sm; + sm.source = synthLib::MidiEventSourcePlugin; + auto syx = SysEx(); + for (int i = 0; i < count; i++) + { + syx.push_back(rawData[i]); + } + sm.sysex = syx; + addMidiEvent(sm); + } + } + } + + pluginLib::Controller& Processor::getController() + { + if (m_controller == nullptr) + m_controller.reset(createController()); + + return *m_controller; + } + + bool Processor::setLatencyBlocks(uint32_t _blocks) + { + return getPlugin().setLatencyBlocks(_blocks); + } + + //============================================================================== + void Processor::getStateInformation (juce::MemoryBlock& destData) + { + // You should use this method to store your parameters in the memory block. + // You could do that either as raw data, or use the XML or ValueTree classes + // as intermediaries to make it easy to save and load complex data. + + std::vector<uint8_t> state; + getPlugin().getState(state, synthLib::StateTypeGlobal); + destData.append(&state[0], state.size()); + } + + void Processor::setStateInformation (const void* data, int sizeInBytes) + { + // You should use this method to restore your parameters from this memory block, + // whose contents will have been created by the getStateInformation() call. + setState(data, sizeInBytes); + } + + void Processor::getCurrentProgramStateInformation(juce::MemoryBlock& destData) + { + std::vector<uint8_t> state; + getPlugin().getState(state, synthLib::StateTypeCurrentProgram); + destData.append(&state[0], state.size()); + } + + void Processor::setCurrentProgramStateInformation(const void* data, int sizeInBytes) + { + setState(data, sizeInBytes); + } + + void Processor::setState(const void* _data, size_t _sizeInBytes) + { + if(_sizeInBytes < 1) + return; + + std::vector<uint8_t> state; + state.resize(_sizeInBytes); + memcpy(&state[0], _data, _sizeInBytes); + getPlugin().setState(state); + if (hasController()) + getController().onStateLoaded(); + } +} diff --git a/source/jucePluginLib/processor.h b/source/jucePluginLib/processor.h @@ -0,0 +1,63 @@ +#pragma once + +#include <juce_audio_processors/juce_audio_processors.h> +#include <juce_audio_devices/juce_audio_devices.h> + +#include "controller.h" + +#include "../synthLib/plugin.h" + +namespace synthLib +{ + class Plugin; + struct SMidiEvent; +} + +namespace pluginLib +{ + class Processor : public juce::AudioProcessor, juce::MidiInputCallback + { + public: + Processor(const BusesProperties& _busesProperties); + ~Processor() override; + + void getLastMidiOut(std::vector<synthLib::SMidiEvent>& dst); + void addMidiEvent(const synthLib::SMidiEvent& ev); + + bool setMidiOutput(const juce::String& _out); + juce::MidiOutput* getMidiOutput() const; + bool setMidiInput(const juce::String& _in); + juce::MidiInput* getMidiInput() const; + + void handleIncomingMidiMessage(juce::MidiInput *source, const juce::MidiMessage &message) override; + + Controller& getController(); + bool isPluginValid() { return getPlugin().isValid(); } + + virtual synthLib::Plugin& getPlugin() = 0; + bool hasController() const + { + return m_controller.get(); + } + + virtual bool setLatencyBlocks(uint32_t _blocks); + + private: + //============================================================================== + void getStateInformation (juce::MemoryBlock& destData) override; + void setStateInformation (const void* data, int sizeInBytes) override; + void getCurrentProgramStateInformation (juce::MemoryBlock& destData) override; + void setCurrentProgramStateInformation (const void* data, int sizeInBytes) override; + + void setState(const void *_data, size_t _sizeInBytes); + + virtual Controller* createController() = 0; + + std::unique_ptr<pluginLib::Controller> m_controller{}; + + protected: + std::unique_ptr<juce::MidiOutput> m_midiOutput{}; + std::unique_ptr<juce::MidiInput> m_midiInput{}; + std::vector<synthLib::SMidiEvent> m_midiOut{}; + }; +} diff --git a/source/juceUiLib/buttonStyle.cpp b/source/juceUiLib/buttonStyle.cpp @@ -36,12 +36,15 @@ namespace genericUI "disabledImageOn" }; - const bool isVerticalTiling = m_tileSizeY <= (m_drawable->getHeight()>>1); - static_assert(std::size(imageDrawables) == std::size(properties), "arrays must have same size"); + const bool isVerticalTiling = m_tileSizeY <= (m_drawable->getHeight()>>1); + std::map<int, juce::Drawable*> drawables; + const auto targetW = _object.getPropertyInt("width", 0); + const auto targetH = _object.getPropertyInt("height", 0); + for(size_t i=0; i<std::size(imageDrawables); ++i) { const auto imageIndex = _object.getPropertyInt(properties[i], -1); @@ -57,8 +60,9 @@ namespace genericUI } else { + const auto needsScale = targetW && targetH && (targetW != m_tileSizeX || targetH != m_tileSizeY); juce::Drawable* d = m_drawable; - if(imageIndex > 0) + if(needsScale || imageIndex > 0) { m_createdDrawables.emplace_back(d->createCopy()); d = m_createdDrawables.back().get(); @@ -67,8 +71,18 @@ namespace genericUI else d->setOriginWithOriginalSize({static_cast<float>(-imageIndex * m_tileSizeX), 0.0f}); } + *imageDrawables[i] = d; + drawables.insert(std::make_pair(imageIndex, d)); + + if(needsScale) + { + auto* d = *imageDrawables[i]; + const auto scaleX = static_cast<float>(targetW) / static_cast<float>(m_tileSizeX); + const auto scaleY = static_cast<float>(targetH) / static_cast<float>(m_tileSizeY); + d->setTransform(d->getTransform().scaled(scaleX, scaleY)); + } } } } diff --git a/source/juceUiLib/condition.cpp b/source/juceUiLib/condition.cpp @@ -4,15 +4,14 @@ namespace genericUI { - Condition::Condition(juce::Component& _target, juce::Value& _value, std::set<uint8_t> _values) : m_target(_target), m_value(_value), m_values(std::move(_values)) + Condition::Condition(juce::Component& _target, juce::Value* _value, int32_t _parameterIndex, std::set<uint8_t> _values) : m_target(_target), m_parameterIndex(_parameterIndex), m_values(std::move(_values)) { - m_value.addListener(this); - valueChanged(m_value); + bind(_value); } Condition::~Condition() { - m_value.removeListener(this); + unbind(); } void Condition::valueChanged(juce::Value& _value) @@ -23,4 +22,26 @@ namespace genericUI Editor::setEnabled(m_target, enable); } + + void Condition::bind(juce::Value* _value) + { + unbind(); + + m_value = _value; + + if(!m_value) + return; + + m_value->addListener(this); + valueChanged(*m_value); + } + + void Condition::unbind() + { + if(!m_value) + return; + + m_value->removeListener(this); + m_value = nullptr; + } } diff --git a/source/juceUiLib/condition.h b/source/juceUiLib/condition.h @@ -15,12 +15,16 @@ namespace genericUI class Condition : juce::Value::Listener { public: - Condition(juce::Component& _target, juce::Value& _value, std::set<uint8_t> _values); + Condition(juce::Component& _target, juce::Value* _value, int32_t _parameterIndex, std::set<uint8_t> _values); ~Condition() override; void valueChanged(juce::Value& _value) override; + void bind(juce::Value* _value); + void unbind(); + int32_t getParameterIndex() const { return m_parameterIndex; } private: juce::Component& m_target; - juce::Value& m_value; + const int32_t m_parameterIndex; + juce::Value* m_value = nullptr; std::set<uint8_t> m_values; }; } diff --git a/source/juceUiLib/editor.cpp b/source/juceUiLib/editor.cpp @@ -227,4 +227,9 @@ namespace genericUI _component.setVisible(_enable); } } + + void Editor::setCurrentPart(uint8_t _part) + { + m_rootObject->setCurrentPart(*this, _part); + } } diff --git a/source/juceUiLib/editor.h b/source/juceUiLib/editor.h @@ -77,6 +77,8 @@ namespace genericUI static void setEnabled(juce::Component& _component, bool _enable); + void setCurrentPart(uint8_t _part); + private: EditorInterface& m_interface; diff --git a/source/juceUiLib/editorInterface.h b/source/juceUiLib/editorInterface.h @@ -10,7 +10,7 @@ namespace genericUI virtual const char* getResourceByFilename(const std::string& _name, uint32_t& _dataSize) = 0; virtual int getParameterIndexByName(const std::string& _name) = 0; - virtual juce::Value* getParameterValue(int _parameterIndex) = 0; + virtual juce::Value* getParameterValue(int _parameterIndex, uint8_t _part) = 0; virtual bool bindParameter(juce::Slider& _target, int _parameterIndex) = 0; virtual bool bindParameter(juce::Button& _target, int _parameterIndex) = 0; diff --git a/source/juceUiLib/rotaryStyle.cpp b/source/juceUiLib/rotaryStyle.cpp @@ -25,7 +25,7 @@ namespace genericUI if(!m_drawable || !m_tileSizeX || !m_tileSizeY) return; -// const auto w = m_drawable->getWidth(); + const auto w = m_drawable->getWidth(); const auto h = m_drawable->getHeight(); // const auto stepsX = w / m_tileSizeX; @@ -36,7 +36,14 @@ namespace genericUI m_drawable->setOriginWithOriginalSize({0.0f, static_cast<float>(-m_tileSizeY * stepY)}); - m_drawable->drawAt(_graphics, static_cast<float>(x), static_cast<float>(y), 1.0f); + auto t = juce::AffineTransform::translation (static_cast<float>(x), static_cast<float>(y)); + + if(width != m_tileSizeX || height != m_tileSizeY) + { + t = t.scaled(static_cast<float>(width) / static_cast<float>(m_tileSizeX), static_cast<float>(height) / static_cast<float>(m_tileSizeY)); + } + + m_drawable->draw(_graphics, 1.0f, t); } void RotaryStyle::drawLinearSlider(juce::Graphics& _graphics, int x, int y, int width, int height, float sliderPos, float minSliderPos, float maxSliderPos, const juce::Slider::SliderStyle _sliderStyle, juce::Slider& _slider) diff --git a/source/juceUiLib/uiObject.cpp b/source/juceUiLib/uiObject.cpp @@ -273,6 +273,21 @@ namespace genericUI return count; } + void UiObject::setCurrentPart(Editor& _editor, uint8_t _part) + { + if(m_condition) + { + m_condition->unbind(); + + const auto v = _editor.getInterface().getParameterValue(m_condition->getParameterIndex(), _part); + if(v) + m_condition->bind(v); + } + + for (const auto& child : m_children) + child->setCurrentPart(_editor, _part); + } + void UiObject::createCondition(Editor& _editor, juce::Component& _target) { if(!hasComponent("condition")) @@ -285,11 +300,6 @@ namespace genericUI if(index < 0) throw std::runtime_error("Parameter named " + paramName + " not found"); - const auto v = _editor.getInterface().getParameterValue(index); - - if(!v) - throw std::runtime_error("Parameter named " + paramName + " not found"); - const auto conditionValues = getProperty("enableOnValues"); size_t start = 0; @@ -310,7 +320,12 @@ namespace genericUI start = i + 1; } - m_condition.reset(new Condition(_target, *v, values)); + const auto v = _editor.getInterface().getParameterValue(index, 0); + + if(!v) + throw std::runtime_error("Parameter named " + paramName + " not found"); + + m_condition.reset(new Condition(_target, v, static_cast<uint32_t>(index), values)); } bool UiObject::parse(juce::DynamicObject* _obj) diff --git a/source/juceUiLib/uiObject.h b/source/juceUiLib/uiObject.h @@ -60,6 +60,8 @@ namespace genericUI size_t getConditionCountRecursive() const; size_t getControllerLinkCountRecursive() const; + void setCurrentPart(Editor& _editor, uint8_t _part); + private: bool hasComponent(const std::string& _component) const; template<typename T> T* createJuceObject(Editor& _editor); diff --git a/source/synthLib/os.cpp b/source/synthLib/os.cpp @@ -265,4 +265,44 @@ namespace synthLib _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON); #endif } + + bool writeFile(const std::string& _filename, const std::vector<uint8_t>& _data) + { + return writeFile(_filename, &_data[0], _data.size()); + } + + bool writeFile(const std::string& _filename, const uint8_t* _data, size_t _size) + { + auto* hFile = fopen(_filename.c_str(), "wb"); + if(!hFile) + return false; + const auto written = fwrite(&_data[0], 1, _size, hFile); + fclose(hFile); + return written == _size; + } + + bool readFile(std::vector<uint8_t>& _data, const std::string& _filename) + { + auto* hFile = fopen(_filename.c_str(), "rb"); + if(!hFile) + return false; + + fseek(hFile, 0, SEEK_END); + const auto size = ftell(hFile); + fseek(hFile, 0, SEEK_SET); + + if(!size) + { + fclose(hFile); + _data.clear(); + return true; + } + + if(_data.size() < static_cast<size_t>(size)) + _data.resize(size); + + const auto read = fread(&_data[0], 1, _data.size(), hFile); + fclose(hFile); + return read == _data.size(); + } } // namespace synthLib diff --git a/source/synthLib/os.h b/source/synthLib/os.h @@ -1,6 +1,8 @@ #pragma once + #include <string> #include <vector> +#include <array> namespace synthLib { @@ -20,4 +22,14 @@ namespace synthLib bool hasExtension(const std::string& _filename, const std::string& _extension); void setFlushDenormalsToZero(); + + bool writeFile(const std::string& _filename, const std::vector<uint8_t>& _data); + bool writeFile(const std::string& _filename, const uint8_t* _data, size_t _size); + + template<size_t Size> bool writeFile(const std::string& _filename, const std::array<uint8_t, Size>& _data) + { + return writeFile(_filename, &_data[0], _data.size()); + } + + bool readFile(std::vector<uint8_t>& _data, const std::string& _filename); } // namespace synthLib diff --git a/source/synthLib/resampler.cpp b/source/synthLib/resampler.cpp @@ -58,7 +58,10 @@ uint32_t synthLib::Resampler::process(TAudioOutputs& _output, const uint32_t _nu uint32_t synthLib::Resampler::processResample(const TAudioOutputs& _output, const uint32_t _numChannels, const uint32_t _numSamples, const TProcessFunc& _processFunc) { - const uint32_t inputLen = std::max(1, dsp56k::round_int(static_cast<float>(_numSamples) * m_factorInToOut)); + // we need to preserve a constant, properly rounded, input length so accumulate the total number of samples we need and subtract the amount we use in this frame + m_inputLen += static_cast<double>(_numSamples) * m_factorInToOut; + const uint32_t inputLen = std::max(1, dsp56k::round_int(m_inputLen)); + m_inputLen -= inputLen; const auto availableInputLen = static_cast<uint32_t>(m_tempOutput[0].size()); diff --git a/source/synthLib/resampler.h b/source/synthLib/resampler.h @@ -39,8 +39,10 @@ namespace synthLib const float m_samplerateIn; const float m_samplerateOut; - const float m_factorInToOut; - const float m_factorOutToIn; + const double m_factorInToOut; + const double m_factorOutToIn; + + double m_inputLen = 0.0; std::vector<void*> m_resamplerOut; diff --git a/source/virusConsoleLib/consoleApp.cpp b/source/virusConsoleLib/consoleApp.cpp @@ -32,9 +32,9 @@ ConsoleApp::ConsoleApp(const std::string& _romFile) virusLib::Device::createDspInstances(dsp1, m_dsp2, m_rom); m_dsp1.reset(dsp1); - uc.reset(new Microcontroller(m_dsp1->getHDI08(), m_rom)); + uc.reset(new Microcontroller(*m_dsp1, m_rom, false)); if(m_dsp2) - uc->addHDI08(m_dsp2->getHDI08()); + uc->addDSP(*m_dsp2, false); } ConsoleApp::~ConsoleApp() @@ -356,7 +356,7 @@ void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampl { sem.wait(); proc.processBlock(blockSize); - uc->processHdi08Tx(midiEvents); + uc->readMidiOut(midiEvents); midiEvents.clear(); } diff --git a/source/virusIntegrationTest/CMakeLists.txt b/source/virusIntegrationTest/CMakeLists.txt @@ -14,3 +14,8 @@ target_sources(virusIntegrationTest PRIVATE ${SOURCES}) source_group("source" FILES ${SOURCES}) target_link_libraries(virusIntegrationTest PUBLIC virusConsoleLib) + +add_test(NAME virusIntegrationTests COMMAND ${CMAKE_COMMAND} + -DTEST_RUNNER=$<TARGET_FILE:virusIntegrationTest> + -DROOT_DIR=${CMAKE_BINARY_DIR} + -P ${CMAKE_CURRENT_SOURCE_DIR}/runTest.cmake) diff --git a/source/virusIntegrationTest/runTest.cmake b/source/virusIntegrationTest/runTest.cmake @@ -0,0 +1,14 @@ +include(${CMAKE_CURRENT_LIST_DIR}/../../scripts/rclone.cmake) + +set(TEST_DATA_DIR integrationTestsData) + +if(EXISTS ${RCLONE_CONF}) + copyDataFrom("integrationtests" ${TEST_DATA_DIR}) + + execute_process(COMMAND ${TEST_RUNNER} -folder ${TEST_DATA_DIR} COMMAND_ECHO STDOUT RESULT_VARIABLE TEST_RESULT) + if(TEST_RESULT) + message(FATAL_ERROR "Failed to execute ${TEST_RUNNER}: " ${CMD_RESULT}) + endif() +else() + message(FATAL_ERROR "rclone.conf not found at ${RCLONE_CONF}, unable to run integration tests") +endif() diff --git a/source/virusLib/CMakeLists.txt b/source/virusLib/CMakeLists.txt @@ -8,7 +8,10 @@ set(SOURCES demoplayback.cpp demoplayback.h device.cpp device.h dspSingle.cpp dspSingle.h + hdi08List.cpp hdi08List.h + hdi08MidiQueue.cpp hdi08MidiQueue.h hdi08TxParser.cpp hdi08TxParser.h + hdi08Queue.cpp hdi08Queue.h romfile.cpp romfile.h microcontroller.cpp microcontroller.h microcontrollerTypes.cpp microcontrollerTypes.h diff --git a/source/virusLib/device.cpp b/source/virusLib/device.cpp @@ -21,10 +21,10 @@ namespace virusLib onAudioWritten(); }, 0); - m_mc.reset(new Microcontroller(m_dsp->getHDI08(), _rom)); + m_mc.reset(new Microcontroller(*m_dsp, _rom, false)); if(m_dsp2) - m_mc->addHDI08(m_dsp2->getHDI08()); + m_mc->addDSP(*m_dsp2, true); auto loader = bootDSP(*m_dsp, m_rom, _createDebugger); @@ -136,7 +136,7 @@ namespace virusLib void Device::readMidiOut(std::vector<synthLib::SMidiEvent>& _midiOut) { - m_mc->processHdi08Tx(_midiOut); + m_mc->readMidiOut(_midiOut); } void Device::processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _samples) @@ -170,11 +170,8 @@ namespace virusLib void Device::onAudioWritten() { + m_mc->getMidiQueue(0).onAudioWritten(); m_mc->process(1); - - m_numSamplesWritten += 1; - - m_mc->sendPendingMidiEvents(m_numSamplesWritten >> 1); } void Device::configureDSP(DspSingle& _dsp, const ROMFile& _rom) diff --git a/source/virusLib/device.h b/source/virusLib/device.h @@ -45,7 +45,6 @@ namespace virusLib DspSingle* m_dsp2 = nullptr; std::unique_ptr<Microcontroller> m_mc; - uint32_t m_numSamplesWritten = 0; uint32_t m_numSamplesProcessed = 0; }; } diff --git a/source/virusLib/hdi08List.cpp b/source/virusLib/hdi08List.cpp @@ -0,0 +1,37 @@ +#include "hdi08List.h" + +namespace virusLib +{ + void Hdi08List::addHDI08(dsp56k::HDI08& _hdi08) + { + m_queues.emplace_back(_hdi08); + } + + bool Hdi08List::rxEmpty() const + { + for (const auto& h : m_queues) + { + if(!h.rxEmpty()) + return false; + } + return true; + } + + void Hdi08List::exec() + { + for (auto& h : m_queues) + h.exec(); + } + + void Hdi08List::writeRX(const dsp56k::TWord* _buf, size_t _length) + { + for (auto& h : m_queues) + h.writeRX(_buf, _length); + } + + void Hdi08List::writeHostFlags(uint8_t _flag0, uint8_t _flag1) + { + for (auto& h : m_queues) + h.writeHostFlags(_flag0, _flag1); + } +} diff --git a/source/virusLib/hdi08List.h b/source/virusLib/hdi08List.h @@ -0,0 +1,37 @@ +#pragma once + +#include <vector> + +#include "hdi08Queue.h" + +namespace virusLib +{ + class Hdi08List + { + public: + Hdi08List() + { + m_queues.reserve(2); + } + + void addHDI08(dsp56k::HDI08& _hdi08); + bool rxEmpty() const; + void exec(); + size_t size() const { return m_queues.size(); } + + dsp56k::HDI08& getHDI08(size_t _index) { return getQueue(_index).get(); } + Hdi08Queue& getQueue(size_t _index) { return m_queues[_index]; } + + void writeRX(const dsp56k::TWord* _buf, size_t _length); + + void writeRX(const std::vector<dsp56k::TWord>& _data) + { + writeRX(&_data[0], _data.size()); + } + + void writeHostFlags(uint8_t _flag0, uint8_t _flag1); + + private: + std::vector<Hdi08Queue> m_queues; + }; +} diff --git a/source/virusLib/hdi08MidiQueue.cpp b/source/virusLib/hdi08MidiQueue.cpp @@ -0,0 +1,76 @@ +#include "hdi08MidiQueue.h" + +#include "dspSingle.h" +#include "hdi08Queue.h" + +#include "dsp56kEmu/types.h" + +namespace virusLib +{ + Hdi08MidiQueue::Hdi08MidiQueue(DspSingle& _dsp, Hdi08Queue& _output, const bool _useEsaiBasedTiming) : m_output(_output), m_esai(_dsp.getPeriphX().getEsai()), m_useEsaiBasedTiming(_useEsaiBasedTiming) + { + if(_useEsaiBasedTiming) + { + m_esai.setCallback([this](dsp56k::Audio*) + { + onAudioWritten(); + }, 0); + } + } + + Hdi08MidiQueue::~Hdi08MidiQueue() + { + if(m_useEsaiBasedTiming) + m_esai.setCallback(nullptr, 0); + } + + void Hdi08MidiQueue::sendPendingMidiEvents(uint32_t _maxOffset) + { + while(!m_pendingMidiEvents.empty() && m_pendingMidiEvents.front().offset <= _maxOffset) + { + const auto& ev = m_pendingMidiEvents.front(); + + sendMidiToDSP(ev.a, ev.b, ev.c); + + m_pendingMidiEvents.pop_front(); + } + } + + void Hdi08MidiQueue::add(const synthLib::SMidiEvent& ev) + { + m_pendingMidiEvents.push_back(ev); + } + + void Hdi08MidiQueue::sendMidiToDSP(uint8_t _a, const uint8_t _b, const uint8_t _c) const + { + m_output.writeHostFlags(1, 1); + + auto sendMIDItoDSP = [this](const uint8_t _midiByte) + { + const dsp56k::TWord word = static_cast<dsp56k::TWord>(_midiByte) << 16; + m_output.writeRX(&word, 1); + }; + + const auto command = (_a & 0xf0); + + if(command == 0xf0) + { + // single-byte status message + sendMIDItoDSP(_a); + } + else + { + sendMIDItoDSP(_a); + sendMIDItoDSP(_b); + + if(command != synthLib::M_AFTERTOUCH) + sendMIDItoDSP(_c); + } + } + + void Hdi08MidiQueue::onAudioWritten() + { + ++m_numSamplesWritten; + sendPendingMidiEvents(m_numSamplesWritten >> 1); + } +} diff --git a/source/virusLib/hdi08MidiQueue.h b/source/virusLib/hdi08MidiQueue.h @@ -0,0 +1,50 @@ +#pragma once + +#include "dsp56kEmu/ringbuffer.h" + +#include "../synthLib/midiTypes.h" + +namespace dsp56k +{ + class Esai; +} + +namespace synthLib +{ + struct SMidiEvent; +} + +namespace virusLib +{ + class Hdi08Queue; + class DspSingle; + + class Hdi08MidiQueue + { + public: + explicit Hdi08MidiQueue(DspSingle& _dsp, Hdi08Queue& _output, bool _useEsaiBasedTiming); + explicit Hdi08MidiQueue(Hdi08MidiQueue&& _s) noexcept : m_output(_s.m_output), m_esai(_s.m_esai), m_useEsaiBasedTiming(_s.m_useEsaiBasedTiming) + { + assert(_s.m_pendingMidiEvents.empty()); + _s.m_useEsaiBasedTiming = false; + } + ~Hdi08MidiQueue(); + + void sendPendingMidiEvents(uint32_t _maxOffset); + + void add(const synthLib::SMidiEvent& ev); + + void onAudioWritten(); + + private: + void sendMidiToDSP(uint8_t _a, uint8_t _b, uint8_t _c) const; + + Hdi08Queue& m_output; + dsp56k::Esai& m_esai; + bool m_useEsaiBasedTiming; + + dsp56k::RingBuffer<synthLib::SMidiEvent, 1024, false> m_pendingMidiEvents; + + uint32_t m_numSamplesWritten = 0; + }; +} diff --git a/source/virusLib/hdi08Queue.cpp b/source/virusLib/hdi08Queue.cpp @@ -0,0 +1,103 @@ +#include "hdi08Queue.h" + +#include "dsp56kEmu/hdi08.h" + +namespace virusLib +{ + Hdi08Queue::Hdi08Queue(dsp56k::HDI08& _hdi08) : m_hdi08(_hdi08) + { + } + + void Hdi08Queue::writeRX(const std::vector<dsp56k::TWord>& _data) + { + if(_data.empty()) + return; + + writeRX(&_data.front(), _data.size()); + } + + void Hdi08Queue::writeRX(const dsp56k::TWord* _data, size_t _count) + { + if(_count == 0 || !_data) + return; + + std::lock_guard lock(m_mutex); + + m_dataRX.push_back(_data[0] | m_nextHostFlags); + m_nextHostFlags = 0; + + for(size_t i=1; i<_count; ++i) + m_dataRX.push_back(_data[i]); + + sendPendingData(); + } + + void Hdi08Queue::writeHostFlags(uint8_t _flag0, uint8_t _flag1) + { + std::lock_guard lock(m_mutex); + + if(m_lastHostFlag0 == _flag0 && m_lastHostFlag1 == _flag1) + return; + + m_lastHostFlag0 = _flag0; + m_lastHostFlag1 = _flag1; + + m_nextHostFlags |= static_cast<dsp56k::TWord>(_flag0) << 24; + m_nextHostFlags |= static_cast<dsp56k::TWord>(_flag1) << 25; + m_nextHostFlags |= 0x80000000; + } + + void Hdi08Queue::exec() + { + std::lock_guard lock(m_mutex); + + sendPendingData(); + } + + bool Hdi08Queue::rxEmpty() const + { + std::lock_guard lock(m_mutex); + + if(!m_dataRX.empty()) + return false; + + if(m_hdi08.hasRXData()) + return false; + return true; + } + + bool Hdi08Queue::rxFull() const + { + return m_hdi08.dataRXFull(); + } + + bool Hdi08Queue::needsToWaitforHostFlags(uint8_t _flag0, uint8_t _flag1) const + { + return m_hdi08.needsToWaitForHostFlags(_flag0, _flag1); + } + + void Hdi08Queue::sendPendingData() + { + while(!m_dataRX.empty() && !rxFull()) + { + auto d = m_dataRX.front(); + + if(d & 0x80000000) + { + const auto hostFlag0 = static_cast<uint8_t>((d >> 24) & 1); + const auto hostFlag1 = static_cast<uint8_t>((d >> 25) & 1); + + if(needsToWaitforHostFlags(hostFlag0, hostFlag1)) + break; + + m_hdi08.setHostFlagsWithWait(hostFlag0, hostFlag1); + + d &= 0xffffff; + } + + m_hdi08.writeRX(&d, 1); + + m_dataRX.pop_front(); + } + } +} diff --git a/source/virusLib/hdi08Queue.h b/source/virusLib/hdi08Queue.h @@ -0,0 +1,58 @@ +#pragma once + +#include <vector> +#include <deque> +#include <mutex> + +#include "dsp56kEmu/types.h" + +namespace dsp56k +{ + class HDI08; +} + +namespace virusLib +{ + class Hdi08Queue + { + public: + Hdi08Queue(dsp56k::HDI08& _hdi08); + Hdi08Queue(Hdi08Queue&& _s) noexcept + : m_hdi08(_s.m_hdi08) + , m_dataRX(std::move(_s.m_dataRX)) + , m_lastHostFlag0(_s.m_lastHostFlag0) + , m_lastHostFlag1(_s.m_lastHostFlag1) + , m_nextHostFlags(_s.m_nextHostFlags) + { + } + Hdi08Queue(const Hdi08Queue&) = delete; + + void writeRX(const std::vector<dsp56k::TWord>& _data); + void writeRX(const dsp56k::TWord* _data, size_t _count); + + void writeHostFlags(uint8_t _flag0, uint8_t _flag1); + + void exec(); + + bool rxEmpty() const; + + dsp56k::HDI08& get() const { return m_hdi08; } + + private: + bool rxFull() const; + bool needsToWaitforHostFlags(uint8_t _flag0, uint8_t _flag1) const; + void sendPendingData(); + + static constexpr uint8_t HostFlagInvalid = 0xff; + + dsp56k::HDI08& m_hdi08; + std::deque<dsp56k::TWord> m_dataRX; + + uint8_t m_lastHostFlag0 = HostFlagInvalid; + uint8_t m_lastHostFlag1 = HostFlagInvalid; + + uint32_t m_nextHostFlags = 0; + + mutable std::mutex m_mutex; + }; +} diff --git a/source/virusLib/hdi08TxParser.cpp b/source/virusLib/hdi08TxParser.cpp @@ -41,6 +41,8 @@ namespace virusLib m_state = State::Preset; LOG("Begin receiving upgraded preset"); + m_presetData.clear(); + if(m_remainingPresetBytes == 0) { m_remainingPresetBytes = std::numeric_limits<uint32_t>::max(); @@ -99,10 +101,11 @@ namespace virusLib if(!matched) { - std::stringstream s; +/* std::stringstream s; for (const auto& w : m_nonPatternWords) s << HEX(w) << ' '; LOG("Unknown DSP words: " << s.str()); +*/ m_nonPatternWords.clear(); } } @@ -189,4 +192,10 @@ namespace virusLib { m_remainingPresetBytes = _byteCount; } + + void Hdi08TxParser::getPresetData(std::vector<uint8_t>& _data) + { + std::swap(m_presetData, _data); + m_presetData.clear(); + } } diff --git a/source/virusLib/hdi08TxParser.h b/source/virusLib/hdi08TxParser.h @@ -47,6 +47,8 @@ namespace virusLib return m_dspHasBooted; } + void getPresetData(std::vector<uint8_t>& _data); + private: Microcontroller& m_mc; diff --git a/source/virusLib/microcontroller.cpp b/source/virusLib/microcontroller.cpp @@ -5,6 +5,7 @@ #include "microcontroller.h" +#include "dspSingle.h" #include "../synthLib/midiTypes.h" using namespace dsp56k; @@ -39,15 +40,15 @@ constexpr uint8_t namespace virusLib { -Microcontroller::Microcontroller(HDI08& _hdi08, const ROMFile& _romFile) : m_rom(_romFile) +Microcontroller::Microcontroller(DspSingle& _dsp, const ROMFile& _romFile, bool _useEsaiBasedMidiTiming) : m_rom(_romFile) { if(!_romFile.isValid()) return; - m_hdi08.addHDI08(_hdi08); - m_hdi08TxParsers.reserve(2); - m_hdi08TxParsers.emplace_back(*this); + m_midiQueues.reserve(2); + + addDSP(_dsp, _useEsaiBasedMidiTiming); m_globalSettings.fill(0xffffffff); @@ -68,7 +69,8 @@ Microcontroller::Microcontroller(HDI08& _hdi08, const ROMFile& _romFile) : m_rom for(uint32_t p=0; p<m_rom.getPresetsPerBank(); ++p) { TPreset single; - m_rom.getSingle(bank, p, single); + if(!m_rom.getSingle(bank, p, single)) + break; if(ROMFile::getSingleName(single).size() != 10) { @@ -95,6 +97,8 @@ Microcontroller::Microcontroller(HDI08& _hdi08, const ROMFile& _romFile) : m_rom m_singleEditBuffers[i] = singles[i]; } } + + m_pendingSysexInput.reserve(64); } void Microcontroller::sendInitControlCommands() @@ -163,6 +167,8 @@ bool Microcontroller::sendPreset(const uint8_t program, const std::vector<TWord> return true; } + receiveUpgradedPreset(); + writeHostBitsWithWait(0,1); // Send header TWord buf[] = {0xf47555, static_cast<TWord>(isMulti ? 0x110000 : 0x100000)}; @@ -176,6 +182,9 @@ bool Microcontroller::sendPreset(const uint8_t program, const std::vector<TWord> for (auto& parser : m_hdi08TxParsers) parser.waitForPreset(isMulti ? m_rom.getMultiPresetSize() : m_rom.getSinglePresetSize()); + m_sentPresetProgram = program; + m_sentPresetIsMulti = isMulti; + return true; } @@ -243,7 +252,9 @@ bool Microcontroller::sendMIDI(const SMidiEvent& _ev) break; } - m_pendingMidiEvents.push_back(_ev); + for (auto& midiQueue : m_midiQueues) + midiQueue.add(_ev); + return true; } @@ -444,6 +455,12 @@ bool Microcontroller::sendSysex(const std::vector<uint8_t>& _data, std::vector<S }; + auto enqueue = [&]() + { + m_pendingSysexInput.emplace_back(std::make_pair(_source, _data)); + return false; + }; + switch (cmd) { case DUMP_SINGLE: @@ -468,6 +485,8 @@ bool Microcontroller::sendSysex(const std::vector<uint8_t>& _data, std::vector<S case REQUEST_SINGLE: { const auto bank = fromMidiByte(_data[7]); + if(!m_pendingPresetWrites.empty() || bank == BankNumber::EditBuffer && waitingForPresetReceiveConfirmation()) + return enqueue(); const uint8_t program = _data[8]; LOG("Request Single, Bank " << (int)toMidiByte(bank) << ", program " << (int)program); buildSingleResponse(bank, program); @@ -476,6 +495,8 @@ bool Microcontroller::sendSysex(const std::vector<uint8_t>& _data, std::vector<S case REQUEST_MULTI: { const auto bank = fromMidiByte(_data[7]); + if(!m_pendingPresetWrites.empty() || bank == BankNumber::EditBuffer && waitingForPresetReceiveConfirmation()) + return enqueue(); const uint8_t program = _data[8]; LOG("Request Multi, Bank " << (int)bank << ", program " << (int)program); buildMultiResponse(bank, program); @@ -507,6 +528,8 @@ bool Microcontroller::sendSysex(const std::vector<uint8_t>& _data, std::vector<S buildTotalResponse(); break; case REQUEST_ARRANGEMENT: + if(!m_pendingPresetWrites.empty() || waitingForPresetReceiveConfirmation()) + return enqueue(); buildArrangementResponse(); break; case PAGE_A: @@ -655,10 +678,12 @@ bool Microcontroller::getSingle(BankNumber _bank, uint32_t _preset, TPreset& _re return true; } -bool Microcontroller::requestMulti(BankNumber _bank, uint8_t _program, TPreset& _data) const +bool Microcontroller::requestMulti(BankNumber _bank, uint8_t _program, TPreset& _data) { if (_bank == BankNumber::EditBuffer) { + receiveUpgradedPreset(); + // Use multi-edit buffer _data = m_multiEditBuffer; return true; @@ -672,10 +697,12 @@ bool Microcontroller::requestMulti(BankNumber _bank, uint8_t _program, TPreset& return true; } -bool Microcontroller::requestSingle(BankNumber _bank, uint8_t _program, TPreset& _data) const +bool Microcontroller::requestSingle(BankNumber _bank, uint8_t _program, TPreset& _data) { if (_bank == BankNumber::EditBuffer) { + receiveUpgradedPreset(); + // Use single-edit buffer if(_program == SINGLE) _data = m_singleEditBuffer; @@ -707,15 +734,22 @@ bool Microcontroller::writeSingle(BankNumber _bank, uint8_t _program, const TPre } if(_program == SINGLE) + { + m_globalSettings[PLAY_MODE] = PlayModeSingle; + m_singleEditBuffer = _data; + } + else if(_program >= m_singleEditBuffers.size()) + { + return false; + } else - m_singleEditBuffers[_program % m_singleEditBuffers.size()] = _data; + { + m_singleEditBuffers[_program] = _data; + } LOG("Loading Single " << ROMFile::getSingleName(_data) << " to part " << static_cast<int>(_program)); - if(_program == SINGLE) - m_globalSettings[PLAY_MODE] = PlayModeSingle; - // Send to DSP return sendPreset(_program, presetToDSPWords(_data, false), false); } @@ -839,7 +873,7 @@ void Microcontroller::process(size_t _size) bool Microcontroller::getState(std::vector<unsigned char>& _state, const StateType _type) { - const uint8_t deviceId = static_cast<uint8_t>(m_globalSettings[DEVICE_ID]); + const auto deviceId = static_cast<uint8_t>(m_globalSettings[DEVICE_ID]); std::vector<SMidiEvent> responses; @@ -903,74 +937,23 @@ bool Microcontroller::setState(const std::vector<unsigned char>& _state, const S return true; } -bool Microcontroller::sendMIDItoDSP(uint8_t _a, const uint8_t _b, const uint8_t _c) +void Microcontroller::addDSP(DspSingle& _dsp, bool _useEsaiBasedMidiTiming) { - std::lock_guard lock(m_mutex); - - writeHostBitsWithWait(1, 1); - - auto sendMIDItoDSP = [this](const uint8_t _midiByte) - { - const TWord word = static_cast<TWord>(_midiByte) << 16; - m_hdi08.writeRX(&word, 1); - }; - - const auto command = (_a & 0xf0); - - if(command == 0xf0) - { - // single-byte status message - sendMIDItoDSP(_a); - } - else - { - sendMIDItoDSP(_a); - sendMIDItoDSP(_b); - - if(command != M_AFTERTOUCH) - sendMIDItoDSP(_c); - } - - return true; -} - -void Microcontroller::sendPendingMidiEvents(const uint32_t _maxOffset) -{ - auto size = m_pendingMidiEvents.size(); - - if(!size) - return; - - while(!m_pendingMidiEvents.empty() && m_pendingMidiEvents.front().offset <= _maxOffset) - { - const auto& ev = m_pendingMidiEvents.front(); - - if(!sendMIDItoDSP(ev.a,ev.b,ev.c)) - break; - - m_pendingMidiEvents.pop_front(); - --size; - } -} - -void Microcontroller::addHDI08(dsp56k::HDI08& _hdi08) -{ - m_hdi08.addHDI08(_hdi08); + m_hdi08.addHDI08(_dsp.getHDI08()); m_hdi08TxParsers.emplace_back(*this); + m_midiQueues.emplace_back(_dsp, m_hdi08.getQueue(m_hdi08.size()-1), _useEsaiBasedMidiTiming); } void Microcontroller::processHdi08Tx(std::vector<synthLib::SMidiEvent>& _midiEvents) { - std::lock_guard lock(m_mutex); - for(size_t i=0; i<m_hdi08.size(); ++i) { - auto* hdi08 = m_hdi08.get(i); + auto& hdi08 = m_hdi08.getHDI08(i); auto& parser = m_hdi08TxParsers[i]; - while(hdi08->hasTX()) + while(hdi08.hasTX()) { - if(parser.append(hdi08->readTX())) + if(parser.append(hdi08.readTX())) { const auto midi = parser.getMidiData(); if(i == 0) @@ -981,6 +964,26 @@ void Microcontroller::processHdi08Tx(std::vector<synthLib::SMidiEvent>& _midiEve } } +void Microcontroller::readMidiOut(std::vector<synthLib::SMidiEvent>& _midiOut) +{ + std::lock_guard lock(m_mutex); + processHdi08Tx(_midiOut); + + if (m_pendingSysexInput.empty() || !m_pendingPresetWrites.empty() || waitingForPresetReceiveConfirmation()) + return; + + for (const auto& input : m_pendingSysexInput) + sendSysex(input.second, _midiOut, input.first); + + m_pendingSysexInput.clear(); +} + +void Microcontroller::sendPendingMidiEvents(const uint32_t _maxOffset) +{ + for (auto& midiQueue : m_midiQueues) + midiQueue.sendPendingMidiEvents(_maxOffset); +} + PresetVersion Microcontroller::getPresetVersion(const TPreset& _preset) { return getPresetVersion(_preset[0]); @@ -1030,9 +1033,10 @@ void Microcontroller::applyToSingleEditBuffer(TPreset& _single, const Page _page uint32_t offset; switch(_page) { - default: case PAGE_A: offset = 0; break; case PAGE_B: offset = 1; break; + default: + return; } offset *= paramsPerPage; @@ -1082,4 +1086,39 @@ bool Microcontroller::waitingForPresetReceiveConfirmation() const } return false; } + +void Microcontroller::receiveUpgradedPreset() +{ + const auto waitingForPresetConfirmation = waitingForPresetReceiveConfirmation(); + + if(waitingForPresetConfirmation) + return; + + std::vector<uint8_t> upgradedPreset; + m_hdi08TxParsers.front().getPresetData(upgradedPreset); + + if(upgradedPreset.empty()) + return; + + LOG("Replacing edit buffer for " << (m_sentPresetIsMulti ? "multi" : "single") << " program " << static_cast<int>(m_sentPresetProgram) << " with upgraded preset"); + + auto copyTo = [&upgradedPreset, this](TPreset& _preset) + { + std::copy(upgradedPreset.begin(), upgradedPreset.end(), _preset.begin()); + }; + + if(m_sentPresetIsMulti) + { + copyTo(m_multiEditBuffer); + } + else if(m_sentPresetProgram == SINGLE) + { + copyTo(m_singleEditBuffer); + } + else if(m_sentPresetProgram < m_singleEditBuffers.size()) + { + copyTo(m_singleEditBuffers[m_sentPresetProgram]); + } } + +} +\ No newline at end of file diff --git a/source/virusLib/microcontroller.h b/source/virusLib/microcontroller.h @@ -1,9 +1,5 @@ #pragma once -#include "../dsp56300/source/dsp56kEmu/dsp.h" -#include "../dsp56300/source/dsp56kEmu/hdi08.h" -#include "../dsp56300/source/dsp56kEmu/hdi08queue.h" - #include "romfile.h" #include "../synthLib/deviceTypes.h" @@ -12,12 +8,15 @@ #include <list> #include <mutex> +#include "hdi08List.h" +#include "hdi08MidiQueue.h" #include "hdi08TxParser.h" #include "microcontrollerTypes.h" namespace virusLib { -class DemoPlayback; + class DspSingle; + class DemoPlayback; class Microcontroller { @@ -25,15 +24,15 @@ class Microcontroller public: using TPreset = ROMFile::TPreset; - explicit Microcontroller(dsp56k::HDI08& hdi08, const ROMFile& romFile); + explicit Microcontroller(DspSingle& _dsp, const ROMFile& romFile, bool _useEsaiBasedMidiTiming); bool sendMIDI(const synthLib::SMidiEvent& _ev); bool sendSysex(const std::vector<uint8_t>& _data, std::vector<synthLib::SMidiEvent>& _responses, synthLib::MidiEventSource _source); bool writeSingle(BankNumber _bank, uint8_t _program, const TPreset& _data); bool writeMulti(BankNumber _bank, uint8_t _program, const TPreset& _data); - bool requestMulti(BankNumber _bank, uint8_t _program, TPreset& _data) const; - bool requestSingle(BankNumber _bank, uint8_t _program, TPreset& _data) const; + bool requestMulti(BankNumber _bank, uint8_t _program, TPreset& _data); + bool requestSingle(BankNumber _bank, uint8_t _program, TPreset& _data); void sendInitControlCommands(); @@ -43,13 +42,11 @@ public: bool getState(std::vector<unsigned char>& _state, synthLib::StateType _type); bool setState(const std::vector<unsigned char>& _state, synthLib::StateType _type); - bool sendMIDItoDSP(uint8_t _a, const uint8_t _b, const uint8_t _c); + void addDSP(DspSingle& _dsp, bool _useEsaiBasedMidiTiming); + void readMidiOut(std::vector<synthLib::SMidiEvent>& _midiOut); void sendPendingMidiEvents(uint32_t _maxOffset); - - void addHDI08(dsp56k::HDI08& _hdi08); - - void processHdi08Tx(std::vector<synthLib::SMidiEvent>& _midiEvents); + Hdi08MidiQueue& getMidiQueue(size_t _index) { return m_midiQueues[_index]; } static PresetVersion getPresetVersion(const TPreset& _preset); static PresetVersion getPresetVersion(uint8_t _versionCode); @@ -80,10 +77,13 @@ private: void applyToMultiEditBuffer(uint8_t _part, uint8_t _param, uint8_t _value); Page globalSettingsPage() const; bool isPageSupported(Page _page) const; + void processHdi08Tx(std::vector<synthLib::SMidiEvent>& _midiEvents); bool waitingForPresetReceiveConfirmation() const; + void receiveUpgradedPreset(); - dsp56k::HDI08Queue m_hdi08; + Hdi08List m_hdi08; std::vector<Hdi08TxParser> m_hdi08TxParsers; + std::vector<Hdi08MidiQueue> m_midiQueues; const ROMFile& m_rom; @@ -101,6 +101,9 @@ private: uint8_t m_currentBank = 0; uint8_t m_currentSingle = 0; + uint8_t m_sentPresetProgram = 0; + bool m_sentPresetIsMulti = false; + // Device does not like if we send everything at once, therefore we delay the send of Singles after sending a Multi struct SPendingPresetWrite { @@ -111,7 +114,9 @@ private: std::list<SPendingPresetWrite> m_pendingPresetWrites; - dsp56k::RingBuffer<synthLib::SMidiEvent, 1024, false> m_pendingMidiEvents; + std::vector<std::pair<synthLib::MidiEventSource, std::vector<uint8_t>>> m_pendingSysexInput; + std::vector<synthLib::SMidiEvent> m_midiOutput; + mutable std::recursive_mutex m_mutex; bool m_loadingState = false; }; diff --git a/source/virusLib/romfile.h b/source/virusLib/romfile.h @@ -2,6 +2,7 @@ #include <thread> #include <vector> +#include <string> #include "dsp56kEmu/types.h"