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 c8b78356077ca0f5a509afd184571d4141ea19c1
parent 58a00a7078b046070f399edc51b76d330de4a631
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Sat,  2 Mar 2024 01:12:15 +0100

update open source version

Diffstat:
M.gitignore | 2++
MCMakeLists.txt | 26++++++++++++++++++--------
Mbase.cmake | 19++++++++++++-------
Mbuild_linux_wsl.sh | 4++--
Mbuild_win64_vs19.bat | 2+-
Adoc/CMakeLists.txt | 3+++
Ddoc/VirusEmulator.tdl | 0
Mdoc/changelog.txt | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePlugin/CMakeLists.txt | 6++++--
Msource/jucePlugin/PluginEditorState.cpp | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePlugin/PluginEditorState.h | 3+++
Msource/jucePlugin/PluginProcessor.cpp | 16++++++++--------
Msource/jucePlugin/PluginProcessor.h | 2+-
Msource/jucePlugin/VirusController.cpp | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msource/jucePlugin/VirusController.h | 19+++++++++++++------
Msource/jucePlugin/parameterDescriptions_C.json | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msource/jucePlugin/skins/Galaxpel/VirusC_Galaxpel.json | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msource/jucePlugin/skins/Hoverland/VirusC_Hoverland.json | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msource/jucePlugin/skins/Trancy/VirusC_Trancy.json | 28+++++-----------------------
Asource/jucePlugin/ui3/PartButton.cpp | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePlugin/ui3/PartButton.h | 27+++++++++++++++++++++++++++
Msource/jucePlugin/ui3/Parts.cpp | 78+++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msource/jucePlugin/ui3/Parts.h | 26++++++++++++++++++++++----
Dsource/jucePlugin/ui3/PatchBrowser.cpp | 243-------------------------------------------------------------------------------
Dsource/jucePlugin/ui3/PatchBrowser.h | 53-----------------------------------------------------
Asource/jucePlugin/ui3/PatchManager.cpp | 444+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePlugin/ui3/PatchManager.h | 44++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePlugin/ui3/VirusEditor.cpp | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msource/jucePlugin/ui3/VirusEditor.h | 19++++++++++---------
Msource/jucePluginEditorLib/CMakeLists.txt | 32++++++++++++++++++++++++++++++--
Msource/jucePluginEditorLib/focusedParameter.h | 7+++++++
Msource/jucePluginEditorLib/focusedParameterTooltip.cpp | 2++
Msource/jucePluginEditorLib/focusedParameterTooltip.h | 7++++++-
Asource/jucePluginEditorLib/partbutton.cpp | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/partbutton.h | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsource/jucePluginEditorLib/patchbrowser.cpp | 380-------------------------------------------------------------------------------
Dsource/jucePluginEditorLib/patchbrowser.h | 99-------------------------------------------------------------------------------
Asource/jucePluginEditorLib/patchmanager/datasourcetree.cpp | 12++++++++++++
Asource/jucePluginEditorLib/patchmanager/datasourcetree.h | 12++++++++++++
Asource/jucePluginEditorLib/patchmanager/datasourcetreeitem.cpp | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/datasourcetreeitem.h | 40++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/defaultskin.h | 19+++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/editable.cpp | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/editable.h | 31+++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/grouptreeitem.cpp | 339+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/grouptreeitem.h | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/info.cpp | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/info.h | 46++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/list.cpp | 581+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/list.h | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/listitem.cpp | 195+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/listitem.h | 44++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/notagtreeitem.cpp | 17+++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/notagtreeitem.h | 17+++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/patchmanager.cpp | 686+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/patchmanager.h | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/resizerbar.cpp | 24++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/resizerbar.h | 19+++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/roottreeitem.cpp | 8++++++++
Asource/jucePluginEditorLib/patchmanager/roottreeitem.h | 21+++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/savepatchdesc.cpp | 0
Asource/jucePluginEditorLib/patchmanager/savepatchdesc.h | 24++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/search.cpp | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/search.h | 24++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/searchlist.cpp | 15+++++++++++++++
Asource/jucePluginEditorLib/patchmanager/searchlist.h | 19+++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/searchtree.cpp | 15+++++++++++++++
Asource/jucePluginEditorLib/patchmanager/searchtree.h | 19+++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/state.cpp | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/state.h | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/status.cpp | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/status.h | 23+++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/tagstree.cpp | 30++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/tagstree.h | 20++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/tagtreeitem.cpp | 118+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/tagtreeitem.h | 34++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/tree.cpp | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/tree.h | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/treeitem.cpp | 270+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/treeitem.h | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/types.cpp | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/patchmanager/types.h | 30++++++++++++++++++++++++++++++
Msource/jucePluginEditorLib/pluginEditor.cpp | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msource/jucePluginEditorLib/pluginEditor.h | 43+++++++++++++++++++++++++++++++++++++------
Msource/jucePluginEditorLib/pluginEditorState.cpp | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msource/jucePluginEditorLib/pluginEditorState.h | 20++++++++++++++++----
Msource/jucePluginEditorLib/pluginEditorWindow.cpp | 17++++++++++++++---
Msource/jucePluginEditorLib/pluginProcessor.cpp | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePluginEditorLib/pluginProcessor.h | 14++++++++++++++
Asource/jucePluginEditorLib/types.h | 10++++++++++
Msource/jucePluginLib/CMakeLists.txt | 18+++++++++++++++++-
Msource/jucePluginLib/controller.cpp | 131++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msource/jucePluginLib/controller.h | 23++++++++++++++++++++++-
Msource/jucePluginLib/dummydevice.h | 6++++++
Msource/jucePluginLib/midipacket.cpp | 40+++++++++++++++++++++++++++++++++++++---
Msource/jucePluginLib/midipacket.h | 11++++++++++-
Msource/jucePluginLib/parameter.cpp | 29+++++++++++++++++++----------
Msource/jucePluginLib/parameter.h | 7++++---
Msource/jucePluginLib/parameterbinding.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msource/jucePluginLib/parameterbinding.h | 6+++++-
Msource/jucePluginLib/parameterdescription.h | 3++-
Msource/jucePluginLib/parameterdescriptions.cpp | 118++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msource/jucePluginLib/parameterdescriptions.h | 17++++++++++++++++-
Asource/jucePluginLib/parameterregion.cpp | 8++++++++
Asource/jucePluginLib/parameterregion.h | 23+++++++++++++++++++++++
Asource/jucePluginLib/patchdb/datasource.cpp | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/datasource.h | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/db.cpp | 1635+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/db.h | 165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/jobqueue.cpp | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/jobqueue.h | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/patch.cpp | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/patch.h | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/patchdbtypes.cpp | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/patchdbtypes.h | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/patchhistory.cpp | 0
Asource/jucePluginLib/patchdb/patchhistory.h | 13+++++++++++++
Asource/jucePluginLib/patchdb/patchmodifications.cpp | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/patchmodifications.h | 30++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/search.cpp | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/search.h | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/serialization.cpp | 24++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/serialization.h | 13+++++++++++++
Asource/jucePluginLib/patchdb/tags.cpp | 213+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/patchdb/tags.h | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePluginLib/processor.cpp | 113++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msource/jucePluginLib/processor.h | 41+++++++++++++++++++++++++++++++++++++++--
Asource/jucePluginLib/types.h | 8++++++++
Msource/juceUiLib/CMakeLists.txt | 9+++++++--
Asource/juceUiLib/button.cpp | 0
Asource/juceUiLib/button.h | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/juceUiLib/editor.cpp | 22++++++++++++++++++++++
Msource/juceUiLib/editor.h | 24++++++++++++++++++++++--
Msource/juceUiLib/hyperlinkbuttonStyle.cpp | 3+++
Msource/juceUiLib/labelStyle.cpp | 3++-
Asource/juceUiLib/listBoxStyle.cpp | 11+++++++++++
Asource/juceUiLib/listBoxStyle.h | 15+++++++++++++++
Asource/juceUiLib/scrollbarStyle.cpp | 17+++++++++++++++++
Asource/juceUiLib/scrollbarStyle.h | 14++++++++++++++
Asource/juceUiLib/textEditorStyle.cpp | 15+++++++++++++++
Asource/juceUiLib/textEditorStyle.h | 15+++++++++++++++
Msource/juceUiLib/textbuttonStyle.cpp | 7+++++++
Msource/juceUiLib/textbuttonStyle.h | 1+
Asource/juceUiLib/treeViewStyle.cpp | 14++++++++++++++
Asource/juceUiLib/treeViewStyle.h | 14++++++++++++++
Msource/juceUiLib/uiObject.cpp | 111++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msource/juceUiLib/uiObject.h | 18++++++++++++++++--
Msource/juceUiLib/uiObjectStyle.cpp | 60+++++++++++++++++++++++++++++++++++++++---------------------
Msource/juceUiLib/uiObjectStyle.h | 18++++++++++++++++++
Asource/mqJucePlugin/.gitignore | 1+
Asource/synthLib/.gitignore | 1+
Msource/synthLib/CMakeLists.txt | 5+++++
Msource/synthLib/binarystream.h | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Asource/synthLib/buildconfig.h.in | 3+++
Msource/synthLib/device.h | 13+++++++++++--
Msource/synthLib/hybridcontainer.h | 220+++++++------------------------------------------------------------------------
Msource/synthLib/midiToSysex.cpp | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msource/synthLib/midiToSysex.h | 3++-
Msource/synthLib/os.cpp | 47+++++++++++++++++++++++++++++++++++++----------
Msource/synthLib/os.h | 3+++
Msource/synthLib/plugin.cpp | 4++--
Msource/synthLib/plugin.h | 8+++++---
Msource/virusConsoleLib/audioProcessor.cpp | 4++--
Msource/virusConsoleLib/consoleApp.cpp | 11++++++-----
Msource/virusIntegrationTest/integrationTest.cpp | 173+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msource/virusLib/device.cpp | 31+++++++++++++++++++++++++++++--
Msource/virusLib/device.h | 9+++++++--
Msource/virusLib/dspSingle.h | 1+
Msource/virusLib/hdi08MidiQueue.cpp | 2+-
Msource/virusLib/hdi08TxParser.h | 5+++--
Msource/virusLib/microcontroller.cpp | 8+++++---
Msource/virusLib/microcontroller.h | 5++++-
Msource/virusLib/midiFileToRomData.cpp | 10++++++++--
Msource/virusLib/midiFileToRomData.h | 1+
Msource/virusLib/romfile.cpp | 12+++++++++---
Mtemp/cmake_win64/gearmulator.sln.DotSettings | 4++++
Axcodeversion.cmake | 20++++++++++++++++++++
177 files changed, 11260 insertions(+), 1459 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -9,3 +9,5 @@ /build_win64_VS2019.bat /out/ /build/ +*atom*.sh +*debug.sh diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -1,16 +1,25 @@ cmake_minimum_required(VERSION 3.15) # build a fat binary that runs on both intel and the new Apple M1 chip -set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "OS X Architectures") - -# Xcode 14+ can not build for anything < High Sierra anymore -if(CMAKE_GENERATOR STREQUAL Xcode AND XCODE_VERSION VERSION_GREATER_EQUAL 14.0.0) - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum OS X deployment version") -else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9" CACHE STRING "Minimum OS X deployment version") +if(APPLE) + include(xcodeversion.cmake) + + set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "OS X Architectures") + + message("CMAKE_GENERATOR: " ${CMAKE_GENERATOR}) + message("XCODE_VERSION: " ${XCODE_VERSION}) + + # Xcode 14+ can not build for anything < High Sierra anymore + if(CMAKE_GENERATOR STREQUAL Xcode AND XCODE_VERSION VERSION_GREATER_EQUAL 14.0.0) + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13" CACHE STRING "Minimum OS X deployment version") + else() + # 10.12 is now the minimum because we use std::shared_mutex in patch manager + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.12" CACHE STRING "Minimum OS X deployment version") + endif() + message("CMAKE_OSX_DEPLOYMENT_TARGET: " ${CMAKE_OSX_DEPLOYMENT_TARGET}) endif() -project(gearmulator VERSION 1.2.36) +project(gearmulator VERSION 1.3.5) include(base.cmake) include(CTest) @@ -45,6 +54,7 @@ set(CPACK_RPM_PACKAGE_DESCRIPTION ${CPACK_PACKAGE_DESCRIPTION_SUMMARY}) # ----------------- source add_subdirectory(source) +add_subdirectory(doc) # ----------------- CPack parameters based on source diff --git a/base.cmake b/base.cmake @@ -18,10 +18,11 @@ if(MSVC) # /Oi Enable Intrinsic Functions # /Ot Favor Fast Code # /permissive- Standards Conformance + # /MP Multiprocessor Compilation set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /O2 /GS- /fp:fast /Oy /GT /GL /Zi /Oi /Ot") set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /O2 /GS- /fp:fast /Oy /GT /GL /Zi /Oi /Ot") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /permissive-") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /permissive- /MP") set(ARCHITECTURE ${CMAKE_VS_PLATFORM_NAME}) @@ -55,21 +56,25 @@ elseif(APPLE) "-framework OpenGL" "-framework QuartzCore" ) - set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -funroll-loops -Ofast -flto") - set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -funroll-loops -Ofast -flto") + string(APPEND CMAKE_C_FLAGS_RELEASE " -funroll-loops -Ofast -flto") + string(APPEND CMAKE_CXX_FLAGS_RELEASE " -funroll-loops -Ofast -flto -fno-stack-protector") else() message("CMAKE_SYSTEM_PROCESSOR: " ${CMAKE_SYSTEM_PROCESSOR}) message("CMAKE_HOST_SYSTEM_PROCESSOR: " ${CMAKE_HOST_SYSTEM_PROCESSOR}) if(NOT CMAKE_SYSTEM_PROCESSOR MATCHES arm AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES aarch64) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -msse") + string(APPEND CMAKE_CXX_FLAGS " -msse") endif() - set(CMAKE_C_FLAGS_RELEASE "-Ofast") - set(CMAKE_CXX_FLAGS_RELEASE "-Ofast") - set(CMAKE_CXX_FLAGS "-Ofast") + string(APPEND CMAKE_C_FLAGS_RELEASE " -Ofast") + string(APPEND CMAKE_CXX_FLAGS_RELEASE " -Ofast -fno-stack-protector") + string(APPEND CMAKE_CXX_FLAGS_DEBUG " -rdynamic") execute_process(COMMAND uname -m COMMAND tr -d '\n' OUTPUT_VARIABLE ARCHITECTURE) endif() message( STATUS "Architecture: ${ARCHITECTURE}" ) +message( STATUS "Compiler Arguments: ${CMAKE_CXX_FLAGS}" ) +message( STATUS "Compiler Arguments (Release): ${CMAKE_CXX_FLAGS_RELEASE}" ) +message( STATUS "Compiler Arguments (Debug): ${CMAKE_CXX_FLAGS_DEBUG}" ) +message( STATUS "Build Configration: ${CMAKE_BUILD_TYPE}" ) # VST3 SDK needs these if(CMAKE_BUILD_TYPE STREQUAL "Debug") diff --git a/build_linux_wsl.sh b/build_linux_wsl.sh @@ -1,3 +1,3 @@ -cmake . -B ./temp/cmake_linux_wsl -Dgearmulator_BUILD_JUCEPLUGIN=OFF -DCMAKE_BUILD_TYPE=Release +cmake . -B ./temp/cmake_linux_wsl -Dgearmulator_BUILD_JUCEPLUGIN=ON -DCMAKE_BUILD_TYPE=Release cd ./temp/cmake_linux_wsl -cmake --build . --config Release +cmake --build . --config Release --parallel 8 diff --git a/build_win64_vs19.bat b/build_win64_vs19.bat @@ -1,5 +1,5 @@ set outdir=temp\cmake_win64\ -cmake . -B %outdir% -G "Visual Studio 16 2019" -A x64 -Dgearmulator_BUILD_FX_PLUGIN=ON +cmake . -B %outdir% -G "Visual Studio 16 2019" -A x64 -Dgearmulator_BUILD_FX_PLUGIN=ON -DDSP56300_DEBUGGER=OFF IF %ERRORLEVEL% NEQ 0 ( popd exit /B 2 diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt @@ -0,0 +1,3 @@ +project(doc) + +add_custom_target( doc SOURCES changelog.txt) 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,72 @@ Release Notes +1.3.5 (TBD) + +DSP: + +- [Imp] Reduced host memory consumption +- [Imp] Performance improvements + +Framework: + +- [Imp] Patch Manager: Add "Locate" context menu entry for patch to select the data source that contains it + +Osirus/OsTIrus: + +- [Fix] It was not possible to select a part preset via left-click menu on macOS + +1.3.4 (TBD) + +DSP: + +- [Imp] Small performance improvements when JIT compiler needs to recompile code + +Framework: + +- [Imp] Patch Manager: Add tooltip for data source to show the full path in case its cut off +- [Imp] Patch Manager: Columns can now be resized by dragging +- [Imp] The DSP can now be underclocked/overclocked via context menu between 50% and 200%. This is + an advanced setting that needs to be confirmed before activation + +- [Fix] Patch Manager: Fix subfolders not enumerated on Mac & Linux +- [Fix] Add missing .vstpreset, .fxb and .cpr file extensions to file selector when loading a patch via "Load" button +- [Fix] Parameter value change by double-click on knob to reset it was not sent to host for automation +- [Fix] Linked parameters confused Bitwig / Ableton and other hosts +- [Fix] Long plugin loading times in some hosts due to excessive initial parameter automation updates +- [Fix] Part context menu and global context menu opened at the same time on right click + +Osirus: + +- [Imp] Parameter regions can now be locked/unlocked via a context menu. Locked parameters do not change + when a preset is loaded. This is useful for example if you want to keep an Arp pattern + while searching for a preset, etc. + +Osirus/OsTIrus: + +- [Imp] Added output gain adjustment to context menu (already present in Vavra before) +- [Imp] The Midi Receive Channel of a part in Multi Mode can now be adjusted by right clicking on the part select button +- [Imp] Patch Manager: Allow to drag & drop patches from patch list to part slots +- [Imp] Patches can now be copied to other parts via drag & drop +- [Imp] Parameter regions can now be locked/unlocked via a context menu. Locked parameters do not change + when a preset is loaded. This is useful for example if you want to keep an Arp pattern + while searching for a preset, etc. + +1.3.3 (2024.02.07) + +- [Imp] Patch Manager added. The patch manager replaced the old preset browser and provides a modern + user experience to manage presets. + Introduction: https://dsp56300.wordpress.com/2024/02/06/patch-manager-introduction/ + +Osirus: + +- [Imp] Support more file formats to load presets from. All supported formats now are: + .syx/.mid Virus A/B/C/TI/TI2 preset dumps + .fxb/.vstpreset Presets saved by DAW from Virus Powercore and Virus TI Control Software + .cpr Cubase Project Files + .mid Virus A/B/C OS Update files that include Factory Presets + +- [Fix] Virus A presets from a very old firmware failed to load + 1.2.25 (2023.01.08) - [Imp] DSP56300 plugins are now also available in CLAP plugin format diff --git a/source/jucePlugin/CMakeLists.txt b/source/jucePlugin/CMakeLists.txt @@ -20,8 +20,10 @@ set(SOURCES ui3/FxPage.h ui3/Parts.cpp ui3/Parts.h - ui3/PatchBrowser.cpp - ui3/PatchBrowser.h + ui3/PatchManager.cpp + ui3/PatchManager.h + ui3/PartButton.cpp + ui3/PartButton.h ui3/Tabs.cpp ui3/Tabs.h ui3/VirusEditor.cpp diff --git a/source/jucePlugin/PluginEditorState.cpp b/source/jucePlugin/PluginEditorState.cpp @@ -22,3 +22,54 @@ genericUI::Editor* PluginEditorState::createEditor(const Skin& _skin, std::funct { return new genericVirusUI::VirusEditor(m_parameterBinding, static_cast<AudioPluginAudioProcessor&>(m_processor), _skin.jsonFilename, _skin.folder, _openMenuCallback); } + +void PluginEditorState::initContextMenu(juce::PopupMenu& _menu) +{ + jucePluginEditorLib::PluginEditorState::initContextMenu(_menu); + auto& p = m_processor; + + { + juce::PopupMenu gainMenu; + + const auto gain = m_processor.getOutputGain(); + + gainMenu.addItem("-12 db", true, gain == 0.25f, [&p] { p.setOutputGain(0.25f); }); + gainMenu.addItem("-6 db", true, gain == 0.5f, [&p] { p.setOutputGain(0.5f); }); + gainMenu.addItem("0 db (default)", true, gain == 1, [&p] { p.setOutputGain(1); }); + gainMenu.addItem("+6 db", true, gain == 2, [&p] { p.setOutputGain(2); }); + gainMenu.addItem("+12 db", true, gain == 4, [&p] { p.setOutputGain(4); }); + + _menu.addSubMenu("Output Gain", gainMenu); + } +} + +bool PluginEditorState::initAdvancedContextMenu(juce::PopupMenu& _menu, bool _enabled) +{ + jucePluginEditorLib::PluginEditorState::initAdvancedContextMenu(_menu, _enabled); + + const auto percent = m_processor.getDspClockPercent(); + const auto hz = m_processor.getDspClockHz(); + + juce::PopupMenu clockMenu; + + auto makeEntry = [&](const int _percent) + { + const auto mhz = hz * _percent / 100 / 1000000; + std::stringstream ss; + ss << _percent << "% (" << mhz << " MHz)"; + if(_percent == 100) + ss << " (Default)"; + clockMenu.addItem(ss.str(), _enabled, percent == _percent, [this, _percent] { m_processor.setDspClockPercent(_percent); }); + }; + + makeEntry(50); + makeEntry(75); + makeEntry(100); + makeEntry(125); + makeEntry(150); + makeEntry(200); + + _menu.addSubMenu("DSP Clock", clockMenu); + + return true; +} diff --git a/source/jucePlugin/PluginEditorState.h b/source/jucePlugin/PluginEditorState.h @@ -10,4 +10,7 @@ public: explicit PluginEditorState(AudioPluginAudioProcessor& _processor, pluginLib::Controller& _controller); genericUI::Editor* createEditor(const Skin& _skin, std::function<void()> _openMenuCallback) override; + + void initContextMenu(juce::PopupMenu& _menu) override; + bool initAdvancedContextMenu(juce::PopupMenu& _menu, bool _enabled) override; }; diff --git a/source/jucePlugin/PluginProcessor.cpp b/source/jucePlugin/PluginProcessor.cpp @@ -26,7 +26,7 @@ AudioPluginAudioProcessor::AudioPluginAudioProcessor() : .withOutput("Out 2", juce::AudioChannelSet::stereo(), true) .withOutput("Out 3", juce::AudioChannelSet::stereo(), true) #endif - , getConfigOptions()) + , ::getConfigOptions()) { m_clockTempoParam = getController().getParameterIndexByName(Virus::g_paramClockTempo); @@ -117,7 +117,7 @@ void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, inputs[channel] = buffer.getReadPointer(channel); for (int channel = 0; channel < totalNumOutputChannels; ++channel) - outputs[channel] = buffer.getWritePointer(channel); + outputs[channel] = buffer.getWritePointer(channel); for(const auto metadata : midiMessages) { @@ -128,9 +128,9 @@ void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, if(message.isSysEx() || message.getRawDataSize() > 3) { ev.sysex.resize(message.getRawDataSize()); - memcpy( &ev.sysex[0], message.getRawData(), ev.sysex.size()); + memcpy(ev.sysex.data(), message.getRawData(), ev.sysex.size()); - // Juce bug? Or VSTHost bug? Juce inserts f0/f7 when converting VST3 midi packet to Juce packet, but its already there + // Juce bug? Or VSTHost bug? Juce inserts f0/f7 when converting VST3 midi packet to Juce packet, but it's already there if(ev.sysex.size() > 1) { if(ev.sysex.front() == 0xf0 && ev.sysex[1] == 0xf0) @@ -183,6 +183,8 @@ void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, getPlugin().process(inputs, outputs, buffer.getNumSamples(), static_cast<float>(pos.bpm), static_cast<float>(pos.ppqPosition), pos.isPlaying); + applyOutputGain(outputs, buffer.getNumSamples()); + m_midiOut.clear(); getPlugin().getMidiOut(m_midiOut); @@ -219,11 +221,9 @@ void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, //============================================================================== -juce::AudioProcessorEditor* AudioPluginAudioProcessor::createEditor() +jucePluginEditorLib::PluginEditorState* AudioPluginAudioProcessor::createEditorState() { - if(!m_editorState) - m_editorState.reset(new PluginEditorState(*this, getController())); - return new jucePluginEditorLib::EditorWindow(*this, *m_editorState, getConfig()); + return new PluginEditorState(*this, getController()); } void AudioPluginAudioProcessor::updateLatencySamples() diff --git a/source/jucePlugin/PluginProcessor.h b/source/jucePlugin/PluginProcessor.h @@ -19,7 +19,7 @@ public: bool isBusesLayoutSupported (const BusesLayout& layouts) const override; void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override; using AudioProcessor::processBlock; - juce::AudioProcessorEditor* createEditor() override; + jucePluginEditorLib::PluginEditorState* createEditorState() override; const juce::String getName() const override; bool acceptsMidi() const override; diff --git a/source/jucePlugin/VirusController.cpp b/source/jucePlugin/VirusController.cpp @@ -190,6 +190,11 @@ namespace Virus return getPresetName("SingleName", _values); } + std::string Controller::getSinglePresetName(const pluginLib::MidiPacket::AnyPartParamValues& _values) const + { + return getPresetName("SingleName", _values); + } + std::string Controller::getMultiPresetName(const pluginLib::MidiPacket::ParamValues& _values) const { return getPresetName("MultiName", _values); @@ -214,7 +219,26 @@ namespace Virus return name; } - void Controller::setSinglePresetName(uint8_t _part, const juce::String& _name) const + std::string Controller::getPresetName(const std::string& _paramNamePrefix, const pluginLib::MidiPacket::AnyPartParamValues& _values) const + { + std::string name; + for(uint32_t i=0; i<kNameLength; ++i) + { + const std::string paramName = _paramNamePrefix + std::to_string(i); + const auto idx = getParameterIndexByName(paramName); + if(idx == InvalidParameterIndex) + break; + + const auto it = _values[idx]; + if(!it) + break; + + name += static_cast<char>(*it); + } + return name; + } + + void Controller::setSinglePresetName(const uint8_t _part, const juce::String& _name) const { for (int i=0; i<kNameLength; i++) { @@ -234,7 +258,20 @@ namespace Virus } } - bool Controller::isMultiMode() const + void Controller::setSinglePresetName(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _name) const + { + for(uint32_t i=0; i<kNameLength; ++i) + { + const std::string paramName = "SingleName" + std::to_string(i); + const auto idx = getParameterIndexByName(paramName); + if(idx == InvalidParameterIndex) + break; + + _values[idx] = (i < _name.size()) ? _name[i] : ' '; + } + } + + bool Controller::isMultiMode() const { const auto paramIdx = getParameterIndexByName(g_paramPlayMode); const auto& value = getParameter(paramIdx)->getValueObject(); @@ -308,7 +345,7 @@ namespace Virus return m_currentPresetSource[_part]; } - bool Controller::parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::ParamValues& _parameterValues, const pluginLib::SysEx& _msg) const + bool Controller::parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _parameterValues, const pluginLib::SysEx& _msg) const { const auto packetName = midiPacketName(MidiPacketType::SingleDump); @@ -348,17 +385,6 @@ namespace Virus return {}; } - void Controller::parseSingle(const pluginLib::SysEx& msg) - { - pluginLib::MidiPacket::Data data; - pluginLib::MidiPacket::ParamValues parameterValues; - - if(!parseSingle(data, parameterValues, msg)) - return; - - parseSingle(msg, data, parameterValues); - } - void Controller::parseSingle(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues) { SinglePatch patch; @@ -390,12 +416,11 @@ namespace Virus for(auto it = _parameterValues.begin(); it != _parameterValues.end(); ++it) { auto* p = getParameter(it->first.second, ch); - p->setValueFromSynth(it->second, true, pluginLib::Parameter::ChangedBy::PresetChange); - - for (const auto& derivedParam : p->getDerivedParameters()) - derivedParam->setValueFromSynth(it->second, true, pluginLib::Parameter::ChangedBy::PresetChange); + p->setValueFromSynth(it->second, false, pluginLib::Parameter::ChangedBy::PresetChange); } + m_processor.updateHostDisplay(juce::AudioProcessorListener::ChangeDetails().withProgramChanged(true)); + if(m_currentPresetSource[ch] != PresetSource::Browser) { bool found = false; @@ -432,11 +457,17 @@ namespace Virus } if (onProgramChange) - onProgramChange(); + onProgramChange(patch.progNumber); } else { - m_singles[virusLib::toArrayIndex(patch.bankNumber)][patch.progNumber] = patch; + const auto bank = toArrayIndex(patch.bankNumber); + const auto program = patch.progNumber; + + m_singles[bank][program] = patch; + + if(onRomPatchReceived) + onRomPatchReceived(patch.bankNumber, program); } } @@ -466,8 +497,10 @@ namespace Virus if(desc.page != virusLib::PAGE_C) continue; - param->setValueFromSynth(value, true, pluginLib::Parameter::ChangedBy::PresetChange); + param->setValueFromSynth(value, false, pluginLib::Parameter::ChangedBy::PresetChange); } + + m_processor.updateHostDisplay(juce::AudioProcessorListener::ChangeDetails().withProgramChanged(true)); } } @@ -614,7 +647,7 @@ namespace Virus return dst; } - std::vector<uint8_t> Controller::createSingleDump(uint8_t _bank, uint8_t _program, const pluginLib::MidiPacket::ParamValues& _paramValues) + std::vector<uint8_t> Controller::createSingleDump(uint8_t _bank, uint8_t _program, const pluginLib::MidiPacket::AnyPartParamValues& _paramValues) { const auto* m = getMidiPacket(midiPacketName(MidiPacketType::SingleDump)); assert(m && "midi packet not found"); @@ -629,15 +662,8 @@ namespace Virus data.insert(std::make_pair(pluginLib::MidiDataType::Bank, _bank)); data.insert(std::make_pair(pluginLib::MidiDataType::Program, _program)); - for (const auto& it : _paramValues) - { - const auto* p = getParameter(it.first.second, _program == virusLib::SINGLE ? 0 : _program); - assert(p); - if(!p) - return {}; - const auto key = std::make_pair(it.first.first, p->getDescription().name); - paramValues.insert(std::make_pair(key, it.second)); - } + if(!createNamedParamValues(paramValues, _paramValues)) + return {}; pluginLib::MidiPacket::Sysex dst; if(!m->create(dst, data, paramValues)) @@ -645,18 +671,40 @@ namespace Virus return dst; } - std::vector<uint8_t> Controller::modifySingleDump(const std::vector<uint8_t>& _sysex, const virusLib::BankNumber _newBank, const uint8_t _newProgram, const bool _modifyBank, const bool _modifyProgram) + std::vector<uint8_t> Controller::modifySingleDump(const std::vector<uint8_t>& _sysex, const virusLib::BankNumber _newBank, const uint8_t _newProgram) { pluginLib::MidiPacket::Data data; - pluginLib::MidiPacket::ParamValues parameterValues; + pluginLib::MidiPacket::AnyPartParamValues parameterValues; if(!parseSingle(data, parameterValues, _sysex)) return {}; - return createSingleDump(_modifyBank ? toMidiByte(_newBank) : data[pluginLib::MidiDataType::Bank], _modifyProgram ? _newProgram : data[pluginLib::MidiDataType::Program], parameterValues); + if(_newBank == virusLib::BankNumber::EditBuffer) + { + const auto lockedParams = getLockedParameters(); + + if(!lockedParams.empty()) + { + const uint8_t part = _newProgram == virusLib::SINGLE ? 0 : _newProgram; + pluginLib::MidiPacket::NamedParamValues currentParams; + createNamedParamValues(currentParams, midiPacketName(MidiPacketType::SingleDump), part); + + for (const auto& name : lockedParams) + { + const auto it = currentParams.find({pluginLib::MidiPacket::AnyPart, name}); + if(it == currentParams.end()) + continue; + + const uint32_t idx = getParameterIndexByName(name); + if(idx != InvalidParameterIndex) + parameterValues[idx] = it->second; + } + } + } + return createSingleDump(toMidiByte(_newBank), _newProgram, parameterValues); } - void Controller::selectPrevPreset(uint8_t _part) + void Controller::selectPrevPreset(const uint8_t _part) { if(getCurrentPartProgram(_part) > 0) { @@ -678,4 +726,41 @@ namespace Virus sprintf(temp, "Bank %c", 'A' + _index); return temp; } + + bool Controller::activatePatch(const std::vector<unsigned char>& _sysex) + { + // re-pack, force to edit buffer + return activatePatch(_sysex, isMultiMode() ? getCurrentPart() : static_cast<uint8_t>(virusLib::ProgramType::SINGLE)); + } + + bool Controller::activatePatch(const std::vector<unsigned char>& _sysex, uint32_t _part) + { + if(_part == virusLib::ProgramType::SINGLE) + { + if(isMultiMode()) + _part = 0; + } + else if(_part >= 16) + { + return false; + } + else if(!isMultiMode() && _part == 0) + { + _part = virusLib::ProgramType::SINGLE; + } + + const auto program = static_cast<uint8_t>(_part); + + const auto msg = modifySingleDump(_sysex, virusLib::BankNumber::EditBuffer, program); + + if(msg.empty()) + return false; + + sendSysEx(msg); + requestSingle(toMidiByte(virusLib::BankNumber::EditBuffer), program); + + setCurrentPartPresetSource(program == virusLib::ProgramType::SINGLE ? 0 : program, PresetSource::Browser); + + return true; + } }; // namespace Virus diff --git a/source/jucePlugin/VirusController.h b/source/jucePlugin/VirusController.h @@ -67,21 +67,26 @@ namespace Virus ~Controller() override; 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); + std::vector<uint8_t> createSingleDump(uint8_t _bank, uint8_t _program, const pluginLib::MidiPacket::AnyPartParamValues& _paramValues); + std::vector<uint8_t> modifySingleDump(const std::vector<uint8_t>& _sysex, virusLib::BankNumber _newBank, uint8_t _newProgram); void selectPrevPreset(uint8_t _part); void selectNextPreset(uint8_t _part); std::string getBankName(uint32_t _index) const; + bool activatePatch(const std::vector<unsigned char>& _sysex); + bool activatePatch(const std::vector<unsigned char>& _sysex, uint32_t _part); + static void printMessage(const pluginLib::SysEx &); juce::Value* getParamValue(uint8_t ch, uint8_t bank, uint8_t paramIndex); juce::StringArray getSinglePresetNames(virusLib::BankNumber bank) const; std::string getSinglePresetName(const pluginLib::MidiPacket::ParamValues& _values) const; + std::string getSinglePresetName(const pluginLib::MidiPacket::AnyPartParamValues& _values) const; std::string getMultiPresetName(const pluginLib::MidiPacket::ParamValues& _values) const; std::string getPresetName(const std::string& _paramNamePrefix, const pluginLib::MidiPacket::ParamValues& _values) const; + std::string getPresetName(const std::string& _paramNamePrefix, const pluginLib::MidiPacket::AnyPartParamValues& _values) const; const Singles& getSinglePresets() const { @@ -104,7 +109,9 @@ namespace Virus } void setSinglePresetName(uint8_t _part, const juce::String& _name) const; - bool isMultiMode() const; + void setSinglePresetName(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _name) const; + + bool isMultiMode() const; // part 0 - 15 (ignored when single! 0x40...) void setCurrentPartPreset(uint8_t _part, virusLib::BankNumber _bank, uint8_t _prg); @@ -118,8 +125,9 @@ namespace Virus uint32_t getBankCount() const { return static_cast<uint32_t>(m_singles.size()); } void parseSysexMessage(const pluginLib::SysEx &) override; void onStateLoaded() override; - std::function<void()> onProgramChange = {}; + std::function<void(int)> onProgramChange = {}; std::function<void()> onMsgDone = {}; + std::function<void(virusLib::BankNumber _bank, uint32_t _program)> onRomPatchReceived = {}; bool requestProgram(uint8_t _bank, uint8_t _program, bool _multi) const; bool requestSingle(uint8_t _bank, uint8_t _program) const; @@ -140,7 +148,7 @@ namespace Virus uint8_t getDeviceId() const { return m_deviceId; } - bool parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::ParamValues& _parameterValues, const pluginLib::SysEx& _msg) const; + bool parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _parameterValues, const pluginLib::SysEx& _msg) const; private: static std::string loadParameterDescriptions(); @@ -153,7 +161,6 @@ namespace Virus MultiPatch m_multiEditBuffer; - 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 pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _parameterValues); diff --git a/source/jucePlugin/parameterDescriptions_C.json b/source/jucePlugin/parameterDescriptions_C.json @@ -375,6 +375,124 @@ {"page":114, "class":"Global", "index":126, "name":"LCD Contrast", "min":0, "max":127, "isPublic":false, "isDiscrete":false, "isBool":false}, {"page":114, "class":"Global", "index":127, "name":"Master Volume", "min":0, "max":127, "isPublic":false, "isDiscrete":false, "isBool":false} ], + "regions": + [ + { "id":"oscA", "name": "Oscillator 1", + "parameters":["Osc1 Shape", "Osc1 Pulsewidth", "Osc1 Wave Select", "Osc1 Semitone", "Osc1 Keyfollow", "Osc1 Shape Velocity"] + }, + { "id":"oscB", "name": "Oscillator 2", + "parameters":["Osc2 Shape", "Osc2 Pulsewidth", "Osc2 Wave Select", "Osc2 Semitone", "Osc2 Detune", "Osc2 FM Amount", "Osc2 Sync", "Osc2 Filt Env Amt", "FM Filt Env Amt", "Osc2 Keyfollow", "Osc FM Mode", "Osc2 Shape Velocity", "Fm Amount Velocity"] + }, + { "id":"oscC", "name": "Oscillator 3", + "parameters":["Osc3 Mode", "Osc3 Volume", "Osc3 Semitone", "Osc3 Detune"] + }, + { "id":"oscCommon", "name": "Osc Common", + "parameters":["Osc Balance", "Suboscillator Volume", "Suboscillator Shape", "Osc Mainvolume", "Ringmodulator Volume", "Osc Init Phase", "PulseWidth Velocity"] + }, + { "id":"noise", "name": "Noise", + "parameters":["Noise Volume", "Noise Color"] + }, + { "id":"filterA", "name": "Filter 1", + "parameters":["Cutoff", "Filter1 Resonance", "Filter1 Env Amt", "Filter1 Keyfollow", "Filter1 Mode", "Filter1 Env Polarity", "Flt1 EnvAmt Velocity", "Resonance1 Velocity"] + }, + { "id":"filterB", "name": "Filter 2", + "parameters":["Cutoff2", "Filter2 Resonance", "Filter2 Env Amt", "Filter2 Keyfollow", "Filter2 Mode", "Filter2 Env Polarity", "Filter2 Cutoff Link", "Flt2 EnvAmt Velocity", "Resonance2 Velocity"] + }, + { "id":"filterCommon", "name": "Filters Common", + "parameters":["Filter Balance", "Saturation Curve", "Filter Routing", "Filter Keytrack Base"] + }, + { "id":"envFilter", "name": "Filter Env", + "parameters":["Filter Env Attack", "Filter Env Decay", "Filter Env Sustain", "Filter Env Sustain Time", "Filter Env Release"] + }, + { "id":"envAmp", "name": "Amp Env", + "parameters":["Amp Env Attack", "Amp Env Decay", "Amp Env Sustain", "Amp Env Sustain Time", "Amp Env Release"] + }, + { "id":"lfoA", "name": "LFO 1", + "parameters":["Lfo1 Rate", "Lfo1 Shape", "Lfo1 Env Mode", "Lfo1 Mode", "Lfo1 Symmetry", "Lfo1 Keyfollow", "Lfo1 Keytrigger", "Osc1 Lfo1 Amount", "Osc2 Lfo1 Amount", "PW Lfo1 Amount", "Reso Lfo1 Amount", "FiltGain Lfo1 Amount", "Lfo1 Clock", "LFO1 Assign Dest", "LFO1 Assign Amount"] + }, + { "id":"lfoB", "name": "LFO 2", + "parameters":["Lfo2 Rate", "Lfo2 Shape", "Lfo2 Env Mode", "Lfo2 Mode", "Lfo2 Symmetry", "Lfo2 Keyfollow", "Lfo2 Keytrigger", "Shape Lfo2 Amount", "FM Lfo2 Amount", "Cutoff1 Lfo2 Amount", "Cutoff2 Lfo2 Amount", "Pan Lfo2 Amount", "Lfo2 Clock", "LFO2 Assign Dest", "LFO2 Assign Amount"] + }, + { "id":"lfoC", "name": "LFO 3", + "parameters":["Lfo3 Rate", "Lfo3 Shape", "Lfo3 Mode", "Lfo3 Keyfollow", "Lfo3 Destination", "Osc Lfo3 Amount", "Lfo3 Fade-In Time", "Lfo3 Clock"] + }, + { "id":"amp", "name": "Amp", + "parameters":["Patch Volume", "Amp Velocity", "Panorama Velocity", "Second Output Balance"] + }, + { "id":"common", "name": "Common", + "parameters":["Transpose", "Key Mode", "Control Smooth Mode", "Bender Range Up", "Bender Range Down", "Bender Scale"] + }, + { "id":"unison", "name": "Unison", + "parameters":["Unison Mode", "Unison Detune", "Unison Pan Spread", "Unison Lfo Phase"] + }, + { "id":"chorus", "name": "Chorus", + "parameters":["Chorus Mix", "Chorus Rate", "Chorus Depth", "Chorus Delay", "Chorus Feedback", "Chorus Lfo Shape"] + }, + { "id":"delayReverb", "name": "Delay/Reverb", + "parameters":["Delay/Reverb Mode", "Effect Send", "Delay Time", "Delay Feedback", "Dly Rate / Rev Decay", "Dly Depth ", "Rev Size", "Delay Lfo Shape", "Reverb Damping", "Delay Color", "Delay Clock"] + }, + { "id":"arp", "name": "Arpeggiator", + "parameters":["Arp Mode", "Arp Pattern Selct", "Arp Octave Range", "Arp Hold Enable", "Arp Note Length", "Arp Swing", "Arp Clock"] + }, + { "id":"punch", "name": "Punch Intensity", + "parameters":["Punch Intensity"] + }, + { "id":"input", "name": "Input", + "parameters":["Input Mode", "Input Select", "Input Ringmodulator"] + }, + { "id":"assignA", "name": "Assign 1", + "parameters":["Assign1 Source", "Assign1 Destination", "Assign1 Amount"] + }, + { "id":"assignB", "name": "Assign 2", + "parameters":["Assign2 Source", "Assign2 Destination1", "Assign2 Amount1", "Assign2 Destination2", "Assign2 Amount2"] + }, + { "id":"assignC", "name": "Assign 3", + "parameters":["Assign3 Source", "Assign3 Destination1", "Assign3 Amount1", "Assign3 Destination2", "Assign3 Amount2", "Assign3 Destination3", "Assign3 Amount3"] + }, + { "id":"assignD", "name": "Assign 4", + "parameters":["Assign 4 Source", "Assign 4 Destination", "Assign 4 Amount"] + }, + { "id":"assignE", "name": "Assign 5", + "parameters":["Assign 5 Source", "Assign 5 Destination", "Assign 5 Amount"] + }, + { "id":"assignF", "name": "Assign 6", + "parameters":["Assign 6 Source", "Assign 6 Destination", "Assign 6 Amount"] + }, + { "id":"phaser", "name": "Phaser", + "parameters":["Phaser Mode", "Phaser Mix", "Phaser Rate", "Phaser Depth", "Phaser Frequency", "Phaser Feedback", "Phaser Spread"] + }, + { "id":"eq", "name": "Equalizer", + "parameters":["LowEQ Gain", "LowEQ Frequency", "MidEQ Gain", "MidEQ Frequency", "MidEQ Q-Factor", "HighEQ Gain", "HighEQ Frequency"] + }, + { "id":"bassBoost", "name": "Analog Boost", + "parameters":["Bass Intensity", "Bass Tune"] + }, + { "id":"distortion", "name": "Distortion", + "parameters":["Distortion Curve", "Distortion Intensity"] + }, + { "id":"vocoder", "name": "Vocoder", + "parameters":["Vocoder Mode", "Filter Select", "Filter Env Sustain Time", "Filter Env Release", "Filter Env Decay", "Cutoff", "Filter1 Resonance", "Filter1 Keyfollow", "Cutoff2", "Filter2 Resonance", "Filter2 Keyfollow"] + }, + { "id":"inputFollower", "name": "Input Follower", + "parameters":["Input Follower Mode", "Filter Env Sustain", "Filter Env Attack", "Filter Env Release"] + }, + + { "id":"oscs", "name": "Oscillators", + "regions":["oscA", "oscB", "oscC", "oscCommon", "noise"] + }, + { "id":"filters", "name": "Filters", + "regions":["filterA", "filterB", "filterCommon", "envFilter"] + }, + { "id":"lfos", "name": "LFOs", + "regions":["lfoA", "lfoB", "lfoC"] + }, + { "id":"fx", "name": "Effects", + "regions":["delayReverb", "chorus", "phaser", "bassBoost", "distortion", "eq", "punch"] + }, + { "id":"modmatrix", "name": "Modulation Slots", + "regions":["assignA", "assignB", "assignC", "assignD", "assignE", "assignF"] + } + ], "parameterlinks": [ { @@ -590,7 +708,7 @@ ], "modmatrixSource": [ - "Off", "PitchBnd", "ChanPres", "ModWheel", "Breath", "Contr3", "Foot", "Data", + "Off", "PitchBnd", "ChanPres", "ModWheel", "Breath", "Contr 3", "Foot", "Data", "Balance", "Contr 9", "Express", "Contr 12", "Contr 13", "Contr 14", "Contr 15", "Contr 16", "HoldPed", "PortaSw", "SostPed", "AmpEnv", "FiltEnv", "Lfo 1", "Lfo 2", "Lfo 3", "VeloOn", "VeloOff", "KeyFlw", "Random" @@ -667,7 +785,7 @@ ], "oscFMMode": [ - "Pos-Tri", "Tri", "Wave", "Noise", "In L", "In L+R", "In R", "Aux1 L", "Aux1 L+R", "Aux1 R", "Aux2 L", "Aux2 L+R", "Aux2 R" + "Pos-Tri", "Tri", "Wave", "Noise", "In L", "In L+R", "In R", "Aux1 L", "Aux1 L+R", "Aux1 R", "Aux2 L", "Aux2 L+R", "Aux2 R" ], "vocoderMode": [ @@ -693,7 +811,8 @@ [ "--", "Lead", "Bass", "Pad", "Decay", "Pluck", "Acid", "Classic", "Arpeggiator", "Effects", "Drums", "Percussion", - "Input", "Vocoder", "Favourite 1", "Favourite 2", "Favourite 3" + "Input", "Vocoder", "Favourite 1", "Favourite 2", "Favourite 3", + "Organ", "Piano", "String", "FM", "Digital", "Atomizer" ], "reverbRoomSize": [ diff --git a/source/jucePlugin/skins/Galaxpel/VirusC_Galaxpel.json b/source/jucePlugin/skins/Galaxpel/VirusC_Galaxpel.json @@ -12,6 +12,86 @@ "buttons": ["TabOsc", "TabLfo", "TabEffects", "TabArp", "Presets"], "pages": ["page_osc", "page_lfo", "page_fx", "page_arp", "page_presets"] }, + "templates" : [ + { + "name" : "pm_treeview", + "treeview" : { + "alignH" : "L", + "alignV" : "C", + "color" : "75A7FEFF", + "backgroundColor" : "1c1f22ff", + "selectedItemBackgroundColor" : "3b4150ff" + } + }, + { + "name" : "pm_search", + "texteditor" : { + "alignH" : "L", + "alignV" : "C", + "color" : "75A7FEFF", + "outlineColor" : "476495ff", + "backgroundColor" : "0e1216ff", + "text": "Filter..." + } + }, + { + "name" : "pm_scrollbar", + "scrollbar" : { + "color" : "75A7FEFF", + } + }, + { + "name" : "pm_listbox", + "listbox" : { + "alignH" : "L", + "alignV" : "C", + "color" : "75A7FEFF", + "backgroundColor" : "1c1f22ff", + "selectedItemBackgroundColor" : "3b4150ff", + "bold": "1" + } + }, + { + "name" : "pm_info_name", + "label" : { + "alignH" : "L", + "alignV" : "T", + "color" : "79AAFFFF", + "backgroundColor" : "1c1f22ff", + "textHeight" : "32", + "bold": "1" + } + }, + { + "name" : "pm_info_label", + "label" : { + "alignH" : "L", + "alignV" : "B", + "color" : "888888ff", + "textHeight" : "16" + } + }, + { + "name" : "pm_info_text", + "label" : { + "alignH" : "L", + "alignV" : "T", + "color" : "75A7FEFF", + "textHeight" : "18" + } + }, + { + "name" : "pm_status_label", + "label" : { + "alignH" : "L", + "alignV" : "C", + "color" : "79AAFFFF", + "backgroundColor" : "1c1f22ff", + "textHeight" : "18", + "bold": "0" + } + } + ], "children" : [ { "name" : "bg", @@ -5562,9 +5642,9 @@ "alignV" : "C", "fontFile" : "BEBASNEUE_BOLD-_1_", "fontName" : "Bebas Neue", - "x" : "1490.77", + "x" : "1290.77", "y" : "1542", - "width" : "1200.616", + "width" : "1400.616", "height" : "45" } } diff --git a/source/jucePlugin/skins/Hoverland/VirusC_Hoverland.json b/source/jucePlugin/skins/Hoverland/VirusC_Hoverland.json @@ -12,6 +12,86 @@ "buttons": ["TabOsc", "TabLfo", "TabEffects", "TabArp", "Presets"], "pages": ["page_osc", "page_lfo", "page_fx", "page_arp", "page_presets"] }, + "templates" : [ + { + "name" : "pm_treeview", + "treeview" : { + "alignH" : "L", + "alignV" : "C", + "color" : "ff7180", + "backgroundColor" : "2d1818", + "selectedItemBackgroundColor" : "3b4150ff" + } + }, + { + "name" : "pm_search", + "texteditor" : { + "alignH" : "L", + "alignV" : "C", + "color" : "ff7180", + "outlineColor" : "954747", + "backgroundColor" : "2d1818", + "text": "Filter..." + } + }, + { + "name" : "pm_scrollbar", + "scrollbar" : { + "color" : "ff7180", + } + }, + { + "name" : "pm_listbox", + "listbox" : { + "alignH" : "L", + "alignV" : "C", + "color" : "ff7180", + "backgroundColor" : "2d1818", + "selectedItemBackgroundColor" : "3b4150ff", + "bold": "1" + } + }, + { + "name" : "pm_info_name", + "label" : { + "alignH" : "L", + "alignV" : "T", + "color" : "ff7180", + "backgroundColor" : "2d1818", + "textHeight" : "32", + "bold": "1" + } + }, + { + "name" : "pm_info_label", + "label" : { + "alignH" : "L", + "alignV" : "B", + "color" : "888888ff", + "textHeight" : "16" + } + }, + { + "name" : "pm_info_text", + "label" : { + "alignH" : "L", + "alignV" : "T", + "color" : "ff7180", + "textHeight" : "18" + } + }, + { + "name" : "pm_status_label", + "label" : { + "alignH" : "L", + "alignV" : "C", + "color" : "ff7180", + "backgroundColor" : "2d1818", + "textHeight" : "18", + "bold": "0" + } + } + ], "children" : [ { "name" : "bg", @@ -5740,31 +5820,13 @@ }, "children" : [ { - "name" : "ContainerFileSelector", + "name" : "ContainerPatchManager", "component" : { "x" : "88", - "y" : "96", - "width" : "928", + "y" : "95", + "width" : "1859", "height" : "1055" } - }, - { - "name" : "ContainerPatchList", - "component" : { - "x" : "1016", - "y" : "96", - "width" : "929", - "height" : "1005" - } - }, - { - "name" : "ContainerPatchListSearchBox", - "component" : { - "x" : "1016", - "y" : "1101", - "width" : "929", - "height" : "50" - } } ] }, diff --git a/source/jucePlugin/skins/Trancy/VirusC_Trancy.json b/source/jucePlugin/skins/Trancy/VirusC_Trancy.json @@ -5404,30 +5404,12 @@ } }, { - "name" : "ContainerFileSelector", + "name" : "ContainerPatchManager", "component" : { - "x" : "6", - "y" : "181", - "width" : "1008", - "height" : "813" - } - }, - { - "name" : "ContainerPatchList", - "component" : { - "x" : "1034", - "y" : "30", - "width" : "1041", - "height" : "914" - } - }, - { - "name" : "ContainerPatchListSearchBox", - "component" : { - "x" : "1034", - "y" : "944", - "width" : "1041", - "height" : "50" + "x" : "2", + "y" : "2", + "width" : "2073", + "height" : "990" } }, { diff --git a/source/jucePlugin/ui3/PartButton.cpp b/source/jucePlugin/ui3/PartButton.cpp @@ -0,0 +1,54 @@ +#include "PartButton.h" + +#include "VirusEditor.h" +#include "../VirusController.h" +#include "../../jucePluginEditorLib/patchmanager/list.h" + +#include "../../jucePluginEditorLib/patchmanager/savepatchdesc.h" + +namespace genericVirusUI +{ + PartButton::PartButton(VirusEditor& _editor) : jucePluginEditorLib::PartButton<TextButton>(_editor), m_editor(_editor) // NOLINT(clang-diagnostic-undefined-func-template) + { + } + + bool PartButton::isInterestedInDragSource(const SourceDetails& _dragSourceDetails) + { + if(getPart() > 0 && !m_editor.getController().isMultiMode()) + return false; + + return jucePluginEditorLib::PartButton<TextButton>::isInterestedInDragSource(_dragSourceDetails); // NOLINT(clang-diagnostic-undefined-func-template) + } + + void PartButton::paint(juce::Graphics& g) + { + jucePluginEditorLib::PartButton<TextButton>::paint(g); + } + + void PartButton::onClick() + { + selectPreset(getPart()); + } + + void PartButton::selectPreset(uint8_t _part) const + { + juce::PopupMenu selector; + + for (uint8_t b = 0; b < static_cast<uint8_t>(m_editor.getController().getBankCount()); ++b) + { + const auto bank = virusLib::fromArrayIndex(b); + auto presetNames = m_editor.getController().getSinglePresetNames(bank); + juce::PopupMenu p; + for (uint8_t j = 0; j < static_cast<uint8_t>(presetNames.size()); j++) + { + const auto& presetName = presetNames[j]; + p.addItem(presetName, [this, bank, j, _part] + { + m_editor.selectRomPreset(_part, bank, j); + }); + } + selector.addSubMenu(m_editor.getController().getBankName(b), p); + } + selector.showMenuAsync(juce::PopupMenu::Options()); + } +} diff --git a/source/jucePlugin/ui3/PartButton.h b/source/jucePlugin/ui3/PartButton.h @@ -0,0 +1,27 @@ +#pragma once + +#include "PatchManager.h" +#include "juce_gui_basics/juce_gui_basics.h" + +#include "../../jucePluginEditorLib/partbutton.h" + +namespace genericVirusUI +{ + class VirusEditor; + + class PartButton final : public jucePluginEditorLib::PartButton<juce::TextButton> + { + public: + explicit PartButton(VirusEditor& _editor); + + bool isInterestedInDragSource(const SourceDetails& _dragSourceDetails) override; + + void paint(juce::Graphics& g) override; + + void onClick() override; + private: + void selectPreset(uint8_t _part) const; + + VirusEditor& m_editor; + }; +} diff --git a/source/jucePlugin/ui3/Parts.cpp b/source/jucePlugin/ui3/Parts.cpp @@ -1,5 +1,6 @@ #include "Parts.h" +#include "PartButton.h" #include "VirusEditor.h" #include "../VirusController.h" @@ -7,21 +8,30 @@ #include "../../jucePluginLib/parameterbinding.h" +#include "../../jucePluginEditorLib/patchmanager/savepatchdesc.h" + namespace genericVirusUI { Parts::Parts(VirusEditor& _editor) : m_editor(_editor) { - _editor.findComponents<juce::Button>(m_partSelect, "SelectPart"); + _editor.findComponents<genericUI::Button<juce::DrawableButton>>(m_partSelect, "SelectPart"); _editor.findComponents<juce::Button>(m_presetPrev, "PresetPrev"); _editor.findComponents<juce::Button>(m_presetNext, "PresetNext"); _editor.findComponents<juce::Slider>(m_partVolume, "PartVolume"); _editor.findComponents<juce::Slider>(m_partPan, "PartPan"); - _editor.findComponents<juce::TextButton>(m_presetName, "PresetName"); + _editor.findComponents<PartButton>(m_presetName, "PresetName"); for(size_t i=0; i<m_partSelect.size(); ++i) { m_partSelect[i]->onClick = [this, i]{ selectPart(i); }; + m_partSelect[i]->onDown = [this, i](const juce::MouseEvent& _e) + { + if(!_e.mods.isPopupMenu()) + return false; + selectPartMidiChannel(i); + return true; + }; if(i < m_presetPrev.size()) m_presetPrev[i]->onClick = [this, i]{ selectPrevPreset(i); }; @@ -29,7 +39,7 @@ namespace genericVirusUI if(i < m_presetNext.size()) m_presetNext[i]->onClick = [this, i]{ selectNextPreset(i); }; - m_presetName[i]->onClick = [this, i]{ selectPreset(i); }; + m_presetName[i]->initalize(static_cast<uint8_t>(i)); const auto partVolume = _editor.getController().getParameterIndexByName(Virus::g_paramPartVolume); const auto partPanorama = _editor.getController().getParameterIndexByName(Virus::g_paramPartPanorama); @@ -69,14 +79,39 @@ namespace genericVirusUI m_editor.setPart(_part); } + void Parts::selectPartMidiChannel(const size_t _part) const + { + if(!m_editor.getController().isMultiMode()) + return; + + juce::PopupMenu menu; + + const auto idx= m_editor.getController().getParameterIndexByName("Part Midi Channel"); + if(idx == pluginLib::Controller::InvalidParameterIndex) + return; + + const auto v = m_editor.getController().getParameter(idx, static_cast<uint8_t>(_part)); + + for(uint8_t i=0; i<16; ++i) + { + menu.addItem("Midi Channel " + std::to_string(i + 1), true, v->getUnnormalizedValue() == i, [v, i] + { + v->setValue(v->convertTo0to1(i), pluginLib::Parameter::ChangedBy::Ui); + }); + } + + menu.showMenuAsync({}); + } + void Parts::selectPrevPreset(size_t _part) const { if(m_presetPrev.size() == 1) _part = m_editor.getController().getCurrentPart(); - auto* pb = m_editor.getPatchBrowser(); - if(!pb || !pb->selectPrevPreset()) - m_editor.getController().selectPrevPreset(static_cast<uint8_t>(_part)); + auto* pm = m_editor.getPatchManager(); + if(pm && pm->selectPrevPreset(static_cast<uint32_t>(_part))) + return; + m_editor.getController().selectPrevPreset(static_cast<uint8_t>(_part)); } void Parts::selectNextPreset(size_t _part) const @@ -84,33 +119,10 @@ namespace genericVirusUI if(m_presetNext.size() == 1) _part = m_editor.getController().getCurrentPart(); - auto* pb = m_editor.getPatchBrowser(); - if(!pb || !pb->selectNextPreset()) - m_editor.getController().selectNextPreset(static_cast<uint8_t>(_part)); - } - - void Parts::selectPreset(size_t _part) const - { - juce::PopupMenu selector; - - const auto pt = static_cast<uint8_t>(_part); - - for (uint8_t b = 0; b < m_editor.getController().getBankCount(); ++b) - { - const auto bank = virusLib::fromArrayIndex(b); - auto presetNames = m_editor.getController().getSinglePresetNames(bank); - juce::PopupMenu p; - for (uint8_t j = 0; j < presetNames.size(); j++) - { - const auto presetName = presetNames[j]; - p.addItem(presetName, [this, bank, j, pt] - { - m_editor.getController().setCurrentPartPreset(pt, bank, j); - }); - } - selector.addSubMenu(m_editor.getController().getBankName(b), p); - } - selector.showMenuAsync(juce::PopupMenu::Options()); + auto* pm = m_editor.getPatchManager(); + if(pm && pm->selectNextPreset(static_cast<uint32_t>(_part))) + return; + m_editor.getController().selectNextPreset(static_cast<uint8_t>(_part)); } void Parts::updatePresetNames() const diff --git a/source/jucePlugin/ui3/Parts.h b/source/jucePlugin/ui3/Parts.h @@ -2,12 +2,30 @@ #include <vector> -#include <juce_audio_processors/juce_audio_processors.h> +#include "../../juceUiLib/button.h" namespace genericVirusUI { + class PartButton; class VirusEditor; + class PartMouseListener : public juce::MouseListener + { + public: + explicit PartMouseListener(const int _part, const std::function<void(const juce::MouseEvent&, int)>& _callback) : m_part(_part), m_callback(_callback) + { + } + + void mouseDrag(const juce::MouseEvent& _event) override + { + m_callback(_event, m_part); + } + + private: + int m_part; + std::function<void(const juce::MouseEvent&, int)> m_callback; + }; + class Parts { public: @@ -20,9 +38,9 @@ namespace genericVirusUI private: void selectPart(size_t _part) const; + void selectPartMidiChannel(size_t _part) const; void selectPrevPreset(size_t _part) const; void selectNextPreset(size_t _part) const; - void selectPreset(size_t _part) const; void updatePresetNames() const; void updateSelectedPart() const; @@ -31,13 +49,13 @@ namespace genericVirusUI VirusEditor& m_editor; - std::vector<juce::Button*> m_partSelect; + std::vector<genericUI::Button<juce::DrawableButton>*> m_partSelect; std::vector<juce::Button*> m_presetPrev; std::vector<juce::Button*> m_presetNext; std::vector<juce::Slider*> m_partVolume; std::vector<juce::Slider*> m_partPan; - std::vector<juce::TextButton*> m_presetName; + std::vector<PartButton*> m_presetName; }; } diff --git a/source/jucePlugin/ui3/PatchBrowser.cpp b/source/jucePlugin/ui3/PatchBrowser.cpp @@ -1,243 +0,0 @@ -#include "PatchBrowser.h" - -#include "VirusEditor.h" -#include "../PluginProcessor.h" - -#include "../../virusLib/microcontrollerTypes.h" -#include "../../virusLib/microcontroller.h" - -#include "../VirusController.h" - -#include "../../synthLib/midiToSysex.h" -#include "../../synthLib/os.h" - -using namespace juce; - -namespace genericVirusUI -{ - enum Columns - { - INDEX = 1, - NAME = 2, - CAT1 = 3, - CAT2 = 4, - ARP = 5, - UNI = 6, - ST = 7, - VER = 8, - }; - - constexpr std::initializer_list<jucePluginEditorLib::PatchBrowser::ColumnDefinition> g_columns = - { - {"#", INDEX, 32}, - {"Name", NAME, 130}, - {"Category1", CAT1, 84}, - {"Category2", CAT2, 84}, - {"Arp", ARP, 32}, - {"Uni", UNI, 32}, - {"ST+-", ST, 32}, - {"Ver", VER, 32} - }; - - PatchBrowser::PatchBrowser(const VirusEditor& _editor) : jucePluginEditorLib::PatchBrowser(_editor, _editor.getController(), _editor.getProcessor().getConfig(), g_columns) - { - const auto& c = _editor.getController(); - - if(m_romBankSelect) - { - int id=1; - - m_romBankSelect->addItem("-", 1); - - for(uint32_t i=0; i<c.getBankCount(); ++i) - { - m_romBankSelect->addItem(c.getBankName(i), ++id); - } - - m_romBankSelect->onChange = [this] - { - const auto index = m_romBankSelect->getSelectedItemIndex(); - if(index > 0) - loadRomBank(index - 1); - }; - } - } - - void PatchBrowser::loadRomBank(const uint32_t _bankIndex) - { - const auto& singles = static_cast<Virus::Controller&>(m_controller).getSinglePresets(); - - if(_bankIndex >= singles.size()) - return; - - const auto& bank = singles[_bankIndex]; - - const auto searchValue = m_search.getText(); - - PatchList patches; - - for(size_t s=0; s<bank.size(); ++s) - { - auto* patch = createPatch(); - - patch->sysex = bank[s].data; - patch->progNumber = static_cast<int>(s); - - if(!initializePatch(*patch)) - continue; - - patches.push_back(std::shared_ptr<jucePluginEditorLib::Patch>(patch)); - } - - fillPatchList(patches); - } - - bool PatchBrowser::selectPrevNextPreset(int _dir) - { - const auto part = m_controller.getCurrentPart(); - - const auto& c = static_cast<const Virus::Controller&>(m_controller); - - if(c.getCurrentPartPresetSource(part) == Virus::Controller::PresetSource::Rom) - return false; - - if(m_filteredPatches.empty()) - return false; - - const auto idx = m_patchList.getSelectedRow(); - if(idx < 0) - return false; - - const auto name = c.getCurrentPartPresetName(part); - - if(m_filteredPatches[idx]->name != name) - return false; - - return jucePluginEditorLib::PatchBrowser::selectPrevNextPreset(_dir); - } - - bool PatchBrowser::initializePatch(jucePluginEditorLib::Patch& _patch) - { - 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)) - return false; - - const auto idxVersion = c.getParameterIndexByName("Version"); - const auto idxCategory1 = c.getParameterIndexByName("Category1"); - const auto idxCategory2 = c.getParameterIndexByName("Category2"); - const auto idxUnison = c.getParameterIndexByName("Unison Mode"); - const auto idxTranspose = c.getParameterIndexByName("Transpose"); - const auto idxArpMode = c.getParameterIndexByName("Arp Mode"); - - 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; - - 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); - - 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; - } - } - - bool PatchBrowser::loadUnkownData(std::vector<std::vector<uint8_t>>& _result, const std::string& _filename) - { - std::vector<uint8_t> data; - - if(!synthLib::readFile(data, _filename)) - return jucePluginEditorLib::PatchBrowser::loadUnkownData(_result, _filename); - - if(virusLib::Device::parsePowercorePreset(_result, data)) - return true; - - return jucePluginEditorLib::PatchBrowser::loadUnkownData(_result, _filename); - } - - 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,53 +0,0 @@ -#pragma once - -#include "PatchBrowser.h" - -#include "../../jucePluginEditorLib/patchbrowser.h" - -#include "../../virusLib/microcontrollerTypes.h" - -namespace Virus -{ - class Controller; -} - -namespace genericVirusUI -{ - class VirusEditor; - - struct Patch : jucePluginEditorLib::Patch - { - std::string category1; - std::string category2; - virusLib::PresetVersion model = virusLib::PresetVersion::A; - uint8_t unison = 0; - uint8_t transpose = 0; - uint8_t arpMode = 0; - }; - - class PatchBrowser : public jucePluginEditorLib::PatchBrowser - { - public: - explicit PatchBrowser(const VirusEditor& _editor); - - private: - 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; - - bool loadUnkownData(std::vector<std::vector<uint8_t>>& _result, const std::string& _filename) override; - - std::string getCellText(const jucePluginEditorLib::Patch& _patch, int _columnId) override; - - void loadRomBank(uint32_t _bankIndex); - - bool selectPrevNextPreset(int _dir) override; - - class PatchBrowserSorter; - }; -} diff --git a/source/jucePlugin/ui3/PatchManager.cpp b/source/jucePlugin/ui3/PatchManager.cpp @@ -0,0 +1,444 @@ +#include "PatchManager.h" + +#include "VirusEditor.h" +#include "../VirusController.h" + +#include "../../jucePluginLib/patchdb/datasource.h" +#include "../../jucePluginEditorLib/pluginEditor.h" + +#include "../../virusLib/microcontroller.h" +#include "../../virusLib/device.h" +#include "../../virusLib/midiFileToRomData.h" + +#include "../../synthLib/midiToSysex.h" +#include "../../synthLib/os.h" + +#include "juce_cryptography/hashing/juce_MD5.h" + +namespace Virus +{ + class Controller; +} + +namespace genericVirusUI +{ + void PatchManager::selectRomPreset(uint32_t _part, virusLib::BankNumber _bank, uint8_t _program) + { + const pluginLib::patchDB::DataSource ds = createRomDataSource(toArrayIndex(_bank)); + + selectPatch(_part, ds, _program); + } + + PatchManager::PatchManager(VirusEditor& _editor, juce::Component* _root, const juce::File& _dir) : jucePluginEditorLib::patchManager::PatchManager(_editor, _root, _dir), m_controller(_editor.getController()) + { + addRomPatches(); + + startLoaderThread(); + + // rom patches are received via midi, make sure we add all remaining ones, too + m_controller.onRomPatchReceived = [this](const virusLib::BankNumber _bank, const uint32_t _program) + { + if (_bank == virusLib::BankNumber::EditBuffer) + return; + + const auto index = virusLib::toArrayIndex(_bank); + + const auto& banks = m_controller.getSinglePresets(); + + if(index < banks.size()) + { + const auto& bank = banks[index]; + + if(_program == bank.size() - 1) + addDataSource(createRomDataSource(index)); + } + }; + addGroupTreeItemForTag(pluginLib::patchDB::TagType::CustomA, "Virus Model"); + addGroupTreeItemForTag(pluginLib::patchDB::TagType::CustomB, "Virus Features"); + } + + PatchManager::~PatchManager() + { + stopLoaderThread(); + m_controller.onRomPatchReceived = {}; + } + + bool PatchManager::loadRomData(pluginLib::patchDB::DataList& _results, const uint32_t _bank, const uint32_t _program) + { + const auto bankIndex = _bank; + + const auto& singles = m_controller.getSinglePresets(); + + if (bankIndex >= singles.size()) + return false; + + const auto& bank = singles[bankIndex]; + + if(_program != pluginLib::patchDB::g_invalidProgram) + { + if (_program >= bank.size()) + return false; + const auto& s = bank[_program]; + if (s.data.empty()) + return false; + _results.push_back(s.data); + } + else + { + _results.reserve(bank.size()); + for (const auto& patch : bank) + _results.push_back(patch.data); + } + return true; + } + + std::shared_ptr<pluginLib::patchDB::Patch> PatchManager::initializePatch(std::vector<uint8_t>&& _sysex) + { + if (_sysex.size() < 267) + return nullptr; + + const auto& c = static_cast<const Virus::Controller&>(m_controller); + + pluginLib::MidiPacket::Data data; + pluginLib::MidiPacket::AnyPartParamValues parameterValues; + + if (!c.parseSingle(data, parameterValues, _sysex)) + return nullptr; + + const auto idxVersion = c.getParameterIndexByName("Version"); + const auto idxCategory1 = c.getParameterIndexByName("Category1"); + const auto idxCategory2 = c.getParameterIndexByName("Category2"); + const auto idxUnison = c.getParameterIndexByName("Unison Mode"); +// const auto idxTranspose = c.getParameterIndexByName("Transpose"); + const auto idxArpMode = c.getParameterIndexByName("Arp Mode"); + const auto idxPhaserMix = c.getParameterIndexByName("Phaser Mix"); + const auto idxChorusMix = c.getParameterIndexByName("Chorus Mix"); + + auto patch = std::make_shared<pluginLib::patchDB::Patch>(); + + { + const auto it = data.find(pluginLib::MidiDataType::Bank); + if (it != data.end()) + patch->bank = it->second; + } + { + const auto it = data.find(pluginLib::MidiDataType::Program); + if (it != data.end()) + patch->program = it->second; + } + + { + constexpr auto frontOffset = 9; // remove bank number, program number and other stuff that we don't need, first index is the patch version + constexpr auto backOffset = 2; // remove f7 and checksum + const juce::MD5 md5(_sysex.data() + frontOffset, _sysex.size() - frontOffset - backOffset); + + static_assert(sizeof(juce::MD5) >= sizeof(pluginLib::patchDB::PatchHash)); + memcpy(patch->hash.data(), md5.getChecksumDataArray(), std::size(patch->hash)); + } + + patch->sysex = std::move(_sysex); + + patch->name = m_controller.getSinglePresetName(parameterValues); + + const auto version = virusLib::Microcontroller::getPresetVersion(*parameterValues[idxVersion]); + const auto unison = *parameterValues[idxUnison]; +// const auto transpose = parameterValues.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idxTranspose))->second; + const auto arpMode = *parameterValues[idxArpMode]; + + const auto category1 = *parameterValues[idxCategory1]; + const auto category2 = *parameterValues[idxCategory2]; + + const auto* paramCategory1 = c.getParameter(idxCategory1, 0); + const auto* paramCategory2 = c.getParameter(idxCategory2, 0); + + auto addCategory = [&patch, version](const uint8_t _value, const pluginLib::Parameter* _param) + { + if(!_value) + return; + const auto& values = _param->getDescription().valueList; + if(_value >= values.texts.size()) + return; + + // Virus < TI had less categories + if(version < virusLib::D && _value > 16) + return; + + const auto t = _param->getDescription().valueList.valueToText(_value); + patch->tags.add(pluginLib::patchDB::TagType::Category, t); + }; + + addCategory(category1, paramCategory1); + addCategory(category2, paramCategory2); + + switch (version) + { + case virusLib::A: patch->tags.add(pluginLib::patchDB::TagType::CustomA, "A"); break; + case virusLib::B: patch->tags.add(pluginLib::patchDB::TagType::CustomA, "B"); break; + case virusLib::C: patch->tags.add(pluginLib::patchDB::TagType::CustomA, "C"); break; + case virusLib::D: patch->tags.add(pluginLib::patchDB::TagType::CustomA, "TI"); break; + case virusLib::D2: patch->tags.add(pluginLib::patchDB::TagType::CustomA, "TI2"); break; + } + + if(arpMode) + patch->tags.add(pluginLib::patchDB::TagType::CustomB, "Arp"); + if(unison) + patch->tags.add(pluginLib::patchDB::TagType::CustomB, "Unison"); + if(*parameterValues[idxPhaserMix] > 0) + patch->tags.add(pluginLib::patchDB::TagType::CustomB, "Phaser"); + if(*parameterValues[idxChorusMix] > 0) + patch->tags.add(pluginLib::patchDB::TagType::CustomB, "Chorus"); + return patch; + } + + pluginLib::patchDB::Data PatchManager::prepareSave(const pluginLib::patchDB::PatchPtr& _patch) const + { + pluginLib::MidiPacket::Data data; + pluginLib::MidiPacket::AnyPartParamValues parameterValues; + + if (!m_controller.parseSingle(data, parameterValues, _patch->sysex)) + return _patch->sysex; + + // apply name + if (!_patch->getName().empty()) + m_controller.setSinglePresetName(parameterValues, _patch->getName()); + + // apply program + auto bank = toMidiByte(virusLib::BankNumber::A); + auto program = data[pluginLib::MidiDataType::Program]; + + if (_patch->program != pluginLib::patchDB::g_invalidProgram) + { + const auto bankOffset = _patch->program / 128; + program = static_cast<uint8_t>(_patch->program - bankOffset * 128); + bank += static_cast<uint8_t>(bankOffset); + } + + // apply categories + const uint32_t indicesCategory[] = { + m_controller.getParameterIndexByName("Category1"), + m_controller.getParameterIndexByName("Category2") + }; + + const pluginLib::Parameter* paramsCategory[] = { + m_controller.getParameter(indicesCategory[0], 0), + m_controller.getParameter(indicesCategory[1], 0) + }; + + uint8_t val0 = 0; + uint8_t val1 = 0; + + const auto& tags = _patch->getTags(pluginLib::patchDB::TagType::Category); + + size_t i = 0; + for (const auto& tag : tags.getAdded()) + { + const auto categoryValue = paramsCategory[i]->getDescription().valueList.textToValue(tag); + if(categoryValue != 0) + { + auto& v = i ? val1 : val0; + v = static_cast<uint8_t>(categoryValue); + ++i; + if (i == 2) + break; + } + } + + parameterValues[indicesCategory[0]] = val0; + parameterValues[indicesCategory[1]] = val1; + + return m_controller.createSingleDump(bank, program, parameterValues); + } + + bool PatchManager::parseFileData(pluginLib::patchDB::DataList& _results, const pluginLib::patchDB::Data& _data) + { + { + std::vector<synthLib::SMidiEvent> events; + virusLib::Device::parseTIcontrolPreset(events, _data); + + for (const auto& e : events) + { + if (!e.sysex.empty()) + _results.push_back(e.sysex); + } + + if (!_results.empty()) + return true; + } + + if (virusLib::Device::parsePowercorePreset(_results, _data)) + return true; + + if(!synthLib::MidiToSysex::extractSysexFromData(_results, _data)) + return false; + + if(!_results.empty()) + { + if(_data.size() > 500000) + { + virusLib::MidiFileToRomData romLoader; + + for (const auto& result : _results) + { + if(!romLoader.add(result)) + break; + } + if(romLoader.isComplete()) + { + const auto& data = romLoader.getData(); + + if(data.size() > 0x10000) + { + // presets are written to ROM address 0x50000, the second half of an OS update is therefore at 0x10000 + constexpr ptrdiff_t startAddr = 0x10000; + ptrdiff_t addr = startAddr; + uint32_t index = 0; + + while(addr + 0x100 <= static_cast<ptrdiff_t>(data.size())) + { + std::vector chunk(data.begin() + addr, data.begin() + addr + 0x100); + + // validate +// const auto idxH = chunk[2]; + const auto idxL = chunk[3]; + + if(/*idxH != (index >> 7) || */idxL != (index & 0x7f)) + break; + + bool validName = true; + for(size_t i=240; i<240+10; ++i) + { + if(chunk[i] < 32 || chunk[i] > 128) + { + validName = false; + break; + } + } + + if(!validName) + continue; + + addr += 0x100; + ++index; + } + + if(index > 0) + { + _results.clear(); + + for(uint32_t i=0; i<index; ++i) + { + // pack into sysex + std::vector<uint8_t>& sysex = _results.emplace_back(std::vector<uint8_t> + {0xf0, 0x00, 0x20, 0x33, 0x01, virusLib::OMNI_DEVICE_ID, 0x10, static_cast<uint8_t>(0x01 + (i >> 7)), static_cast<uint8_t>(i & 0x7f)} + ); + sysex.insert(sysex.end(), data.begin() + i * 0x100 + startAddr, data.begin() + i * 0x100 + 0x100 + startAddr); + sysex.push_back(virusLib::Microcontroller::calcChecksum(sysex, 5)); + sysex.push_back(0xf7); + } + } + } + } + } + + } + + return !_results.empty(); + } + + bool PatchManager::requestPatchForPart(pluginLib::patchDB::Data& _data, const uint32_t _part) + { + _data = m_controller.createSingleDump(static_cast<uint8_t>(_part), toMidiByte(virusLib::BankNumber::A), 0); + return !_data.empty(); + } + + uint32_t PatchManager::getCurrentPart() const + { + return m_controller.getCurrentPart(); + } + + bool PatchManager::equals(const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b) const + { + pluginLib::MidiPacket::Data dataA, dataB; + pluginLib::MidiPacket::AnyPartParamValues parameterValuesA, parameterValuesB; + + if (!m_controller.parseSingle(dataA, parameterValuesA, _a->sysex) || !m_controller.parseSingle(dataB, parameterValuesB, _b->sysex)) + return false; + + if(parameterValuesA.size() != parameterValuesB.size()) + return false; + + for(uint32_t i=0; i<parameterValuesA.size(); ++i) + { + const auto& itA = parameterValuesA[i]; + const auto& itB = parameterValuesB[i]; + + if(!itA) + { + if(itB) + return false; + continue; + } + + if(!itB) + return false; + + auto vA = *itA; + auto vB = *itB; + + if(vA != vB) + { + // parameters might be out of range because some dumps have values that are out of range indeed, clamp to valid range and compare again + const auto* param = m_controller.getParameter(i); + if(!param) + return false; + + if(param->getDescription().isNonPartSensitive()) + continue; + + vA = static_cast<uint8_t>(param->getDescription().range.clipValue(vA)); + vB = static_cast<uint8_t>(param->getDescription().range.clipValue(vB)); + + if(vA != vB) + return false; + } + } + return true; + } + + bool PatchManager::activatePatch(const pluginLib::patchDB::PatchPtr& _patch) + { + return m_controller.activatePatch(_patch->sysex); + } + + bool PatchManager::activatePatch(const pluginLib::patchDB::PatchPtr& _patch, uint32_t _part) + { + return m_controller.activatePatch(_patch->sysex, _part); + } + + void PatchManager::addRomPatches() + { + const auto& singles = m_controller.getSinglePresets(); + + for (uint32_t b = 0; b < singles.size(); ++b) + { + const auto& bank = singles[b]; + + const auto& single = bank[bank.size()-1]; + + if (single.data.empty()) + continue; + + addDataSource(createRomDataSource(b)); + } + } + + pluginLib::patchDB::DataSource PatchManager::createRomDataSource(const uint32_t _bank) const + { + pluginLib::patchDB::DataSource ds; + ds.type = pluginLib::patchDB::SourceType::Rom; + ds.bank = _bank; + ds.name = m_controller.getBankName(_bank); + return ds; + } + +} diff --git a/source/jucePlugin/ui3/PatchManager.h b/source/jucePlugin/ui3/PatchManager.h @@ -0,0 +1,44 @@ +#pragma once + +#include "../../jucePluginEditorLib/patchmanager/patchmanager.h" +#include "../../jucePluginLib/patchdb/patch.h" + +#include "../../virusLib/microcontrollerTypes.h" + +namespace Virus +{ + class Controller; +} + +namespace genericVirusUI +{ + class VirusEditor; + + class PatchManager : public jucePluginEditorLib::patchManager::PatchManager + { + public: + PatchManager(VirusEditor& _editor, juce::Component* _root, const juce::File& _dir); + ~PatchManager() override; + + void selectRomPreset(uint32_t _part, virusLib::BankNumber _bank, uint8_t _program); + + // PatchDB impl + bool loadRomData(pluginLib::patchDB::DataList& _results, uint32_t _bank, uint32_t _program) override; + std::shared_ptr<pluginLib::patchDB::Patch> initializePatch(std::vector<uint8_t>&& _sysex) override; + pluginLib::patchDB::Data prepareSave(const pluginLib::patchDB::PatchPtr& _patch) const override; + bool parseFileData(std::vector<std::vector<uint8_t>>& _results, const std::vector<uint8_t>& _data) override; + bool requestPatchForPart(pluginLib::patchDB::Data& _data, uint32_t _part) override; + uint32_t getCurrentPart() const override; + bool equals(const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b) const override; + + // PatchManager impl + bool activatePatch(const pluginLib::patchDB::PatchPtr& _patch) override; + bool activatePatch(const pluginLib::patchDB::PatchPtr& _patch, uint32_t _part) override; + + private: + void addRomPatches(); + pluginLib::patchDB::DataSource createRomDataSource(uint32_t _bank) const; + + Virus::Controller& m_controller; + }; +} diff --git a/source/jucePlugin/ui3/VirusEditor.cpp b/source/jucePlugin/ui3/VirusEditor.cpp @@ -1,6 +1,7 @@ #include "VirusEditor.h" #include "BinaryData.h" +#include "PartButton.h" #include "../ParameterNames.h" #include "../PluginProcessor.h" @@ -8,6 +9,7 @@ #include "../version.h" #include "../../jucePluginLib/parameterbinding.h" +#include "../../jucePluginEditorLib/patchmanager/savepatchdesc.h" #include "../../synthLib/os.h" @@ -37,7 +39,15 @@ namespace genericVirusUI if(!getConditionCountRecursive()) m_fxPage.reset(new FxPage(*this)); - m_patchBrowser.reset(new PatchBrowser(*this)); + const auto configOptions = getProcessor().getConfigOptions(); + const auto dir = configOptions.getDefaultFile().getParentDirectory(); + + { + auto pmParent = findComponent("ContainerPatchManager", false); + if(!pmParent) + pmParent = findComponent("page_presets"); + setPatchManager(new PatchManager(*this, pmParent, dir)); + } m_presetName = findComponentT<juce::Label>("PatchName"); @@ -69,7 +79,7 @@ namespace genericVirusUI m_romSelector->setSelectedId(1, juce::dontSendNotification); } - getController().onProgramChange = [this] { onProgramChange(); }; + getController().onProgramChange = [this](int _part) { onProgramChange(_part); }; addMouseListener(this, true); @@ -103,9 +113,14 @@ namespace genericVirusUI if (text.trim().length() > 0) { getController().setSinglePresetName(getController().getCurrentPart(), text); - onProgramChange(); + onProgramChange(getController().getCurrentPart()); } }; + m_presetNameMouseListener = new PartMouseListener(pluginLib::MidiPacket::AnyPart, [this](const juce::MouseEvent& _mouseEvent, int ) + { + startDragging(new jucePluginEditorLib::patchManager::SavePatchDesc(getController().getCurrentPart()), m_presetName); + }); + m_presetName->addMouseListener(m_presetNameMouseListener, false); auto* menuButton = findComponentT<juce::Button>("Menu", false); @@ -118,6 +133,10 @@ namespace genericVirusUI VirusEditor::~VirusEditor() { + m_presetName->removeMouseListener(m_presetNameMouseListener); + delete m_presetNameMouseListener; + m_presetNameMouseListener = nullptr; + m_focusedParameter.reset(); m_parameterBinding.clearBindings(); @@ -130,6 +149,18 @@ namespace genericVirusUI return static_cast<Virus::Controller&>(m_processor.getController()); } + void VirusEditor::selectRomPreset(const uint8_t _part, const virusLib::BankNumber _bank, const uint8_t _program) const + { + if(getPatchManager()) + { + static_cast<PatchManager*>(getPatchManager())->selectRomPreset(_part, _bank, _program); + } + else + { + getController().setCurrentPartPreset(_part, _bank, _program); + } + } + const char* VirusEditor::findEmbeddedResource(const std::string& _filename, uint32_t& _size) { for(size_t i=0; i<BinaryData::namedResourceListSize; ++i) @@ -150,17 +181,32 @@ namespace genericVirusUI return findEmbeddedResource(_filename, _size); } - PatchBrowser* VirusEditor::getPatchBrowser() + std::pair<std::string, std::string> VirusEditor::getDemoRestrictionText() const + { + return { + JucePlugin_Name " - Demo Mode", + JucePlugin_Name " runs in demo mode, the following restrictions apply:\n" + "\n" + "* The plugin state is not preserved\n" + "* Preset saving is disabled"}; + } + + genericUI::Button<juce::TextButton>* VirusEditor::createJuceComponent(genericUI::Button<juce::TextButton>* _button, genericUI::UiObject& _object) { - return m_patchBrowser.get(); + if(_object.getName() == "PresetName") + return new PartButton(*this); + + return Editor::createJuceComponent(_button, _object); } - void VirusEditor::onProgramChange() + void VirusEditor::onProgramChange(int _part) { m_parts->onProgramChange(); updatePresetName(); updatePlayModeButtons(); updateDeviceModel(); + if(getPatchManager()) + getPatchManager()->onProgramChanged(_part); } void VirusEditor::onPlayModeChanged() @@ -226,24 +272,24 @@ namespace genericVirusUI { juce::PopupMenu menu; - auto addEntry = [&](juce::PopupMenu& _menu, const std::string& _name, const std::function<void(FileType)>& _callback) + auto addEntry = [&](juce::PopupMenu& _menu, const std::string& _name, const std::function<void(jucePluginEditorLib::FileType)>& _callback) { juce::PopupMenu subMenu; - subMenu.addItem(".syx", [_callback](){_callback(FileType::Syx); }); - subMenu.addItem(".mid", [_callback](){_callback(FileType::Mid); }); + subMenu.addItem(".syx", [_callback](){_callback(jucePluginEditorLib::FileType::Syx); }); + subMenu.addItem(".mid", [_callback](){_callback(jucePluginEditorLib::FileType::Mid); }); _menu.addSubMenu(_name, subMenu); }; - addEntry(menu, "Current Single (Edit Buffer)", [this](FileType _type) + addEntry(menu, "Current Single (Edit Buffer)", [this](jucePluginEditorLib::FileType _type) { savePresets(SaveType::CurrentSingle, _type); }); if(getController().isMultiMode()) { - addEntry(menu, "Arrangement (Multi + 16 Singles)", [this](FileType _type) + addEntry(menu, "Arrangement (Multi + 16 Singles)", [this](jucePluginEditorLib::FileType _type) { savePresets(SaveType::Arrangement, _type); }); @@ -252,7 +298,7 @@ namespace genericVirusUI juce::PopupMenu banksMenu; for(uint8_t b=0; b<static_cast<uint8_t>(getController().getBankCount()); ++b) { - addEntry(banksMenu, getController().getBankName(b), [this, b](const FileType _type) + addEntry(banksMenu, getController().getBankName(b), [this, b](const jucePluginEditorLib::FileType _type) { savePresets(SaveType::Bank, _type, b); }); @@ -267,33 +313,37 @@ namespace genericVirusUI { Editor::loadPreset([this](const juce::File& _result) { - const auto ext = _result.getFileExtension().toLowerCase(); + pluginLib::patchDB::DataList results; - PatchBrowser::PatchList patches; + if(!getPatchManager()->loadFile(results, _result.getFullPathName().toStdString())) + return; - m_patchBrowser->loadBankFile(patches, nullptr, _result); + auto& c = getController(); - if (patches.empty()) - return; + // we attempt to convert all results as some of them might not be valid preset data + for(size_t i=0; i<results.size();) + { + // convert to load to edit buffer of current part + const auto data = c.modifySingleDump(results[i], virusLib::BankNumber::EditBuffer, c.isMultiMode() ? c.getCurrentPart() : virusLib::SINGLE); + if(data.empty()) + results.erase(results.begin() + i); + else + results[i++] = data; + } - if (patches.size() == 1) + if (results.size() == 1) { - // load to edit buffer of current part - const auto data = getController().modifySingleDump(patches.front()->sysex, virusLib::BankNumber::EditBuffer, - getController().isMultiMode() ? getController().getCurrentPart() : virusLib::SINGLE, true, true); - getController().sendSysEx(data); + c.sendSysEx(results.front()); } - else + else if(results.size() > 1) { - // 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); - getController().sendSysEx(data); - } + juce::NativeMessageBox::showMessageBox(juce::AlertWindow::InfoIcon, "Information", + "The selected file contains more than one patch. Please add this file as a data source in the Patch Manager instead.\n\n" + "Go to the Patch Manager, right click the 'Data Sources' node and select 'Add File...' to import it." + ); } - getController().onStateLoaded(); + c.onStateLoaded(); }); } @@ -302,7 +352,7 @@ namespace genericVirusUI const auto playMode = getController().getParameterIndexByName(Virus::g_paramPlayMode); auto* param = getController().getParameter(playMode); - param->setValue(_playMode, pluginLib::Parameter::ChangedBy::Ui); + param->setValue(param->convertTo0to1(_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); @@ -315,18 +365,21 @@ namespace genericVirusUI getController().requestArrangement(); } - void VirusEditor::savePresets(SaveType _saveType, FileType _fileType, uint8_t _bankNumber/* = 0*/) + void VirusEditor::savePresets(SaveType _saveType, jucePluginEditorLib::FileType _fileType, uint8_t _bankNumber/* = 0*/) { Editor::savePreset([this, _saveType, _bankNumber, _fileType](const juce::File& _result) { - FileType fileType = _fileType; + jucePluginEditorLib::FileType fileType = _fileType; const auto file = createValidFilename(fileType, _result); savePresets(file, _saveType, fileType, _bankNumber); }); } - bool VirusEditor::savePresets(const std::string& _pathName, SaveType _saveType, FileType _fileType, uint8_t _bankNumber/* = 0*/) const + bool VirusEditor::savePresets(const std::string& _pathName, SaveType _saveType, jucePluginEditorLib::FileType _fileType, uint8_t _bankNumber/* = 0*/) const { +#if SYNTHLIB_DEMO_MODE + return false; +#else std::vector< std::vector<uint8_t> > messages; switch (_saveType) @@ -364,6 +417,7 @@ namespace genericVirusUI } return Editor::savePresets(_fileType, _pathName, messages); +#endif } void VirusEditor::setPart(size_t _part) diff --git a/source/jucePlugin/ui3/VirusEditor.h b/source/jucePlugin/ui3/VirusEditor.h @@ -7,7 +7,7 @@ #include "Parts.h" #include "Tabs.h" #include "FxPage.h" -#include "PatchBrowser.h" +#include "PatchManager.h" #include "ControllerLinks.h" namespace jucePluginEditorLib @@ -41,18 +41,21 @@ namespace genericVirusUI void setPart(size_t _part); - AudioPluginAudioProcessor& getProcessor() const { return m_processor; } pluginLib::ParameterBinding& getParameterBinding() const { return m_parameterBinding; } Virus::Controller& getController() const; + void selectRomPreset(uint8_t _part, virusLib::BankNumber _bank, uint8_t _program) const; + static const char* findEmbeddedResource(const std::string& _filename, uint32_t& _size); const char* findResourceByFilename(const std::string& _filename, uint32_t& _size) override; - PatchBrowser* getPatchBrowser(); + std::pair<std::string, std::string> getDemoRestrictionText() const override; + + genericUI::Button<juce::TextButton>* createJuceComponent(genericUI::Button<juce::TextButton>*, genericUI::UiObject& _object) override; private: - void onProgramChange(); + void onProgramChange(int _part); void onPlayModeChanged(); void onCurrentPartChanged(); @@ -68,8 +71,8 @@ namespace genericVirusUI void setPlayMode(uint8_t _playMode); - void savePresets(SaveType _saveType, FileType _fileType, uint8_t _bankNumber = 0); - bool savePresets(const std::string& _pathName, SaveType _saveType, FileType _fileType, uint8_t _bankNumber = 0) const; + void savePresets(SaveType _saveType, jucePluginEditorLib::FileType _fileType, uint8_t _bankNumber = 0); + bool savePresets(const std::string& _pathName, SaveType _saveType, jucePluginEditorLib::FileType _fileType, uint8_t _bankNumber = 0) const; AudioPluginAudioProcessor& m_processor; pluginLib::ParameterBinding& m_parameterBinding; @@ -78,10 +81,10 @@ namespace genericVirusUI std::unique_ptr<Tabs> m_tabs; 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; juce::Label* m_presetName = nullptr; + PartMouseListener* m_presetNameMouseListener = nullptr; std::unique_ptr<jucePluginEditorLib::FocusedParameter> m_focusedParameter; @@ -93,8 +96,6 @@ namespace genericVirusUI juce::Label* m_deviceModel = nullptr; - juce::TooltipWindow m_tooltipWindow; - std::function<void()> m_openMenuCallback; }; } diff --git a/source/jucePluginEditorLib/CMakeLists.txt b/source/jucePluginEditorLib/CMakeLists.txt @@ -5,17 +5,45 @@ set(SOURCES focusedParameter.cpp focusedParameter.h focusedParameterTooltip.cpp focusedParameterTooltip.h midiPorts.cpp midiPorts.h - patchbrowser.cpp patchbrowser.h + partbutton.cpp partbutton.h pluginEditor.cpp pluginEditor.h pluginEditorWindow.cpp pluginEditorWindow.h pluginEditorState.cpp pluginEditorState.h pluginProcessor.cpp pluginProcessor.h + types.h +) + +set(SOURCES_PM + patchmanager/datasourcetree.cpp patchmanager/datasourcetree.h + patchmanager/datasourcetreeitem.cpp patchmanager/datasourcetreeitem.h + patchmanager/defaultskin.h + patchmanager/editable.cpp patchmanager/editable.h + patchmanager/grouptreeitem.cpp patchmanager/grouptreeitem.h + patchmanager/info.cpp patchmanager/info.h + patchmanager/list.cpp patchmanager/list.h + patchmanager/listitem.cpp patchmanager/listitem.h + patchmanager/notagtreeitem.cpp patchmanager/notagtreeitem.h + patchmanager/patchmanager.cpp patchmanager/patchmanager.h + patchmanager/resizerbar.cpp patchmanager/resizerbar.h + patchmanager/roottreeitem.cpp patchmanager/roottreeitem.h + patchmanager/savepatchdesc.cpp patchmanager/savepatchdesc.h + patchmanager/tagtreeitem.cpp patchmanager/tagtreeitem.h + patchmanager/tagstree.cpp patchmanager/tagstree.h + patchmanager/tree.cpp patchmanager/tree.h + patchmanager/treeitem.cpp patchmanager/treeitem.h + patchmanager/types.cpp patchmanager/types.h + patchmanager/search.cpp patchmanager/search.h + patchmanager/searchlist.cpp patchmanager/searchlist.h + patchmanager/searchtree.cpp patchmanager/searchtree.h + patchmanager/state.cpp patchmanager/state.h + patchmanager/status.cpp patchmanager/status.h ) add_library(jucePluginEditorLib STATIC) -target_sources(jucePluginEditorLib PRIVATE ${SOURCES}) +target_sources(jucePluginEditorLib PRIVATE ${SOURCES} ${SOURCES_PM}) source_group("source" FILES ${SOURCES}) +source_group("source\\patchmanager" FILES ${SOURCES_PM}) target_link_libraries(jucePluginEditorLib PUBLIC jucePluginLib juceUiLib) target_include_directories(jucePluginEditorLib PUBLIC ../JUCE/modules) diff --git a/source/jucePluginEditorLib/focusedParameter.h b/source/jucePluginEditorLib/focusedParameter.h @@ -2,6 +2,13 @@ #include "focusedParameterTooltip.h" +#include "juce_events/juce_events.h" + +namespace juce +{ + class MouseEvent; +} + namespace genericUI { class Editor; diff --git a/source/jucePluginEditorLib/focusedParameterTooltip.cpp b/source/jucePluginEditorLib/focusedParameterTooltip.cpp @@ -1,5 +1,7 @@ #include "focusedParameterTooltip.h" +#include "juce_gui_basics/juce_gui_basics.h" + namespace jucePluginEditorLib { FocusedParameterTooltip::FocusedParameterTooltip(juce::Label* _label) : m_label(_label) diff --git a/source/jucePluginEditorLib/focusedParameterTooltip.h b/source/jucePluginEditorLib/focusedParameterTooltip.h @@ -1,6 +1,11 @@ #pragma once -#include <juce_audio_processors/juce_audio_processors.h> +namespace juce +{ + class String; + class Component; + class Label; +} namespace jucePluginEditorLib { diff --git a/source/jucePluginEditorLib/partbutton.cpp b/source/jucePluginEditorLib/partbutton.cpp @@ -0,0 +1,116 @@ +#include "partbutton.h" + +#include "pluginEditor.h" +#include "patchmanager/list.h" +#include "patchmanager/patchmanager.h" +#include "patchmanager/savepatchdesc.h" + +namespace jucePluginEditorLib +{ + namespace + { + std::pair<pluginLib::patchDB::PatchPtr, patchManager::List*> getPatchFromDragSource(const juce::DragAndDropTarget::SourceDetails& _source) + { + auto* list = dynamic_cast<patchManager::List*>(_source.sourceComponent.get()); + if(!list) + return {}; + + const auto patches = patchManager::List::getPatchesFromDragSource(_source); + + if (patches.size() != 1) + return {}; + + return {patches.front(), list}; + } + } + + template <typename T> bool PartButton<T>::isInterestedInDragSource(const SourceDetails& _dragSourceDetails) + { + const auto* savePatchDesc = patchManager::SavePatchDesc::fromDragSource(_dragSourceDetails); + + if(savePatchDesc) + return savePatchDesc->getPart() != getPart(); + + const auto patch = getPatchFromDragSource(_dragSourceDetails); + return patch.first != nullptr; + } + + template <typename T> void PartButton<T>::itemDragEnter(const SourceDetails& dragSourceDetails) + { + if(isInterestedInDragSource(dragSourceDetails)) + setIsDragTarget(true); + } + + template <typename T> void PartButton<T>::itemDragExit(const SourceDetails& _dragSourceDetails) + { + DragAndDropTarget::itemDragExit(_dragSourceDetails); + setIsDragTarget(false); + } + + template <typename T> void PartButton<T>::itemDropped(const SourceDetails& _dragSourceDetails) + { + setIsDragTarget(false); + + const auto* savePatchDesc = patchManager::SavePatchDesc::fromDragSource(_dragSourceDetails); + + auto* pm = m_editor.getPatchManager(); + + if(savePatchDesc) + { + if(savePatchDesc->getPart() == m_part) + return; + pm->copyPart(m_part, static_cast<uint8_t>(savePatchDesc->getPart())); + } + + const auto [patch, list] = getPatchFromDragSource(_dragSourceDetails); + if(!patch) + return; + + if(pm->getCurrentPart() == m_part) + { + list->setSelectedPatches({patch}); + list->activateSelectedPatch(); + } + else + { + pm->setSelectedPatch(m_part, patch, list->getSearchHandle()); + } + } + + template <typename T> void PartButton<T>::paint(juce::Graphics& g) + { + genericUI::Button<T>::paint(g); + + if(m_isDragTarget) + { + g.setColour(juce::Colour(0xff00ff00)); + g.drawRect(0, 0, genericUI::Button<T>::getWidth(), genericUI::Button<T>::getHeight(), 3); + } + } + + template <typename T> void PartButton<T>::mouseDrag(const juce::MouseEvent& _event) + { + m_editor.startDragging(new patchManager::SavePatchDesc(m_part), this); + genericUI::Button<T>::mouseDrag(_event); + } + + template <typename T> void PartButton<T>::mouseUp(const juce::MouseEvent& _event) + { + if(!_event.mods.isPopupMenu() && genericUI::Button<T>::isDown() && genericUI::Button<T>::isOver()) + onClick(); + + genericUI::Button<T>::mouseUp(_event); + } + + template <typename T> void PartButton<T>::setIsDragTarget(const bool _isDragTarget) + { + if(m_isDragTarget == _isDragTarget) + return; + m_isDragTarget = _isDragTarget; + genericUI::Button<T>::repaint(); + } + + template class PartButton<juce::TextButton>; + template class PartButton<juce::DrawableButton>; + template class PartButton<juce::ImageButton>; +} diff --git a/source/jucePluginEditorLib/partbutton.h b/source/jucePluginEditorLib/partbutton.h @@ -0,0 +1,56 @@ +#pragma once + +#include <cstdint> + +#include "../juceUiLib/button.h" + +namespace jucePluginEditorLib +{ + namespace patchManager + { + class List; + } + + class Editor; + + template<typename T> + class PartButton : public genericUI::Button<T>, public juce::DragAndDropTarget + { + public: + template<class... TArgs> + explicit PartButton(Editor& _editor, const TArgs&... _args) : genericUI::Button<T>(_args...) , m_editor(_editor) + { + } + + void initalize(const uint8_t _part) + { + m_part = _part; + } + + auto getPart() const + { + return m_part; + } + + bool isInterestedInDragSource(const SourceDetails& _dragSourceDetails) override; + + void itemDragEnter(const SourceDetails& dragSourceDetails) override; + void itemDragExit(const SourceDetails& _dragSourceDetails) override; + void itemDropped(const SourceDetails& _dragSourceDetails) override; + + void paint(juce::Graphics& g) override; + + void mouseDrag(const juce::MouseEvent& _event) override; + void mouseUp(const juce::MouseEvent& _event) override; + + virtual void onClick() {} + + private: + void setIsDragTarget(const bool _isDragTarget); + + + Editor& m_editor; + uint8_t m_part = 0xff; + bool m_isDragTarget = false; + }; +} diff --git a/source/jucePluginEditorLib/patchbrowser.cpp b/source/jucePluginEditorLib/patchbrowser.cpp @@ -1,380 +0,0 @@ -#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;*.vstpreset;*.fxb;*.fxp", "*", "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 = std::shared_ptr<Patch>(createPatch()); - patch->sysex = _data; - patch->progNumber = static_cast<int>(_result.size()); - - if(!initializePatch(*patch)) - return false; - - if (!_dedupeChecksums) - { - _result.emplace_back(std::move(patch)); - } - else - { - const auto md5 = std::string(getChecksum(*patch).toHexString().toRawUTF8()); - - if (_dedupeChecksums->find(md5) == _dedupeChecksums->end()) - { - _dedupeChecksums->insert(md5); - _result.emplace_back(std::move(patch)); - } - } - - return true; - } - - bool PatchBrowser::loadUnkownData(std::vector<std::vector<unsigned char>>& _result, const std::string& _filename) - { - synthLib::MidiToSysex::extractSysexFromFile(_result, _filename); - return !_result.empty(); - } - - 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); - } - - std::vector<std::vector<uint8_t>> packets; - if(!loadUnkownData(packets, file.getFullPathName().toStdString())) - return false; - return load(_result, _dedupeChecksums, packets); - } - - 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" || ext == ".fxb" || ext == ".fxp" || ext == ".vstpreset") - { - 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 m_forward ? compareElements(*_a, *_b) < 0 : compareElements(*_a, *_b) > 0; - } - - 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 @@ -1,99 +0,0 @@ -#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); - virtual bool loadUnkownData(std::vector<std::vector<uint8_t>>& _result, const std::string& _filename); - 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/patchmanager/datasourcetree.cpp b/source/jucePluginEditorLib/patchmanager/datasourcetree.cpp @@ -0,0 +1,12 @@ +#include "datasourcetree.h" + +namespace jucePluginEditorLib::patchManager +{ + DatasourceTree::DatasourceTree(PatchManager& _pm): Tree(_pm) + { + addGroup(GroupType::Favourites); + addGroup(GroupType::LocalStorage); + addGroup(GroupType::Factory); + addGroup(GroupType::DataSources); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/datasourcetree.h b/source/jucePluginEditorLib/patchmanager/datasourcetree.h @@ -0,0 +1,12 @@ +#pragma once + +#include "tree.h" + +namespace jucePluginEditorLib::patchManager +{ + class DatasourceTree : public Tree + { + public: + explicit DatasourceTree(PatchManager& _pm); + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/datasourcetreeitem.cpp b/source/jucePluginEditorLib/patchmanager/datasourcetreeitem.cpp @@ -0,0 +1,200 @@ +#include "datasourcetreeitem.h" + +#include <cassert> + +#include "patchmanager.h" +#include "../pluginEditor.h" + +#include "../../jucePluginLib/patchdb/datasource.h" +#include "../../jucePluginLib/patchdb/search.h" + +#include "../../synthLib/buildconfig.h" + +namespace jucePluginEditorLib::patchManager +{ + namespace + { + std::string getDataSourceTitle(const pluginLib::patchDB::DataSource& _ds) + { + switch (_ds.type) + { + case pluginLib::patchDB::SourceType::Invalid: + case pluginLib::patchDB::SourceType::Count: + return {}; + case pluginLib::patchDB::SourceType::Rom: + case pluginLib::patchDB::SourceType::File: + case pluginLib::patchDB::SourceType::Folder: + case pluginLib::patchDB::SourceType::LocalStorage: + return _ds.name; + default: + assert(false); + return"invalid"; + } + } + + std::string getDataSourceNodeTitle(const pluginLib::patchDB::DataSourceNodePtr& _ds) + { + if (_ds->origin == pluginLib::patchDB::DataSourceOrigin::Manual) + return getDataSourceTitle(*_ds); + + auto t = getDataSourceTitle(*_ds); + const auto pos = t.find_last_of("\\/"); + if (pos != std::string::npos) + return t.substr(pos + 1); + return t; + } + } + + DatasourceTreeItem::DatasourceTreeItem(PatchManager& _pm, const pluginLib::patchDB::DataSourceNodePtr& _ds) : TreeItem(_pm,{}), m_dataSource(_ds) + { + setTitle(getDataSourceNodeTitle(_ds)); + + pluginLib::patchDB::SearchRequest sr; + sr.sourceNode = _ds; + search(std::move(sr)); + } + + bool DatasourceTreeItem::isInterestedInSavePatchDesc(const SavePatchDesc& _desc) + { + return m_dataSource->type == pluginLib::patchDB::SourceType::LocalStorage; + } + + bool DatasourceTreeItem::isInterestedInPatchList(const List* _list, const juce::Array<juce::var>& _indices) + { + return m_dataSource->type == pluginLib::patchDB::SourceType::LocalStorage; + } + + void DatasourceTreeItem::patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) + { + TreeItem::patchesDropped(_patches); + + if (m_dataSource->type != pluginLib::patchDB::SourceType::LocalStorage) + return; + + if (juce::ModifierKeys::currentModifiers.isShiftDown()) + { + if(List::showDeleteConfirmationMessageBox()) + getPatchManager().removePatches(m_dataSource, _patches); + } + else + { +#if SYNTHLIB_DEMO_MODE + getPatchManager().getEditor().showDemoRestrictionMessageBox(); +#else + getPatchManager().copyPatchesTo(m_dataSource, _patches); +#endif + } + } + + void DatasourceTreeItem::itemClicked(const juce::MouseEvent& _mouseEvent) + { + if(!_mouseEvent.mods.isPopupMenu()) + return; + + juce::PopupMenu menu; + + menu.addItem("Refresh", [this] + { + getPatchManager().refreshDataSource(m_dataSource); + }); + + if(m_dataSource->type == pluginLib::patchDB::SourceType::File || + m_dataSource->type == pluginLib::patchDB::SourceType::Folder || + m_dataSource->type == pluginLib::patchDB::SourceType::LocalStorage) + { + menu.addItem("Remove", [this] + { + getPatchManager().removeDataSource(*m_dataSource); + }); + } + if(m_dataSource->type == pluginLib::patchDB::SourceType::LocalStorage) + { + menu.addItem("Delete", [this] + { + if(1 == juce::NativeMessageBox::showYesNoBox(juce::AlertWindow::WarningIcon, + "Patch Manager", + "Are you sure that you want to delete your user bank named '" + getDataSource()->name + "'?")) + getPatchManager().removeDataSource(*m_dataSource); + }); + } + if(m_dataSource->type == pluginLib::patchDB::SourceType::LocalStorage) + { + menu.addItem("Rename...", [this] + { + beginEdit(); + }); + + auto fileTypeMenu = [this](const std::function<void(FileType)>& _func) + { + juce::PopupMenu menu; + menu.addItem(".syx", [this, _func]{_func(FileType::Syx);}); + menu.addItem(".mid", [this, _func]{_func(FileType::Mid);}); + return menu; + }; + + menu.addSubMenu("Export...", fileTypeMenu([this](const FileType _fileType) + { + const auto s = getPatchManager().getSearch(getSearchHandle()); + if(s) + { + std::vector patches(s->results.begin(), s->results.end()); + getPatchManager().exportPresets(std::move(patches), _fileType); + } + })); + } + menu.showMenuAsync({}); + } + + void DatasourceTreeItem::refresh() + { + setTitle(getDataSourceNodeTitle(m_dataSource)); + } + + int DatasourceTreeItem::compareElements(const TreeViewItem* _a, const TreeViewItem* _b) + { + const auto* a = dynamic_cast<const DatasourceTreeItem*>(_a); + const auto* b = dynamic_cast<const DatasourceTreeItem*>(_b); + + if (!a || !b) + return TreeItem::compareElements(_a, _b); + + const auto& dsA = a->m_dataSource; + const auto& dsB = b->m_dataSource; + + if (dsA->type != dsB->type) + return static_cast<int>(dsA->type) - static_cast<int>(dsB->type); + + return TreeItem::compareElements(_a, _b); + } + + bool DatasourceTreeItem::beginEdit() + { + if(m_dataSource->type != pluginLib::patchDB::SourceType::LocalStorage) + return TreeItem::beginEdit(); + + static_cast<TreeItem&>(*this).beginEdit(m_dataSource->name, [this](bool _success, const std::string& _newName) + { + if(_newName != m_dataSource->name) + { + getPatchManager().renameDataSource(m_dataSource, _newName); + } + }); + return true; + } + + juce::String DatasourceTreeItem::getTooltip() + { + const auto& ds = getDataSource(); + if(!ds) + return {}; + switch (ds->type) + { + case pluginLib::patchDB::SourceType::Invalid: + case pluginLib::patchDB::SourceType::Rom: + case pluginLib::patchDB::SourceType::Count: + return{}; + default: + return ds->name; + } + } +} diff --git a/source/jucePluginEditorLib/patchmanager/datasourcetreeitem.h b/source/jucePluginEditorLib/patchmanager/datasourcetreeitem.h @@ -0,0 +1,40 @@ +#pragma once +#include "treeitem.h" +#include "../../jucePluginLib/patchdb/datasource.h" + +namespace pluginLib::patchDB +{ + struct DataSource; +} + +namespace jucePluginEditorLib::patchManager +{ + class DatasourceTreeItem : public TreeItem + { + public: + DatasourceTreeItem(PatchManager& _pm, const pluginLib::patchDB::DataSourceNodePtr& _ds); + + bool mightContainSubItems() override + { + return m_dataSource->type == pluginLib::patchDB::SourceType::Folder; + } + + bool isInterestedInSavePatchDesc(const SavePatchDesc& _desc) override; + bool isInterestedInPatchList(const List* _list, const juce::Array<juce::var>& _indices) override; + + void patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) override; + + void itemClicked(const juce::MouseEvent&) override; + void refresh(); + + int compareElements(const TreeViewItem* _a, const TreeViewItem* _b) override; + + bool beginEdit() override; + + const auto& getDataSource() const { return m_dataSource; } + + juce::String getTooltip() override; + private: + const pluginLib::patchDB::DataSourceNodePtr m_dataSource; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/defaultskin.h b/source/jucePluginEditorLib/patchmanager/defaultskin.h @@ -0,0 +1,19 @@ +#pragma once + +#include <cstdint> + +namespace jucePluginEditorLib::patchManager +{ + namespace defaultSkin::colors + { + constexpr uint32_t background = 0xff222222; + constexpr uint32_t selectedItem = 0xff444444; + constexpr uint32_t itemText = 0xffeeeeee; + constexpr uint32_t textEditOutline = selectedItem; + constexpr uint32_t infoLabel = 0xff999999; + constexpr uint32_t infoText = itemText; + constexpr uint32_t infoHeadline = infoText; + constexpr uint32_t statusText = itemText; + constexpr uint32_t scrollbar = 0xff999999; + } +} diff --git a/source/jucePluginEditorLib/patchmanager/editable.cpp b/source/jucePluginEditorLib/patchmanager/editable.cpp @@ -0,0 +1,64 @@ +#include "editable.h" + +namespace jucePluginEditorLib::patchManager +{ + Editable::~Editable() + { + destroyEditorLabel(); + } + + bool Editable::beginEdit(juce::Component* _parent, const juce::Rectangle<int>& _position, const std::string& _initialText, FinishedEditingCallback&& _callback) + { + if (m_editorLabel) + return false; + + m_editorInitialText = _initialText; + m_editorLabel = new juce::Label({}, _initialText); + + const auto pos = _position; + + m_editorLabel->setTopLeftPosition(pos.getTopLeft()); + m_editorLabel->setSize(pos.getWidth(), pos.getHeight()); + + m_editorLabel->setEditable(true, true, true); + m_editorLabel->setColour(juce::Label::backgroundColourId, juce::Colour(0xff333333)); + + m_editorLabel->addListener(this); + + _parent->addAndMakeVisible(m_editorLabel); + + m_editorLabel->showEditor(); + + m_finishedEditingCallback = std::move(_callback); + + return true; + } + + void Editable::editorHidden(juce::Label* _label, juce::TextEditor& _textEditor) + { + if (m_editorLabel) + { + const auto text = m_editorLabel->getText().toStdString(); + if(text != m_editorInitialText) + m_finishedEditingCallback(true, text); + destroyEditorLabel(); + } + Listener::editorHidden(_label, _textEditor); + } + + void Editable::labelTextChanged(juce::Label* _label) + { + } + + void Editable::destroyEditorLabel() + { + if (!m_editorLabel) + return; + + m_editorLabel->getParentComponent()->removeChildComponent(m_editorLabel); + delete m_editorLabel; + m_editorLabel = nullptr; + + m_finishedEditingCallback = {}; + } +} diff --git a/source/jucePluginEditorLib/patchmanager/editable.h b/source/jucePluginEditorLib/patchmanager/editable.h @@ -0,0 +1,31 @@ +#pragma once + +#include <functional> +#include <string> + +#include "juce_gui_basics/juce_gui_basics.h" + +namespace jucePluginEditorLib::patchManager +{ + class Editable : juce::Label::Listener + { + public: + using FinishedEditingCallback = std::function<void(bool, const std::string&)>; + + ~Editable() override; + + protected: + bool beginEdit(juce::Component* _parent, const juce::Rectangle<int>& _position, const std::string& _initialText, FinishedEditingCallback&& _callback); + + // juce::Label::Listener + void editorHidden(juce::Label*, juce::TextEditor&) override; + void labelTextChanged(juce::Label* _label) override; + + private: + void destroyEditorLabel(); + + FinishedEditingCallback m_finishedEditingCallback; + juce::Label* m_editorLabel = nullptr; + std::string m_editorInitialText; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/grouptreeitem.cpp b/source/jucePluginEditorLib/patchmanager/grouptreeitem.cpp @@ -0,0 +1,339 @@ +#include "grouptreeitem.h" + +#include "datasourcetreeitem.h" +#include "patchmanager.h" +#include "search.h" +#include "tagtreeitem.h" + +namespace jucePluginEditorLib::patchManager +{ + class DatasourceTreeItem; + + GroupTreeItem::GroupTreeItem(PatchManager& _pm, const GroupType _type, const std::string& _groupName) : TreeItem(_pm, _groupName), m_type(_type) + { + onParentSearchChanged({}); + } + + void GroupTreeItem::updateFromTags(const std::set<std::string>& _tags) + { + for(auto it = m_itemsByTag.begin(); it != m_itemsByTag.end();) + { + const auto tag = it->first; + const auto* item = it->second; + + if(_tags.find(tag) == _tags.end()) + { + item->removeFromParent(true); + m_itemsByTag.erase(it++); + } + else + { + ++it; + } + } + + for (const auto& tag : _tags) + { + auto itExisting = m_itemsByTag.find(tag); + + if (itExisting != m_itemsByTag.end()) + { + validateParent(itExisting->second); + continue; + } + + const auto oldNumSubItems = getNumSubItems(); + createSubItem(tag); + + if (getNumSubItems() == 1 && oldNumSubItems == 0) + setOpen(true); + } + } + + void GroupTreeItem::removeItem(const DatasourceTreeItem* _item) + { + if (!_item) + return; + + for(auto it = m_itemsByDataSource.begin(); it != m_itemsByDataSource.end(); ++it) + { + if (it->second == _item) + { + m_itemsByDataSource.erase(it); + break; + } + } + + while (_item->getNumSubItems()) + removeItem(dynamic_cast<DatasourceTreeItem*>(_item->getSubItem(0))); + + _item->removeFromParent(true); + } + + void GroupTreeItem::removeDataSource(const pluginLib::patchDB::DataSourceNodePtr& _ds) + { + const auto it = m_itemsByDataSource.find(_ds); + if (it == m_itemsByDataSource.end()) + return; + removeItem(it->second); + } + + void GroupTreeItem::updateFromDataSources(const std::vector<pluginLib::patchDB::DataSourceNodePtr>& _dataSources) + { + const auto previousItems = m_itemsByDataSource; + + for (const auto& previousItem : previousItems) + { + const auto& ds = previousItem.first; + + if (std::find(_dataSources.begin(), _dataSources.end(), ds) == _dataSources.end()) + removeDataSource(ds); + } + + for (const auto& d : _dataSources) + { + const auto itExisting = m_itemsByDataSource.find(d); + + if (m_itemsByDataSource.find(d) != m_itemsByDataSource.end()) + { + validateParent(itExisting->first, itExisting->second); + itExisting->second->refresh(); + continue; + } + + auto* item = createItemForDataSource(d); + + m_itemsByDataSource.insert({ d, item }); + } + } + + void GroupTreeItem::processDirty(const std::set<pluginLib::patchDB::SearchHandle>& _dirtySearches) + { + for (const auto& it : m_itemsByTag) + it.second->processDirty(_dirtySearches); + + for (const auto& it : m_itemsByDataSource) + it.second->processDirty(_dirtySearches); + + TreeItem::processDirty(_dirtySearches); + } + + void GroupTreeItem::itemClicked(const juce::MouseEvent& _mouseEvent) + { + if(_mouseEvent.mods.isPopupMenu()) + { + TreeItem::itemClicked(_mouseEvent); + + juce::PopupMenu menu; + + const auto tagType = toTagType(m_type); + + if(m_type == GroupType::DataSources) + { + menu.addItem("Add Folder...", [this] + { + juce::FileChooser fc("Select Folder"); + + if(fc.browseForDirectory()) + { + const auto result = fc.getResult().getFullPathName().toStdString(); + pluginLib::patchDB::DataSource ds; + ds.type = pluginLib::patchDB::SourceType::Folder; + ds.name = result; + ds.origin = pluginLib::patchDB::DataSourceOrigin::Manual; + getPatchManager().addDataSource(ds); + } + }); + + menu.addItem("Add File...", [this] + { + juce::FileChooser fc("Select File"); + if(fc.browseForFileToOpen()) + { + const auto result = fc.getResult().getFullPathName().toStdString(); + pluginLib::patchDB::DataSource ds; + ds.type = pluginLib::patchDB::SourceType::File; + ds.name = result; + ds.origin = pluginLib::patchDB::DataSourceOrigin::Manual; + getPatchManager().addDataSource(ds); + } + }); + } + else if(m_type == GroupType::LocalStorage) + { + menu.addItem("Create...", [this] + { + beginEdit("Enter name...", [this](bool _success, const std::string& _newText) + { + pluginLib::patchDB::DataSource ds; + + ds.name = _newText; + ds.type = pluginLib::patchDB::SourceType::LocalStorage; + ds.origin = pluginLib::patchDB::DataSourceOrigin::Manual; + ds.timestamp = std::chrono::system_clock::now(); + + getPatchManager().addDataSource(ds); + }); + }); + } + if(tagType != pluginLib::patchDB::TagType::Invalid) + { + menu.addItem("Add...", [this] + { + beginEdit("Enter name...", [this](bool _success, const std::string& _newText) + { + if (!_newText.empty()) + getPatchManager().addTag(toTagType(m_type), _newText); + }); + }); + } + + menu.showMenuAsync(juce::PopupMenu::Options()); + } + } + + void GroupTreeItem::setFilter(const std::string& _filter) + { + if (m_filter == _filter) + return; + + m_filter = _filter; + + for (const auto& it : m_itemsByDataSource) + validateParent(it.first, it.second); + + for (const auto& it : m_itemsByTag) + validateParent(it.second); + } + + bool GroupTreeItem::isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& _dragSourceDetails) + { + if (isOpen()) + return false; + + if( (!m_itemsByDataSource.empty() && m_itemsByDataSource.begin()->second->isInterestedInDragSource(_dragSourceDetails)) || + (!m_itemsByTag.empty() && m_itemsByTag.begin()->second->isInterestedInDragSource(_dragSourceDetails))) + setOpen(true); + + return false; + } + + DatasourceTreeItem* GroupTreeItem::getItem(const pluginLib::patchDB::DataSource& _ds) const + { + for (const auto& [_, item] : m_itemsByDataSource) + { + if(*item->getDataSource() == _ds) + return item; + } + return nullptr; + } + + void GroupTreeItem::setParentSearchRequest(const pluginLib::patchDB::SearchRequest& _parentSearch) + { + TreeItem::setParentSearchRequest(_parentSearch); + + for (const auto& it : m_itemsByDataSource) + it.second->setParentSearchRequest(_parentSearch); + + for (const auto& it : m_itemsByTag) + it.second->setParentSearchRequest(_parentSearch); + } + + void GroupTreeItem::onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _parentSearchRequest) + { + TreeItem::onParentSearchChanged(_parentSearchRequest); + + const auto sourceType = toSourceType(m_type); + + if(sourceType != pluginLib::patchDB::SourceType::Invalid) + { + pluginLib::patchDB::SearchRequest req = _parentSearchRequest; + req.sourceType = sourceType; + search(std::move(req)); + } + + const auto tagType = toTagType(m_type); + + if(tagType != pluginLib::patchDB::TagType::Invalid) + { + pluginLib::patchDB::SearchRequest req = _parentSearchRequest; + req.anyTagOfType.insert(tagType); + search(std::move(req)); + } + } + + DatasourceTreeItem* GroupTreeItem::createItemForDataSource(const pluginLib::patchDB::DataSourceNodePtr& _dataSource) + { + const auto it = m_itemsByDataSource.find(_dataSource); + + if (it != m_itemsByDataSource.end()) + return it->second; + + auto* item = new DatasourceTreeItem(getPatchManager(), _dataSource); + + m_itemsByDataSource.insert({ _dataSource, item }); + + const auto oldNumSubItems = getNumSubItems(); + + validateParent(_dataSource, item); + + if(getNumSubItems() == 1 && oldNumSubItems == 0) + setOpen(true); + + return item; + } + + TagTreeItem* GroupTreeItem::createSubItem(const std::string& _tag) + { + auto item = new TagTreeItem(getPatchManager(), m_type, _tag); + + validateParent(item); + + m_itemsByTag.insert({ _tag, item }); + + return item; + } + + bool GroupTreeItem::needsParentItem(const pluginLib::patchDB::DataSourceNodePtr& _ds) const + { + if (!m_filter.empty()) + return false; + return _ds->hasParent() && _ds->origin != pluginLib::patchDB::DataSourceOrigin::Manual; + } + + void GroupTreeItem::validateParent(const pluginLib::patchDB::DataSourceNodePtr& _ds, DatasourceTreeItem* _item) + { + TreeViewItem* parentNeeded = nullptr; + + if (needsParentItem(_ds)) + { + parentNeeded = createItemForDataSource(_ds->getParent()); + } + else if (_ds->type == pluginLib::patchDB::SourceType::Folder && !m_filter.empty()) + { + parentNeeded = nullptr; + } + else if (match(*_item)) + { + parentNeeded = this; + } + + _item->setParent(parentNeeded, true); + } + + void GroupTreeItem::validateParent(TagTreeItem* _item) + { + if (match(*_item)) + _item->setParent(this, true); + else + _item->setParent(nullptr, true); + } + + bool GroupTreeItem::match(const TreeItem& _item) const + { + if (m_filter.empty()) + return true; + const auto t = Search::lowercase(_item.getText()); + return t.find(m_filter) != std::string::npos; + } +} diff --git a/source/jucePluginEditorLib/patchmanager/grouptreeitem.h b/source/jucePluginEditorLib/patchmanager/grouptreeitem.h @@ -0,0 +1,60 @@ +#pragma once + +#include <set> + +#include "treeitem.h" +#include "types.h" + +#include "../../jucePluginLib/patchdb/patchdbtypes.h" + +namespace pluginLib::patchDB +{ + struct DataSource; +} + +namespace jucePluginEditorLib::patchManager +{ + class DatasourceTreeItem; + class TagTreeItem; + + class GroupTreeItem : public TreeItem + { + public: + GroupTreeItem(PatchManager& _pm, GroupType _type, const std::string& _groupName); + + bool mightContainSubItems() override + { + return true; + } + + void updateFromTags(const std::set<std::string>& _tags); + + void removeItem(const DatasourceTreeItem* _item); + void removeDataSource(const pluginLib::patchDB::DataSourceNodePtr& _ds); + void updateFromDataSources(const std::vector<pluginLib::patchDB::DataSourceNodePtr>& _dataSources); + + void processDirty(const std::set<pluginLib::patchDB::SearchHandle>& _dirtySearches) override; + void itemClicked(const juce::MouseEvent&) override; + + void setFilter(const std::string& _filter); + + bool isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& _dragSourceDetails) override; + DatasourceTreeItem* getItem(const pluginLib::patchDB::DataSource& _ds) const; + + void setParentSearchRequest(const pluginLib::patchDB::SearchRequest& _parentSearch) override; + void onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _parentSearchRequest) override; + + private: + DatasourceTreeItem* createItemForDataSource(const pluginLib::patchDB::DataSourceNodePtr& _dataSource); + TagTreeItem* createSubItem(const std::string& _tag); + bool needsParentItem(const pluginLib::patchDB::DataSourceNodePtr& _ds) const; + void validateParent(const pluginLib::patchDB::DataSourceNodePtr& _ds, DatasourceTreeItem* _item); + void validateParent(TagTreeItem* _item); + bool match(const TreeItem& _item) const; + + const GroupType m_type; + std::map<std::string, TagTreeItem*> m_itemsByTag; + std::map<pluginLib::patchDB::DataSourceNodePtr, DatasourceTreeItem*> m_itemsByDataSource; + std::string m_filter; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/info.cpp b/source/jucePluginEditorLib/patchmanager/info.cpp @@ -0,0 +1,193 @@ +#include "info.h" + +#include "patchmanager.h" +#include "defaultskin.h" + +#include "../../jucePluginLib/patchdb/patch.h" +#include "../../juceUiLib/uiObject.h" +#include "../pluginEditor.h" + +namespace jucePluginEditorLib::patchManager +{ + Info::Info(PatchManager& _pm) : m_patchManager(_pm) + { + addAndMakeVisible(m_content); + + m_name = addChild(new juce::Label()); + m_lbSource = addChild(new juce::Label("", "Source")); + m_source = addChild(new juce::Label()); + m_lbCategories = addChild(new juce::Label("", "Categories")); + m_categories = addChild(new juce::Label()); + m_lbTags = addChild(new juce::Label("", "Tags")); + m_tags = addChild(new juce::Label()); + + if(const auto& t = _pm.getTemplate("pm_info_label")) + { + t->apply(_pm.getEditor(), *m_lbSource); + t->apply(_pm.getEditor(), *m_lbCategories); + t->apply(_pm.getEditor(), *m_lbTags); + } + else + { + m_lbSource->setColour(juce::Label::textColourId, juce::Colour(defaultSkin::colors::infoLabel)); + m_lbCategories->setColour(juce::Label::textColourId, juce::Colour(defaultSkin::colors::infoLabel)); + m_lbTags->setColour(juce::Label::textColourId, juce::Colour(defaultSkin::colors::infoLabel)); + + m_lbSource->setJustificationType(juce::Justification::bottomLeft); + m_lbCategories->setJustificationType(juce::Justification::bottomLeft); + m_lbTags->setJustificationType(juce::Justification::bottomLeft); + } + + if (const auto& t = _pm.getTemplate("pm_info_text")) + { + t->apply(_pm.getEditor(), *m_source); + t->apply(_pm.getEditor(), *m_categories); + t->apply(_pm.getEditor(), *m_tags); + } + else + { + m_source->setColour(juce::Label::textColourId, juce::Colour(defaultSkin::colors::infoText)); + m_categories->setColour(juce::Label::textColourId, juce::Colour(defaultSkin::colors::infoText)); + m_tags->setColour(juce::Label::textColourId, juce::Colour(defaultSkin::colors::infoText)); + + m_source->setJustificationType(juce::Justification::topLeft); + m_categories->setJustificationType(juce::Justification::topLeft); + m_tags->setJustificationType(juce::Justification::topLeft); + } + + if (const auto& t = _pm.getTemplate("pm_info_name")) + { + t->apply(_pm.getEditor(), *m_name); + } + else + { + auto f = m_name->getFont(); + f.setHeight(f.getHeight() * 2); + f.setBold(true); + m_name->setFont(f); + m_name->setJustificationType(juce::Justification::topLeft); + m_name->setColour(juce::Label::textColourId, juce::Colour(defaultSkin::colors::infoHeadline)); + m_name->setColour(juce::Label::backgroundColourId, juce::Colour(defaultSkin::colors::background)); + } + } + + Info::~Info() + { + m_content.deleteAllChildren(); + } + + void Info::setPatch(const pluginLib::patchDB::PatchPtr& _patch) const + { + if (!_patch) + { + clear(); + return; + } + + m_name->setText(_patch->getName(), juce::sendNotification); + m_source->setText(toText(_patch->source.lock()), juce::sendNotification); + m_categories->setText(toText(_patch->getTags().get(pluginLib::patchDB::TagType::Category)), juce::sendNotification); + m_tags->setText(toText(_patch->getTags().get(pluginLib::patchDB::TagType::Tag)), juce::sendNotification); + + doLayout(); + } + + void Info::clear() const + { + m_name->setText({}, juce::sendNotification); + m_source->setText({}, juce::sendNotification); + m_categories->setText({}, juce::sendNotification); + m_tags->setText({}, juce::sendNotification); + + doLayout(); + } + + std::string Info::toText(const pluginLib::patchDB::Tags& _tags) + { + const auto& tags = _tags.getAdded(); + std::stringstream ss; + + size_t i = 0; + for (const auto& tag : tags) + { + if (i) + ss << ", "; + ss << tag; + ++i; + } + return ss.str(); + } + + std::string Info::toText(const pluginLib::patchDB::DataSourceNodePtr& _source) + { + if (!_source) + return {}; + + switch (_source->type) + { + case pluginLib::patchDB::SourceType::Invalid: + case pluginLib::patchDB::SourceType::Count: + return {}; + case pluginLib::patchDB::SourceType::Rom: + case pluginLib::patchDB::SourceType::Folder: + return _source->name; + case pluginLib::patchDB::SourceType::File: + { + auto t = _source->name; + const auto pos = t.find_last_of("\\/"); + if (pos != std::string::npos) + return t.substr(pos + 1); + return t; + } + } + return {}; + } + + void Info::paint(juce::Graphics& g) + { + g.fillAll(m_name->findColour(juce::Label::backgroundColourId)); + } + + juce::Label* Info::addChild(juce::Label* _label) + { + m_content.addAndMakeVisible(_label); + return _label; + } + + void Info::doLayout() const + { + juce::FlexBox fb; + fb.flexWrap = juce::FlexBox::Wrap::noWrap; + fb.justifyContent = juce::FlexBox::JustifyContent::flexStart; + fb.alignContent = juce::FlexBox::AlignContent::flexStart; + fb.flexDirection = juce::FlexBox::Direction::column; + + for (const auto& cChild : m_content.getChildren()) + { + juce::FlexItem item(*cChild); + item = item.withWidth(static_cast<float>(getWidth())); + + const auto* label = dynamic_cast<const juce::Label*>(cChild); + if (label) + { + const auto t = label->getText(); + int lineCount = 1; + for (const auto ch : t) + { + if (ch == '\n') + ++lineCount; + } + item = item.withHeight(label->getFont().getHeight() * (static_cast<float>(lineCount) + 1.5f)); + } + fb.items.add(item); + } + + fb.performLayout(m_content.getLocalBounds()); + } + + void Info::resized() + { + m_content.setSize(getWidth(), getHeight()); + doLayout(); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/info.h b/source/jucePluginEditorLib/patchmanager/info.h @@ -0,0 +1,46 @@ +#pragma once + +#include "../../jucePluginLib/patchdb/patchdbtypes.h" + +#include "juce_gui_basics/juce_gui_basics.h" + +namespace pluginLib::patchDB +{ + class Tags; +} + +namespace jucePluginEditorLib::patchManager +{ + class PatchManager; + + class Info final : public juce::Viewport + { + public: + Info(PatchManager& _pm); + ~Info() override; + + void setPatch(const pluginLib::patchDB::PatchPtr& _patch) const; + void clear() const; + + static std::string toText(const pluginLib::patchDB::Tags& _tags); + static std::string toText(const pluginLib::patchDB::DataSourceNodePtr& _source); + + void paint(juce::Graphics& g) override; + private: + juce::Label* addChild(juce::Label* _label); + void doLayout() const; + void resized() override; + + PatchManager& m_patchManager; + + Component m_content; + + juce::Label* m_name = nullptr; + juce::Label* m_lbSource = nullptr; + juce::Label* m_source = nullptr; + juce::Label* m_lbCategories = nullptr; + juce::Label* m_categories = nullptr; + juce::Label* m_lbTags = nullptr; + juce::Label* m_tags = nullptr; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/list.cpp b/source/jucePluginEditorLib/patchmanager/list.cpp @@ -0,0 +1,581 @@ +#include "list.h" + +#include "defaultskin.h" +#include "listitem.h" +#include "patchmanager.h" +#include "search.h" +#include "../pluginEditor.h" +#include "../../juceUiLib/uiObject.h" + +#include "../../juceUiLib/uiObjectStyle.h" + +namespace jucePluginEditorLib::patchManager +{ + List::List(PatchManager& _pm): m_patchManager(_pm) + { + setColour(backgroundColourId, juce::Colour(defaultSkin::colors::background)); + setColour(textColourId, juce::Colour(defaultSkin::colors::itemText)); + + getViewport()->setScrollBarsShown(true, false); + setModel(this); + setMultipleSelectionEnabled(true); + + if (const auto& t = _pm.getTemplate("pm_listbox")) + t->apply(_pm.getEditor(), *this); + + if(const auto t = _pm.getTemplate("pm_scrollbar")) + { + t->apply(_pm.getEditor(), getVerticalScrollBar()); + t->apply(_pm.getEditor(), getHorizontalScrollBar()); + } + else + { + getVerticalScrollBar().setColour(juce::ScrollBar::thumbColourId, juce::Colour(defaultSkin::colors::scrollbar)); + getVerticalScrollBar().setColour(juce::ScrollBar::trackColourId, juce::Colour(defaultSkin::colors::scrollbar)); + getHorizontalScrollBar().setColour(juce::ScrollBar::thumbColourId, juce::Colour(defaultSkin::colors::scrollbar)); + getHorizontalScrollBar().setColour(juce::ScrollBar::trackColourId, juce::Colour(defaultSkin::colors::scrollbar)); + } + + setRowSelectedOnMouseDown(false); + } + + void List::setContent(const pluginLib::patchDB::SearchHandle& _handle) + { + cancelSearch(); + + const auto& search = m_patchManager.getSearch(_handle); + + if (!search) + return; + + setContent(search); + } + + void List::setContent(pluginLib::patchDB::SearchRequest&& _request) + { + cancelSearch(); + const auto sh = getPatchManager().search(std::move(_request)); + setContent(sh); + m_searchHandle = sh; + } + + void List::refreshContent() + { + setContent(m_search); + } + + void List::setContent(const std::shared_ptr<pluginLib::patchDB::Search>& _search) + { + const std::set<Patch> selectedPatches = getSelectedPatches(); + + m_search = _search; + + m_patches.clear(); + { + std::shared_lock lock(_search->resultsMutex); + m_patches.insert(m_patches.end(), _search->results.begin(), _search->results.end()); + } + + sortPatches(); + filterPatches(); + + updateContent(); + + setSelectedPatches(selectedPatches); + + repaint(); + + getPatchManager().setListStatus(static_cast<uint32_t>(selectedPatches.size()), static_cast<uint32_t>(getPatches().size())); + } + + bool List::exportPresets(const bool _selectedOnly, FileType _fileType) const + { + Patches patches; + + if(_selectedOnly) + { + const auto selected = getSelectedPatches(); + if(selected.empty()) + return false; + patches.assign(selected.begin(), selected.end()); + } + else + { + patches = getPatches(); + } + + if(patches.empty()) + return false; + + return getPatchManager().exportPresets(std::move(patches), _fileType); + } + + bool List::onClicked(const juce::MouseEvent& _mouseEvent) + { + if(!_mouseEvent.mods.isPopupMenu()) + return false; + + auto fileTypeMenu = [this](const std::function<void(FileType)>& _func) + { + juce::PopupMenu menu; + menu.addItem(".syx", [this, _func]{_func(FileType::Syx);}); + menu.addItem(".mid", [this, _func]{_func(FileType::Mid);}); + return menu; + }; + + auto selectedPatches = getSelectedPatches(); + + const auto hasSelectedPatches = !selectedPatches.empty(); + + juce::PopupMenu menu; + if(hasSelectedPatches) + menu.addSubMenu("Export selected...", fileTypeMenu([this](const FileType _fileType) { exportPresets(true, _fileType); })); + menu.addSubMenu("Export all...", fileTypeMenu([this](const FileType _fileType) { exportPresets(false, _fileType); })); + + if(hasSelectedPatches) + { + menu.addSeparator(); + + if(selectedPatches.size() == 1) + { + const auto& patch = *selectedPatches.begin(); + const auto row = getSelectedRow(); + const auto pos = getRowPosition(row, true); + + menu.addItem("Rename...", [this, patch, pos] + { + beginEdit(this, pos, patch->getName(), [this, patch](bool _cond, const std::string& _name) + { + if(_name != patch->getName()) + getPatchManager().renamePatch(patch, _name); + }); + }); + + menu.addItem("Locate", [this, patch, pos] + { + m_patchManager.setSelectedDataSource(patch->source.lock()); + }); + } + + if(!m_search->request.tags.empty()) + { + menu.addItem("Remove selected", [this, s = std::move(selectedPatches)] + { + const std::vector patches(s.begin(), s.end()); + pluginLib::patchDB::TypedTags removeTags; + + // converted "added" tags to "removed" tags + for (const auto& tags : m_search->request.tags.get()) + { + const pluginLib::patchDB::TagType type = tags.first; + const auto& t = tags.second; + + for (const auto& tag : t.getAdded()) + removeTags.addRemoved(type, tag); + } + + m_patchManager.modifyTags(patches, removeTags); + m_patchManager.repaint(); + }); + } + else if(getSourceType() == pluginLib::patchDB::SourceType::LocalStorage) + { + menu.addItem("Deleted selected", [this, s = std::move(selectedPatches)] + { + if(showDeleteConfirmationMessageBox()) + { + const std::vector patches(s.begin(), s.end()); + m_patchManager.removePatches(m_search->request.sourceNode, patches); + } + }); + } + } + menu.addSeparator(); + menu.addItem("Hide Duplicates", true, m_hideDuplicates, [this] + { + setFilter(m_filter, !m_hideDuplicates); + }); + menu.showMenuAsync({}); + return true; + } + + void List::cancelSearch() + { + if(m_searchHandle == pluginLib::patchDB::g_invalidSearchHandle) + return; + getPatchManager().cancelSearch(m_searchHandle); + m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle; + } + + int List::getNumRows() + { + return static_cast<int>(getPatches().size()); + } + + void List::paintListBoxItem(const int _rowNumber, juce::Graphics& _g, const int _width, const int _height, const bool _rowIsSelected) + { + const auto* style = dynamic_cast<genericUI::UiObjectStyle*>(&getLookAndFeel()); + + if (_rowNumber >= getNumRows()) + return; // Juce what are you up to? + + const auto& patch = getPatch(_rowNumber); + + const auto text = patch->getName(); + + if(_rowIsSelected) + { + if(style) + _g.setColour(style->getSelectedItemBackgroundColor()); + else + _g.setColour(juce::Colour(0x33ffffff)); + _g.fillRect(0, 0, _width, _height); + } + + if (style) + { + if (const auto f = style->getFont()) + _g.setFont(*f); + } + + const auto c = getPatchManager().getPatchColor(patch); + + constexpr int offsetX = 20; + + if(c != pluginLib::patchDB::g_invalidColor) + { + _g.setColour(juce::Colour(c)); + constexpr auto s = 8.f; + constexpr auto sd2 = 0.5f * s; + _g.fillEllipse(10 - sd2, static_cast<float>(_height) * 0.5f - sd2, s, s); +// _g.setColour(juce::Colour(0xffffffff)); +// _g.drawEllipse(10 - sd2, static_cast<float>(_height) * 0.5f - sd2, s, s, 1.0f); +// offsetX += 14; + } + +// if(c != pluginLib::patchDB::g_invalidColor) +// _g.setColour(juce::Colour(c)); +// else + _g.setColour(findColour(textColourId)); + + _g.drawText(text, offsetX, 0, _width - 4, _height, style ? style->getAlign() : juce::Justification::centredLeft, true); + } + + juce::var List::getDragSourceDescription(const juce::SparseSet<int>& rowsToDescribe) + { + const auto& ranges = rowsToDescribe.getRanges(); + + if (ranges.isEmpty()) + return {}; + + juce::Array<juce::var> indices; + + for (const auto& range : ranges) + { + for (int i = range.getStart(); i < range.getEnd(); ++i) + { + if(i >= 0 && static_cast<size_t>(i) < getPatches().size()) + indices.add(i); + } + } + + return indices; + } + + juce::Component* List::refreshComponentForRow(int rowNumber, bool isRowSelected, Component* existingComponentToUpdate) + { + auto* existing = dynamic_cast<ListItem*>(existingComponentToUpdate); + + if (existing) + { + existing->setRow(rowNumber); + return existing; + } + + delete existingComponentToUpdate; + + return new ListItem(*this, rowNumber); + } + + void List::selectedRowsChanged(const int lastRowSelected) + { + ListBoxModel::selectedRowsChanged(lastRowSelected); + + if(!m_ignoreSelectedRowsChanged) + activateSelectedPatch(); + + const auto patches = getSelectedPatches(); + getPatchManager().setListStatus(static_cast<uint32_t>(patches.size()), static_cast<uint32_t>(getPatches().size())); + } + + std::set<List::Patch> List::getSelectedPatches() const + { + std::set<Patch> result; + + const auto selectedRows = getSelectedRows(); + const auto& ranges = selectedRows.getRanges(); + + for (const auto& range : ranges) + { + for (int i = range.getStart(); i < range.getEnd(); ++i) + { + if (i >= 0 && static_cast<size_t>(i) < getPatches().size()) + result.insert(getPatch(i)); + } + } + return result; + } + + bool List::setSelectedPatches(const std::set<Patch>& _patches) + { + if (_patches.empty()) + return false; + + std::set<pluginLib::patchDB::PatchKey> patches; + + for (const auto& patch : _patches) + { + if(!patch->source.expired()) + patches.insert(pluginLib::patchDB::PatchKey(*patch)); + } + return setSelectedPatches(patches); + } + + bool List::setSelectedPatches(const std::set<pluginLib::patchDB::PatchKey>& _patches) + { + if (_patches.empty()) + { + deselectAllRows(); + return false; + } + + juce::SparseSet<int> selection; + + int maxRow = std::numeric_limits<int>::min(); + int minRow = std::numeric_limits<int>::max(); + + for(int i=0; i<static_cast<int>(getPatches().size()); ++i) + { + const auto key = pluginLib::patchDB::PatchKey(*getPatch(i)); + + if (_patches.find(key) != _patches.end()) + { + selection.addRange({ i, i + 1 }); + + maxRow = std::max(maxRow, i); + minRow = std::min(minRow, i); + } + } + + if(selection.isEmpty()) + { + deselectAllRows(); + return false; + } + + m_ignoreSelectedRowsChanged = true; + setSelectedRows(selection); + m_ignoreSelectedRowsChanged = false; + scrollToEnsureRowIsOnscreen((minRow + maxRow) >> 1); + return true; + } + + void List::activateSelectedPatch() const + { + const auto patches = getSelectedPatches(); + + if(patches.size() == 1) + m_patchManager.setSelectedPatch(*patches.begin(), m_search->handle); + } + + void List::processDirty(const pluginLib::patchDB::Dirty& _dirty) + { + if (!m_search) + return; + + if (_dirty.searches.empty()) + return; + + if(_dirty.searches.find(m_search->handle) != _dirty.searches.end()) + setContent(m_search); + } + + std::vector<pluginLib::patchDB::PatchPtr> List::getPatchesFromDragSource(const juce::DragAndDropTarget::SourceDetails& _dragSourceDetails) + { + const auto* list = dynamic_cast<List*>(_dragSourceDetails.sourceComponent.get()); + if(!list) + return {}; + + const auto* arr = _dragSourceDetails.description.getArray(); + if (!arr) + return {}; + + std::vector<pluginLib::patchDB::PatchPtr> patches; + + for (const auto& var : *arr) + { + if (!var.isInt()) + continue; + const int idx = var; + if (const auto patch = list->getPatch(idx)) + patches.push_back(patch); + } + + return patches; + } + + pluginLib::patchDB::DataSourceNodePtr List::getDataSource() const + { + if(!m_search) + return nullptr; + + return m_search->request.sourceNode; + } + + void List::setFilter(const std::string& _filter) + { + setFilter(_filter, m_hideDuplicates); + } + + void List::setFilter(const std::string& _filter, const bool _hideDuplicates) + { + if (m_filter == _filter && _hideDuplicates == m_hideDuplicates) + return; + + const auto selected = getSelectedPatches(); + + m_filter = _filter; + m_hideDuplicates = _hideDuplicates; + + filterPatches(); + updateContent(); + + setSelectedPatches(selected); + + repaint(); + + getPatchManager().setListStatus(static_cast<uint32_t>(selected.size()), static_cast<uint32_t>(getPatches().size())); + } + + void List::sortPatches(Patches& _patches, pluginLib::patchDB::SourceType _sourceType) + { + std::sort(_patches.begin(), _patches.end(), [_sourceType](const Patch& _a, const Patch& _b) + { + const auto sourceType = _sourceType; + + if(sourceType == pluginLib::patchDB::SourceType::Folder) + { + const auto aSource = _a->source.lock(); + const auto bSource = _b->source.lock(); + if (*aSource != *bSource) + return *aSource < *bSource; + } + else if (sourceType == pluginLib::patchDB::SourceType::File || sourceType == pluginLib::patchDB::SourceType::Rom || sourceType == pluginLib::patchDB::SourceType::LocalStorage) + { + if (_a->program != _b->program) + return _a->program < _b->program; + } + + return _a->getName().compare(_b->getName()) < 0; + }); + } + + void List::listBoxItemClicked(const int _row, const juce::MouseEvent& _mouseEvent) + { + if(!onClicked(_mouseEvent)) + ListBoxModel::listBoxItemClicked(_row, _mouseEvent); + } + + void List::backgroundClicked(const juce::MouseEvent& _mouseEvent) + { + if(!onClicked(_mouseEvent)) + ListBoxModel::backgroundClicked(_mouseEvent); + } + + bool List::showDeleteConfirmationMessageBox() + { + return 1 == juce::NativeMessageBox::showYesNoBox(juce::AlertWindow::WarningIcon, "Confirmation needed", "Delete selected patches from bank?"); + } + + pluginLib::patchDB::SourceType List::getSourceType() const + { + if(!m_search) + return pluginLib::patchDB::SourceType::Invalid; + return m_search->getSourceType(); + } + + bool List::canReorderPatches() const + { + if(!m_search) + return false; + if(getSourceType() != pluginLib::patchDB::SourceType::LocalStorage) + return false; + if(!m_search->request.tags.empty()) + return false; + return true; + } + + bool List::hasTagFilters() const + { + if(!m_search) + return false; + return !m_search->request.tags.empty(); + } + + bool List::hasFilters() const + { + return hasTagFilters() || !m_filter.empty(); + } + + pluginLib::patchDB::SearchHandle List::getSearchHandle() const + { + if(!m_search) + return pluginLib::patchDB::g_invalidSearchHandle; + return m_search->handle; + } + + void List::sortPatches() + { + // Note: If this list is no longer sorted by calling this function, be sure to modify the second caller in state.cpp, too, as it is used to track the selected entry across multiple parts + sortPatches(m_patches); + } + + void List::sortPatches(Patches& _patches) const + { + sortPatches(_patches, getSourceType()); + } + + void List::filterPatches() + { + if (m_filter.empty() && !m_hideDuplicates) + { + m_filteredPatches.clear(); + return; + } + + m_filteredPatches.reserve(m_patches.size()); + m_filteredPatches.clear(); + + std::set<pluginLib::patchDB::PatchHash> knownPatches; + + for (const auto& patch : m_patches) + { + if(m_hideDuplicates) + { + if(knownPatches.find(patch->hash) != knownPatches.end()) + continue; + knownPatches.insert(patch->hash); + } + + if (m_filter.empty() || match(patch)) + m_filteredPatches.emplace_back(patch); + } + } + + bool List::match(const Patch& _patch) const + { + const auto name = _patch->getName(); + const auto t = Search::lowercase(name); + return t.find(m_filter) != std::string::npos; + } +} diff --git a/source/jucePluginEditorLib/patchmanager/list.h b/source/jucePluginEditorLib/patchmanager/list.h @@ -0,0 +1,114 @@ +#pragma once + +#include "editable.h" + +#include "../../jucePluginLib/patchdb/patchdbtypes.h" + +#include "juce_gui_basics/juce_gui_basics.h" + +#include "../types.h" + +namespace pluginLib::patchDB +{ + struct SearchRequest; + struct PatchKey; + struct Search; +} + +namespace jucePluginEditorLib::patchManager +{ + class PatchManager; + + class List : public juce::ListBox, juce::ListBoxModel, Editable + { + public: + using Patch = pluginLib::patchDB::PatchPtr; + using Patches = std::vector<Patch>; + + explicit List(PatchManager& _pm); + + void setContent(const pluginLib::patchDB::SearchHandle& _handle); + void setContent(pluginLib::patchDB::SearchRequest&& _request); + + void refreshContent(); + + // ListBoxModel + int getNumRows() override; + void paintListBoxItem(int _rowNumber, juce::Graphics& _g, int _width, int _height, bool _rowIsSelected) override; + juce::var getDragSourceDescription(const juce::SparseSet<int>& rowsToDescribe) override; + Component* refreshComponentForRow(int rowNumber, bool isRowSelected, Component* existingComponentToUpdate) override; + + void selectedRowsChanged(int lastRowSelected) override; + + const Patches& getPatches() const + { + if (m_filter.empty() && !m_hideDuplicates) + return m_patches; + return m_filteredPatches; + } + + Patch getPatch(const size_t _index) const + { + return getPatch(getPatches(), _index); + } + + std::set<Patch> getSelectedPatches() const; + bool setSelectedPatches(const std::set<Patch>& _patches); + bool setSelectedPatches(const std::set<pluginLib::patchDB::PatchKey>& _patches); + + void activateSelectedPatch() const; + + void processDirty(const pluginLib::patchDB::Dirty& _dirty); + + static std::vector<pluginLib::patchDB::PatchPtr> getPatchesFromDragSource(const juce::DragAndDropTarget::SourceDetails& _dragSourceDetails); + + pluginLib::patchDB::DataSourceNodePtr getDataSource() const; + + static Patch getPatch(const Patches& _patches, const size_t _index) + { + if (_index >= _patches.size()) + return {}; + return _patches[_index]; + } + + void setFilter(const std::string& _filter); + void setFilter(const std::string& _filter, bool _hideDuplicates); + + PatchManager& getPatchManager() const + { + return m_patchManager; + } + + static void sortPatches(Patches& _patches, pluginLib::patchDB::SourceType _sourceType); + void listBoxItemClicked(int _row, const juce::MouseEvent&) override; + void backgroundClicked(const juce::MouseEvent&) override; + + static bool showDeleteConfirmationMessageBox(); + pluginLib::patchDB::SourceType getSourceType() const; + bool canReorderPatches() const; + bool hasTagFilters() const; + bool hasFilters() const; + + pluginLib::patchDB::SearchHandle getSearchHandle() const; + + private: + void sortPatches(); + void sortPatches(Patches& _patches) const; + void filterPatches(); + bool match(const Patch& _patch) const; + void setContent(const std::shared_ptr<pluginLib::patchDB::Search>& _search); + bool exportPresets(bool _selectedOnly, FileType _fileType) const; + bool onClicked(const juce::MouseEvent&); + void cancelSearch(); + + PatchManager& m_patchManager; + + std::shared_ptr<pluginLib::patchDB::Search> m_search; + Patches m_patches; + Patches m_filteredPatches; + std::string m_filter; + bool m_hideDuplicates = false; + pluginLib::patchDB::SearchHandle m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle; + bool m_ignoreSelectedRowsChanged = false; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/listitem.cpp b/source/jucePluginEditorLib/patchmanager/listitem.cpp @@ -0,0 +1,195 @@ +#include "listitem.h" + +#include "list.h" +#include "patchmanager.h" +#include "savepatchdesc.h" + +#include "../../synthLib/buildconfig.h" + +#include "../pluginEditor.h" + +namespace jucePluginEditorLib::patchManager +{ + // Juce is funny: + // Having mouse clicks enabled prevents the list from getting mouse events, i.e. list entry selection is broken. + // However, by disabling mouse clicks, we also disable the ability to D&D onto these entries, even though D&D are not clicks... + // We solve both by overwriting hitTest() and return true as long as a D&D is in progress, false otherwise + + ListItem::ListItem(List& _list, const int _row) : m_list(_list), m_row(_row) + { +// setInterceptsMouseClicks(false, false); + } + + void ListItem::paint(juce::Graphics& g) + { + Component::paint(g); + + g.setColour(juce::Colour(0xff00ff00)); + + if(m_drag == DragType::Above || m_drag == DragType::Over) + g.drawRect(0, 0, getWidth(), 3, 3); + if(m_drag == DragType::Below || m_drag == DragType::Over) + g.drawRect(0, getHeight()-3, getWidth(), 3, 3); + if(m_drag == DragType::Over) + { + g.drawRect(0, 0, 3, getHeight(), 3); + g.drawRect(getWidth() - 3, 0, 3, getHeight(), 3); + } + } + + void ListItem::itemDragEnter(const SourceDetails& dragSourceDetails) + { + DragAndDropTarget::itemDragEnter(dragSourceDetails); + updateDragTypeFromPosition(dragSourceDetails); + } + + void ListItem::itemDragExit(const SourceDetails& dragSourceDetails) + { + DragAndDropTarget::itemDragExit(dragSourceDetails); + m_drag = DragType::Off; + repaint(); + } + + void ListItem::itemDragMove(const SourceDetails& dragSourceDetails) + { + updateDragTypeFromPosition(dragSourceDetails); + } + + bool ListItem::isInterestedInDragSource(const SourceDetails& dragSourceDetails) + { + if(m_list.getSourceType() != pluginLib::patchDB::SourceType::LocalStorage) + return false; + + const auto* list = dynamic_cast<const List*>(dragSourceDetails.sourceComponent.get()); + + if(list && list == &m_list && m_list.canReorderPatches()) + return true; + + const auto* savePatchDesc = SavePatchDesc::fromDragSource(dragSourceDetails); + + if(!savePatchDesc) + return false; + return true; + } + + void ListItem::itemDropped(const SourceDetails& dragSourceDetails) + { + if(m_drag == DragType::Off) + return; + + auto& pm = m_list.getPatchManager(); + + const auto drag = m_drag; + m_drag = DragType::Off; + + repaint(); + + const auto row = drag == DragType::Above ? m_row : m_row + 1; + + if(const auto* list = dynamic_cast<const List*>(dragSourceDetails.sourceComponent.get())) + { + const auto patches = List::getPatchesFromDragSource(dragSourceDetails); + + if(!patches.empty() && pm.movePatchesTo(row, patches)) + m_list.refreshContent(); + } + else + { + const auto& source = m_list.getDataSource(); + if(!source) + return; + + const auto* savePatchDesc = SavePatchDesc::fromDragSource(dragSourceDetails); + if(!savePatchDesc) + return; + + const auto patch = pm.requestPatchForPart(savePatchDesc->getPart()); + if(!patch) + return; + + if(drag == DragType::Over) + { + repaint(); + + const auto existingPatch = m_list.getPatch(m_row); + + if(1 == juce::NativeMessageBox::showYesNoBox(juce::AlertWindow::QuestionIcon, + "Replace Patch", + "Do you want to replace the existing patch '" + existingPatch->name + "' with contents of part " + std::to_string(savePatchDesc->getPart()+1) + "?")) + { + pm.replacePatch(existingPatch, patch); + } + } + else + { +#if SYNTHLIB_DEMO_MODE + pm.getEditor().showDemoRestrictionMessageBox(); +#else + pm.copyPatchesTo(source, {patch}, row); +#endif + } + + repaint(); + } + + } + + void ListItem::mouseDown(const juce::MouseEvent& event) + { + m_list.mouseDown(event); + } + + bool ListItem::hitTest(int x, int y) + { + if (const juce::DragAndDropContainer* container = juce::DragAndDropContainer::findParentDragContainerFor(this)) + { + if (container->isDragAndDropActive()) + return true; + } + + return false; + } + + void ListItem::updateDragTypeFromPosition(const SourceDetails& dragSourceDetails) + { + const auto prev = m_drag; + + const auto* list = dynamic_cast<const List*>(dragSourceDetails.sourceComponent.get()); + + if(list && list == &m_list) + { + // list is being sorted + if (dragSourceDetails.localPosition.y < (getHeight() >> 1)) + m_drag = DragType::Above; + else + m_drag = DragType::Below; + } + else + { + const auto* savePatchDesc = SavePatchDesc::fromDragSource(dragSourceDetails); + + if(savePatchDesc) + { + // a patch wants to be saved + + if(m_list.hasFilters()) + { + // only allow to replace + m_drag = DragType::Over; + } + else + { + if (dragSourceDetails.localPosition.y < (getHeight() / 3)) + m_drag = DragType::Above; + else if (dragSourceDetails.localPosition.y >= (getHeight() * 2 / 3)) + m_drag = DragType::Below; + else + m_drag = DragType::Over; + } + } + } + + if (prev != m_drag) + repaint(); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/listitem.h b/source/jucePluginEditorLib/patchmanager/listitem.h @@ -0,0 +1,44 @@ +#pragma once + +#include "juce_gui_basics/juce_gui_basics.h" + +namespace jucePluginEditorLib::patchManager +{ + class List; + + class ListItem : public juce::Component, public juce::DragAndDropTarget + { + public: + explicit ListItem(List& _list, int _row); + + void setRow(int _row) + { + m_row = _row; + } + + void paint(juce::Graphics& g) override; + + void itemDragEnter(const SourceDetails& dragSourceDetails) override; + void itemDragExit(const SourceDetails& dragSourceDetails) override; + void itemDragMove(const SourceDetails& dragSourceDetails) override; + bool isInterestedInDragSource(const SourceDetails& dragSourceDetails) override; + void itemDropped(const SourceDetails& dragSourceDetails) override; + void mouseDown(const juce::MouseEvent& event) override; + bool hitTest(int x, int y) override; + private: + + void updateDragTypeFromPosition(const SourceDetails& dragSourceDetails); + + enum class DragType + { + Off, + Above, + Below, + Over + }; + + List& m_list; + int m_row; + DragType m_drag = DragType::Off; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/notagtreeitem.cpp b/source/jucePluginEditorLib/patchmanager/notagtreeitem.cpp @@ -0,0 +1,17 @@ +#include "notagtreeitem.h" + +namespace jucePluginEditorLib::patchManager +{ + NoTagTreeItem::NoTagTreeItem(PatchManager& _pm, const pluginLib::patchDB::TagType _type, const std::string& _title) : TreeItem(_pm, _title), m_tagType(_type) + { + onParentSearchChanged({}); + } + + void NoTagTreeItem::onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _parentSearchRequest) + { + TreeItem::onParentSearchChanged(_parentSearchRequest); + auto req = _parentSearchRequest; + req.noTagOfType.insert(m_tagType); + search(std::move(req)); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/notagtreeitem.h b/source/jucePluginEditorLib/patchmanager/notagtreeitem.h @@ -0,0 +1,17 @@ +#pragma once +#include "treeitem.h" + +namespace jucePluginEditorLib::patchManager +{ + class NoTagTreeItem : public TreeItem + { + public: + NoTagTreeItem(PatchManager& _pm, pluginLib::patchDB::TagType _type, const std::string& _title); + + void onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _parentSearchRequest) override; + bool mightContainSubItems() override { return false; } + + private: + pluginLib::patchDB::TagType m_tagType; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/patchmanager.cpp b/source/jucePluginEditorLib/patchmanager/patchmanager.cpp @@ -0,0 +1,686 @@ +#include "patchmanager.h" + +#include "datasourcetree.h" +#include "datasourcetreeitem.h" +#include "info.h" +#include "list.h" +#include "searchlist.h" +#include "searchtree.h" +#include "status.h" +#include "tagstree.h" +#include "tree.h" + +#include "../pluginEditor.h" + +#include "../../jucePluginLib/types.h" +#include "juce_gui_extra/misc/juce_ColourSelector.h" + +namespace jucePluginEditorLib::patchManager +{ + constexpr int g_scale = 2; + constexpr auto g_searchBarHeight = 32; + constexpr int g_padding = 4; + + PatchManager::PatchManager(Editor& _editor, Component* _root, const juce::File& _dir) : DB(_dir), m_editor(_editor), m_state(*this) + { + const auto rootW = _root->getWidth() / g_scale; + const auto rootH = _root->getHeight() / g_scale; + const auto scale = juce::AffineTransform::scale(g_scale); + + setSize(rootW, rootH); + setTransform(scale); + + _root->addAndMakeVisible(this); + + auto weight = [&](int _weight) + { + return rootW * _weight / 100; + }; + + // 1st column + auto w = weight(33); + m_treeDS = new DatasourceTree(*this); + m_treeDS->setSize(w - g_padding, rootH - g_searchBarHeight - g_padding); + + m_searchTreeDS = new SearchTree(*m_treeDS); + m_searchTreeDS->setSize(m_treeDS->getWidth(), g_searchBarHeight); + m_searchTreeDS->setTopLeftPosition(m_treeDS->getX(), m_treeDS->getHeight() + g_padding); + + addAndMakeVisible(m_treeDS); + addAndMakeVisible(m_searchTreeDS); + + // 2nd column + w = weight(20); + m_treeTags = new TagsTree(*this); + m_treeTags->setTopLeftPosition(m_treeDS->getRight() + g_padding, 0); + m_treeTags->setSize(w - g_padding, rootH - g_searchBarHeight - g_padding); + + m_searchTreeTags = new SearchTree(*m_treeTags); + m_searchTreeTags->setTopLeftPosition(m_treeTags->getX(), m_treeTags->getHeight() + g_padding); + m_searchTreeTags->setSize(m_treeTags->getWidth(), g_searchBarHeight); + + addAndMakeVisible(m_treeTags); + addAndMakeVisible(m_searchTreeTags); + + // 3rd column + w = weight(15); + m_list = new List(*this); + m_list->setTopLeftPosition(m_treeTags->getRight() + g_padding, 0); + m_list->setSize(w - g_padding, rootH - g_searchBarHeight - g_padding); + + m_searchList = new SearchList(*m_list); + m_searchList->setTopLeftPosition(m_list->getX(), m_list->getHeight() + g_padding); + m_searchList->setSize(m_list->getWidth(), g_searchBarHeight); + + addAndMakeVisible(m_list); + addAndMakeVisible(m_searchList); + + // 4th column + m_info = new Info(*this); + m_info->setTopLeftPosition(m_list->getRight() + g_padding, 0); + m_info->setSize(getWidth() - m_info->getX(), rootH - g_searchBarHeight - g_padding); + + m_status = new Status(); + m_status->setTopLeftPosition(m_info->getX(), m_info->getHeight() + g_padding); + m_status->setSize(m_info->getWidth(), g_searchBarHeight); + + addAndMakeVisible(m_info); + addAndMakeVisible(m_status); + + if(const auto t = getTemplate("pm_search")) + { + t->apply(getEditor(), *m_searchList); + t->apply(getEditor(), *m_searchTreeDS); + t->apply(getEditor(), *m_searchTreeTags); + } + + if(const auto t = getTemplate("pm_status_label")) + { + t->apply(getEditor(), *m_status); + } + + juce::StretchableLayoutManager lm; + + m_stretchableManager.setItemLayout(0, 100, rootW * 0.5, m_treeDS->getWidth()); m_stretchableManager.setItemLayout(1, 5, 5, 5); + m_stretchableManager.setItemLayout(2, 100, rootW * 0.5, m_treeTags->getWidth()); m_stretchableManager.setItemLayout(3, 5, 5, 5); + m_stretchableManager.setItemLayout(4, 100, rootW * 0.5, m_list->getWidth()); m_stretchableManager.setItemLayout(5, 5, 5, 5); + m_stretchableManager.setItemLayout(6, 100, rootW * 0.5, m_info->getWidth()); + + m_resizerBarA.setSize(5, rootH); + m_resizerBarB.setSize(5, rootH); + m_resizerBarC.setSize(5, rootH); + + addAndMakeVisible(m_resizerBarA); + addAndMakeVisible(m_resizerBarB); + addAndMakeVisible(m_resizerBarC); + + resized(); + + startTimer(200); + } + + PatchManager::~PatchManager() + { + stopTimer(); + + delete m_status; + delete m_info; + delete m_searchList; + delete m_list; + + // trees emit onSelectionChanged, be sure to guard it + m_list = nullptr; + + delete m_searchTreeTags; + delete m_treeTags; + delete m_searchTreeDS; + delete m_treeDS; + } + + void PatchManager::timerCallback() + { + pluginLib::patchDB::Dirty dirty; + uiProcess(dirty); + + m_treeDS->processDirty(dirty); + m_treeTags->processDirty(dirty); + m_list->processDirty(dirty); + + m_status->setScanning(isScanning()); + + if(!dirty.errors.empty()) + { + std::string msg = "Patch Manager encountered errors:\n\n"; + for(size_t i=0; i<dirty.errors.size(); ++i) + { + msg += dirty.errors[i]; + if(i < dirty.errors.size() - 1) + msg += "\n"; + } + + juce::NativeMessageBox::showMessageBox(juce::AlertWindow::WarningIcon, "Patch Manager Error", msg); + } + } + + void PatchManager::setSelectedItem(Tree* _tree, const TreeItem* _item) + { + m_selectedItems[_tree] = std::set{_item}; + + if(_tree == m_treeDS) + m_treeTags->onParentSearchChanged(_item->getSearchRequest()); + + onSelectedItemsChanged(); + } + + void PatchManager::addSelectedItem(Tree* _tree, const TreeItem* _item) + { + const auto oldCount = m_selectedItems[_tree].size(); + m_selectedItems[_tree].insert(_item); + const auto newCount = m_selectedItems[_tree].size(); + if(newCount > oldCount) + onSelectedItemsChanged(); + } + + void PatchManager::removeSelectedItem(Tree* _tree, const TreeItem* _item) + { + const auto it = m_selectedItems.find(_tree); + if(it == m_selectedItems.end()) + return; + if(!it->second.erase(_item)) + return; + onSelectedItemsChanged(); + } + + bool PatchManager::setSelectedPatch(const pluginLib::patchDB::PatchPtr& _patch, const pluginLib::patchDB::SearchHandle _fromSearch) + { + return setSelectedPatch(getCurrentPart(), _patch, _fromSearch); + } + + bool PatchManager::selectPatch(const uint32_t _part, const int _offset) + { + auto [patch, _] = m_state.getNeighbourPreset(_part, _offset); + + if(!patch) + return false; + + if(!setSelectedPatch(_part, patch, m_state.getSearchHandle(_part))) + return false; + + if(_part == getCurrentPart()) + m_list->setSelectedPatches({patch}); + + return true; + } + + bool PatchManager::setSelectedPatch(const uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch, pluginLib::patchDB::SearchHandle _fromSearch) + { + if(!activatePatch(_patch, _part)) + return false; + + m_state.setSelectedPatch(_part, pluginLib::patchDB::PatchKey(*_patch), _fromSearch); + + if(_part == getCurrentPart()) + m_info->setPatch(_patch); + + return true; + } + + bool PatchManager::setSelectedDataSource(const pluginLib::patchDB::DataSourceNodePtr& _ds) const + { + if(auto* item = m_treeDS->getItem(*_ds)) + { + selectTreeItem(item); + return true; + } + return false; + } + + bool PatchManager::setSelectedPatch(const uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch) + { + if(!isValid(_patch)) + return false; + + const auto patchDs = _patch->source.lock(); + + if(!patchDs) + return false; + + if(!setSelectedPatch(_part, pluginLib::patchDB::PatchKey(*_patch))) + return false; + + return true; + } + + bool PatchManager::setSelectedPatch(const uint32_t _part, const pluginLib::patchDB::PatchKey& _patch) + { + // we've got a patch, but we do not know its search handle, i.e. which list it is part of, find the missing information + + if(!_patch.isValid()) + return false; + + const auto searchHandle = getSearchHandle(*_patch.source, _part == getCurrentPart()); + + if(searchHandle == pluginLib::patchDB::g_invalidSearchHandle) + return false; + + m_state.setSelectedPatch(_part, _patch, searchHandle); + + if(getCurrentPart() == _part) + m_list->setSelectedPatches({_patch}); + + return true; + } + + bool PatchManager::selectPrevPreset(const uint32_t _part) + { + return selectPatch(_part, -1); + } + + bool PatchManager::selectNextPreset(const uint32_t _part) + { + return selectPatch(_part, 1); + } + + bool PatchManager::selectPatch(const uint32_t _part, const pluginLib::patchDB::DataSource& _ds, const uint32_t _program) + { + const auto searchHandle = getSearchHandle(_ds, _part == getCurrentPart()); + + if(searchHandle == pluginLib::patchDB::g_invalidSearchHandle) + return false; + + auto s = getSearch(searchHandle); + if(!s) + return false; + + pluginLib::patchDB::PatchPtr p; + + std::shared_lock lockResults(s->resultsMutex); + for (const auto& patch : s->results) + { + if(patch->program == _program) + { + p = patch; + break; + } + } + + if(!p) + return false; + + if(!activatePatch(p, _part)) + return false; + + setSelectedPatch(_part, p, s->handle); + + if(_part == getCurrentPart()) + m_list->setSelectedPatches({p}); + + return true; + } + + void PatchManager::setListStatus(uint32_t _selected, uint32_t _total) + { + m_status->setListStatus(_selected, _total); + } + + pluginLib::patchDB::Color PatchManager::getPatchColor(const pluginLib::patchDB::PatchPtr& _patch) const + { + // we want to prevent that a whole list is colored with one color just because that list is based on a tag, prefer other tags instead + pluginLib::patchDB::TypedTags ignoreTags; + + for (const auto& selectedItem : m_selectedItems) + { + for (const auto& item : selectedItem.second) + { + const auto& s = item->getSearchRequest(); + ignoreTags.add(s.tags); + } + } + return DB::getPatchColor(_patch, ignoreTags); + } + + bool PatchManager::addGroupTreeItemForTag(const pluginLib::patchDB::TagType _type, const std::string& _name) + { + const auto groupType = toGroupType(_type); + if(groupType == GroupType::Invalid) + return false; + if(_name.empty()) + return false; + if(m_treeTags->getItem(groupType)) + return false; + m_treeTags->addGroup(groupType, _name); + return true; + } + + void PatchManager::paint(juce::Graphics& g) + { + g.fillAll(juce::Colour(0,0,0)); + } + + void PatchManager::exportPresets(const juce::File& _file, const std::vector<pluginLib::patchDB::PatchPtr>& _patches, FileType _fileType) const + { +#if SYNTHLIB_DEMO_MODE + getEditor().showDemoRestrictionMessageBox(); +#else + FileType type = _fileType; + const auto name = Editor::createValidFilename(type, _file); + + std::vector<pluginLib::patchDB::Data> patchData; + for (const auto& patch : _patches) + { + const auto patchSysex = prepareSave(patch); + + if(!patchSysex.empty()) + patchData.push_back(patchSysex); + } + + if(!getEditor().savePresets(type, name, patchData)) + juce::NativeMessageBox::showMessageBox(juce::AlertWindow::WarningIcon, "Save failed", "Failed to write data to " + _file.getFullPathName().toStdString()); +#endif + } + + bool PatchManager::exportPresets(std::vector<pluginLib::patchDB::PatchPtr>&& _patches, FileType _fileType) const + { + if(_patches.size() > 128) + { + if(1 != juce::NativeMessageBox::showOkCancelBox(juce::AlertWindow::WarningIcon, + "Patch Manager", + "You are trying to export more than 128 presets into a single file. Note that this dump exceeds the size of one bank and may not be compatible with your hardware")) + return true; + } + + List::sortPatches(_patches, pluginLib::patchDB::SourceType::LocalStorage); + + getEditor().savePreset([this, p = std::move(_patches), _fileType](const juce::File& _file) + { + exportPresets(_file, p, _fileType); + }); + + return true; + } + + void PatchManager::resized() + { + if(!m_treeDS) + return; + + Component* comps[] = {m_treeDS, &m_resizerBarA, m_treeTags, &m_resizerBarB, m_list, &m_resizerBarC, m_info}; + m_stretchableManager.layOutComponents(comps, (int)std::size(comps), 0, 0, getWidth(), getHeight(), false, false); + + auto layoutXAxis = [](Component* _target, const Component* _source) + { + _target->setTopLeftPosition(_source->getX(), _target->getY()); + _target->setSize(_source->getWidth(), _target->getHeight()); + }; + + layoutXAxis(m_searchTreeDS, m_treeDS); + layoutXAxis(m_searchTreeTags, m_treeTags); + layoutXAxis(m_searchList, m_list); + layoutXAxis(m_status, m_info); + } + + juce::Colour PatchManager::getResizerBarColor() const + { + return m_treeDS->findColour(juce::TreeView::ColourIds::selectedItemBackgroundColourId); + } + + bool PatchManager::copyPart(const uint8_t _target, const uint8_t _source) + { + if(_target == _source) + return false; + + const auto source = requestPatchForPart(_source); + if(!source) + return false; + + if(!activatePatch(source, _target)) + return false; + + m_state.copy(_target, _source); + + if(getCurrentPart() == _target) + setSelectedPatch(_target, m_state.getPatch(_target)); + + return true; + } + + std::shared_ptr<genericUI::UiObject> PatchManager::getTemplate(const std::string& _name) const + { + return m_editor.getTemplate(_name); + } + + void PatchManager::onLoadFinished() + { + DB::onLoadFinished(); + + for(uint32_t i=0; i<m_state.getPartCount(); ++i) + { + const auto p = m_state.getPatch(i); + + // If the state has been deserialized, the patch key is valid but the search handle is not. Only restore if that is the case + if(p.isValid() && m_state.getSearchHandle(i) == pluginLib::patchDB::g_invalidSearchHandle) + { + if(!setSelectedPatch(i, p)) + m_state.clear(i); + } + else if(!m_state.isValid(i)) + { + // otherwise, try to restore from the currently loaded patch + updateStateAsync(i, requestPatchForPart(i)); + } + } + } + + void PatchManager::setPerInstanceConfig(const std::vector<uint8_t>& _data) + { + if(_data.empty()) + return; + try + { + pluginLib::PluginStream s(_data); + const auto version = s.read<uint32_t>(); + if(version != 1) + return; + m_state.setConfig(s); + } + catch(std::range_error&) + { + } + } + + void PatchManager::getPerInstanceConfig(std::vector<uint8_t>& _data) + { + pluginLib::PluginStream s; + s.write<uint32_t>(1); // version + m_state.getConfig(s); + s.toVector(_data); + } + + void PatchManager::onProgramChanged(const uint32_t _part) + { + if(isLoading()) + return; + return; + pluginLib::patchDB::Data data; + if(!requestPatchForPart(data, _part)) + return; + const auto patch = initializePatch(std::move(data)); + if(!patch) + return; + updateStateAsync(_part, patch); + } + + void PatchManager::setCurrentPart(uint32_t _part) + { + if(!m_state.isValid(_part)) + return; + + setSelectedPatch(_part, m_state.getPatch(_part)); + } + + void PatchManager::updateStateAsync(const uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch) + { + if(!isValid(_patch)) + return; + + const auto patchDs = _patch->source.lock(); + + if(patchDs) + { + setSelectedPatch(_part, _patch); + return; + } + + // we've got a patch, but we do not know its datasource and search handle, find the data source by executing a search + + findDatasourceForPatch(_patch, [this, _part](const pluginLib::patchDB::Search& _search) + { + const auto handle = _search.handle; + + std::vector<pluginLib::patchDB::PatchPtr> results; + results.assign(_search.results.begin(), _search.results.end()); + + if(results.empty()) + return; + + if(results.size() > 1) + { + // if there are multiple results, sort them, we prefer ROM results over other results + + std::sort(results.begin(), results.end(), [](const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b) + { + const auto dsA = _a->source.lock(); + const auto dsB = _b->source.lock(); + + if(!dsA || !dsB) + return true; + + if(dsA->type < dsB->type) + return true; + if(dsA->type > dsB->type) + return false; + if(dsA->name < dsB->name) + return true; + if(dsA->name > dsB->name) + return false; + if(_a->program < _b->program) + return true; + return false; + }); + } + + const auto currentPatch = results.front(); + + const auto key = pluginLib::patchDB::PatchKey(*currentPatch); + + runOnUiThread([this, _part, key, handle] + { + cancelSearch(handle); + setSelectedPatch(_part, key); + }); + }); + } + + pluginLib::patchDB::SearchHandle PatchManager::getSearchHandle(const pluginLib::patchDB::DataSource& _ds, bool _selectTreeItem) + { + if(auto* item = m_treeDS->getItem(_ds)) + { + const auto searchHandle = item->getSearchHandle(); + + // select the tree item that contains the data source and expand all parents to make it visible + if(_selectTreeItem) + { + selectTreeItem(item); + } + + return searchHandle; + } + + const auto search = getSearch(_ds); + + if(!search) + return pluginLib::patchDB::g_invalidSearchHandle; + + return search->handle; + } + + void PatchManager::onSelectedItemsChanged() + { + // trees emit onSelectionChanged in destructor, be sure to guard it + if(!m_list) + return; + + const auto selectedTags = m_selectedItems[m_treeTags]; + + auto selectItem = [&](const TreeItem* _item) + { + if(_item->getSearchHandle() != pluginLib::patchDB::g_invalidSearchHandle) + { + m_list->setContent(_item->getSearchHandle()); + return true; + } + return false; + }; + + if(!selectedTags.empty()) + { + if(selectedTags.size() == 1) + { + if(selectItem(*selectedTags.begin())) + return; + } + else + { + pluginLib::patchDB::SearchRequest search = (*selectedTags.begin())->getSearchRequest(); + for (const auto& selectedTag : selectedTags) + search.tags.add(selectedTag->getSearchRequest().tags); + m_list->setContent(std::move(search)); + return; + } + } + + const auto selectedDataSources = m_selectedItems[m_treeDS]; + + if(!selectedDataSources.empty()) + { + const auto* item = *selectedDataSources.begin(); + selectItem(item); + } + } + + void PatchManager::changeListenerCallback(juce::ChangeBroadcaster* _source) + { + auto* cs = dynamic_cast<juce::ColourSelector*>(_source); + + if(cs) + { + const auto tagType = static_cast<pluginLib::patchDB::TagType>(static_cast<int>(cs->getProperties()["tagType"])); + const auto tag = cs->getProperties()["tag"].toString().toStdString(); + + if(tagType != pluginLib::patchDB::TagType::Invalid && !tag.empty()) + { + const auto color = cs->getCurrentColour(); + setTagColor(tagType, tag, color.getARGB()); + + repaint(); + } + } + } + + void PatchManager::selectTreeItem(TreeItem* _item) + { + if(!_item) + return; + + _item->setSelected(true, true); + + auto* parent = _item->getParentItem(); + while(parent) + { + parent->setOpen(true); + parent = parent->getParentItem(); + } + + _item->getOwnerView()->scrollToKeepItemVisible(_item); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/patchmanager.h b/source/jucePluginEditorLib/patchmanager/patchmanager.h @@ -0,0 +1,127 @@ +#pragma once + +#include "resizerbar.h" +#include "../../jucePluginLib/patchdb/db.h" + +#include "juce_gui_basics/juce_gui_basics.h" + +#include "state.h" + +namespace jucePluginEditorLib +{ + enum class FileType; + class Editor; +} + +namespace genericUI +{ + class UiObject; +} + +namespace jucePluginEditorLib::patchManager +{ + class Status; + class TreeItem; + class SearchTree; + class SearchList; + class Info; + class List; + class Tree; + + class PatchManager : public juce::Component, public pluginLib::patchDB::DB, juce::Timer, public juce::ChangeListener + { + public: + explicit PatchManager(Editor& _editor, Component* _root, const juce::File& _dir); + ~PatchManager() override; + + void timerCallback() override; + + void setSelectedItem(Tree* _tree, const TreeItem* _item); + void addSelectedItem(Tree* _tree, const TreeItem* _item); + void removeSelectedItem(Tree* _tree, const TreeItem* _item); + + bool setSelectedPatch(const pluginLib::patchDB::PatchPtr& _patch, pluginLib::patchDB::SearchHandle _fromSearch); + + bool selectPrevPreset(uint32_t _part); + bool selectNextPreset(uint32_t _part); + + bool selectPatch(uint32_t _part, const pluginLib::patchDB::DataSource& _ds, uint32_t _program); + + void setListStatus(uint32_t _selected, uint32_t _total); + + pluginLib::patchDB::Color getPatchColor(const pluginLib::patchDB::PatchPtr& _patch) const; + + bool addGroupTreeItemForTag(pluginLib::patchDB::TagType _type, const std::string& _name); + + void paint(juce::Graphics& g) override; + + void exportPresets(const juce::File& _file, const std::vector<pluginLib::patchDB::PatchPtr>& _patches, FileType _fileType) const; + bool exportPresets(std::vector<pluginLib::patchDB::PatchPtr>&& _patches, FileType _fileType) const; + + void resized() override; + + juce::Colour getResizerBarColor() const; + + bool copyPart(uint8_t _target, uint8_t _source); + + bool setSelectedPatch(uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch, pluginLib::patchDB::SearchHandle _fromSearch); + + bool setSelectedDataSource(const pluginLib::patchDB::DataSourceNodePtr& _ds) const; + + private: + bool selectPatch(uint32_t _part, int _offset); + + bool setSelectedPatch(uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch); + bool setSelectedPatch(uint32_t _part, const pluginLib::patchDB::PatchKey& _patch); + + public: + auto& getEditor() const { return m_editor; } + std::shared_ptr<genericUI::UiObject> getTemplate(const std::string& _name) const; + + virtual uint32_t getCurrentPart() const = 0; + virtual bool activatePatch(const pluginLib::patchDB::PatchPtr& _patch) = 0; + virtual bool activatePatch(const pluginLib::patchDB::PatchPtr& _patch, uint32_t _part) = 0; + + void onLoadFinished() override; + + void setPerInstanceConfig(const std::vector<uint8_t>& _data); + void getPerInstanceConfig(std::vector<uint8_t>& _data); + + void onProgramChanged(uint32_t _part); + + void setCurrentPart(uint32_t _part); + + protected: + void updateStateAsync(uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch); + + private: + pluginLib::patchDB::SearchHandle getSearchHandle(const pluginLib::patchDB::DataSource& _ds, bool _selectTreeItem); + void onSelectedItemsChanged(); + + void changeListenerCallback (juce::ChangeBroadcaster* _source) override; + + static void selectTreeItem(TreeItem* _item); + + Editor& m_editor; + + Tree* m_treeDS = nullptr; + Tree* m_treeTags = nullptr; + List* m_list = nullptr; + Info* m_info = nullptr; + + SearchTree* m_searchTreeDS = nullptr; + SearchTree* m_searchTreeTags = nullptr; + SearchList* m_searchList = nullptr; + Status* m_status = nullptr; + + State m_state; + + std::map<Tree*, std::set<const TreeItem*>> m_selectedItems; + + juce::StretchableLayoutManager m_stretchableManager; + + ResizerBar m_resizerBarA{*this, &m_stretchableManager, 1}; + ResizerBar m_resizerBarB{*this, &m_stretchableManager, 3}; + ResizerBar m_resizerBarC{*this, &m_stretchableManager, 5}; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/resizerbar.cpp b/source/jucePluginEditorLib/patchmanager/resizerbar.cpp @@ -0,0 +1,24 @@ +#include "resizerbar.h" + +#include "patchmanager.h" + +namespace jucePluginEditorLib::patchManager +{ + ResizerBar::ResizerBar(PatchManager& _pm, juce::StretchableLayoutManager* _layout, const int _itemIndexInLayout) + : StretchableLayoutResizerBar(_layout, _itemIndexInLayout, true) + , m_patchManager(_pm) + { + } + + void ResizerBar::hasBeenMoved() + { + juce::StretchableLayoutResizerBar::hasBeenMoved(); + } + + void ResizerBar::paint(juce::Graphics& g) + { +// juce::StretchableLayoutResizerBar::paint(g); + if (isMouseOver()|| isMouseButtonDown()) + g.fillAll (m_patchManager.getResizerBarColor()); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/resizerbar.h b/source/jucePluginEditorLib/patchmanager/resizerbar.h @@ -0,0 +1,19 @@ +#pragma once + +#include "juce_gui_basics/juce_gui_basics.h" + +namespace jucePluginEditorLib::patchManager +{ + class PatchManager; + + class ResizerBar : public juce::StretchableLayoutResizerBar + { + public: + ResizerBar(PatchManager& _pm, juce::StretchableLayoutManager* _layout, int _itemIndexInLayout); + void hasBeenMoved() override; + void paint(juce::Graphics& g) override; + + private: + PatchManager& m_patchManager; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/roottreeitem.cpp b/source/jucePluginEditorLib/patchmanager/roottreeitem.cpp @@ -0,0 +1,8 @@ +#include "roottreeitem.h" + +namespace jucePluginEditorLib::patchManager +{ + RootTreeItem::RootTreeItem(PatchManager& _pm): TreeItem(_pm, "root") + { + } +} diff --git a/source/jucePluginEditorLib/patchmanager/roottreeitem.h b/source/jucePluginEditorLib/patchmanager/roottreeitem.h @@ -0,0 +1,21 @@ +#pragma once +#include "treeitem.h" + +namespace jucePluginEditorLib::patchManager +{ + class RootTreeItem : public TreeItem + { + public: + explicit RootTreeItem(PatchManager& _pm); + + bool mightContainSubItems() override + { + return true; + } + + bool isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& _dragSourceDetails) override + { + return false; + } + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/savepatchdesc.cpp b/source/jucePluginEditorLib/patchmanager/savepatchdesc.cpp diff --git a/source/jucePluginEditorLib/patchmanager/savepatchdesc.h b/source/jucePluginEditorLib/patchmanager/savepatchdesc.h @@ -0,0 +1,24 @@ +#pragma once + +#include "juce_core/juce_core.h" + +namespace jucePluginEditorLib::patchManager +{ + class SavePatchDesc : public juce::ReferenceCountedObject + { + public: + SavePatchDesc(int _part) : m_part(_part) + { + } + + auto getPart() const { return m_part; } + + static const SavePatchDesc* fromDragSource(const juce::DragAndDropTarget::SourceDetails& _source) + { + return dynamic_cast<const SavePatchDesc*>(_source.description.getObject()); + } + + private: + int m_part; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/search.cpp b/source/jucePluginEditorLib/patchmanager/search.cpp @@ -0,0 +1,49 @@ +#include "search.h" + +#include "defaultskin.h" +#include "dsp56kEmu/logging.h" + +namespace jucePluginEditorLib::patchManager +{ + Search::Search() + { + setColour(textColourId, juce::Colour(defaultSkin::colors::itemText)); + setColour(backgroundColourId, juce::Colour(defaultSkin::colors::background)); + setColour(outlineColourId, juce::Colour(defaultSkin::colors::textEditOutline)); + + addListener(this); + } + + void Search::textEditorTextChanged(juce::TextEditor& _textEditor) + { + setText(_textEditor.getText().toStdString()); + } + + std::string Search::lowercase(const std::string& _s) + { + auto t = _s; + std::transform(t.begin(), t.end(), t.begin(), tolower); + return t; + } + + void Search::onTextChanged(const std::string& _text) + { + } + + void Search::paint(juce::Graphics& g) + { + TextEditor::paint(g); + } + + void Search::setText(const std::string& _text) + { + const auto t = lowercase(_text); + + if (m_text == t) + return; + + m_text = t; + onTextChanged(t); + repaint(); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/search.h b/source/jucePluginEditorLib/patchmanager/search.h @@ -0,0 +1,24 @@ +#pragma once + +#include "juce_gui_basics/juce_gui_basics.h" + +namespace jucePluginEditorLib::patchManager +{ + class Search : public juce::TextEditor, juce::TextEditor::Listener + { + public: + Search(); + + void textEditorTextChanged(juce::TextEditor&) override; + + static std::string lowercase(const std::string& _s); + + virtual void onTextChanged(const std::string& _text); + + void paint(juce::Graphics& g) override; + private: + void setText(const std::string& _text); + + std::string m_text; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/searchlist.cpp b/source/jucePluginEditorLib/patchmanager/searchlist.cpp @@ -0,0 +1,15 @@ +#include "searchlist.h" + +#include "list.h" + +namespace jucePluginEditorLib::patchManager +{ + SearchList::SearchList(List& _list): m_list(_list) + { + } + + void SearchList::onTextChanged(const std::string& _text) + { + m_list.setFilter(_text); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/searchlist.h b/source/jucePluginEditorLib/patchmanager/searchlist.h @@ -0,0 +1,19 @@ +#pragma once + +#include "search.h" + +namespace jucePluginEditorLib::patchManager +{ + class List; + + class SearchList : public Search + { + public: + explicit SearchList(List& _list); + + void onTextChanged(const std::string& _text) override; + + private: + List& m_list; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/searchtree.cpp b/source/jucePluginEditorLib/patchmanager/searchtree.cpp @@ -0,0 +1,15 @@ +#include "searchtree.h" + +#include "tree.h" + +namespace jucePluginEditorLib::patchManager +{ + SearchTree::SearchTree(Tree& _tree): m_tree(_tree) + { + } + + void SearchTree::onTextChanged(const std::string& _text) + { + m_tree.setFilter(_text); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/searchtree.h b/source/jucePluginEditorLib/patchmanager/searchtree.h @@ -0,0 +1,19 @@ +#pragma once + +#include "search.h" + +namespace jucePluginEditorLib::patchManager +{ + class Tree; + + class SearchTree : public Search + { + public: + explicit SearchTree(Tree& _tree); + + void onTextChanged(const std::string& _text) override; + + private: + Tree& m_tree; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/state.cpp b/source/jucePluginEditorLib/patchmanager/state.cpp @@ -0,0 +1,166 @@ +#include "state.h" + +#include "list.h" +#include "patchmanager.h" + +namespace jucePluginEditorLib::patchManager +{ + void PartState::setSelectedPatch(const pluginLib::patchDB::PatchKey& _patch, uint32_t _searchHandle) + { + m_patch = _patch; + m_searchHandle = _searchHandle; + } + + void PartState::setConfig(pluginLib::PluginStream& _s) + { + const auto patchKey = _s.readString(); + m_patch = pluginLib::patchDB::PatchKey::fromString(patchKey); + } + + void PartState::getConfig(pluginLib::PluginStream& _s) const + { + _s.write(m_patch.toString()); + } + + void PartState::clear() + { + m_patch = {}; + m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle; + } + + void State::setSelectedPatch(const uint32_t _part, const pluginLib::patchDB::PatchKey& _patch, const uint32_t _searchHandle) + { + if(_part >= m_parts.size()) + return; + + m_parts[_part].setSelectedPatch(_patch, _searchHandle); + } + + std::pair<pluginLib::patchDB::PatchPtr, uint32_t> State::getNeighbourPreset(const uint32_t _part, const int _offset) const + { + if(_part >= m_parts.size()) + return {nullptr, pluginLib::patchDB::g_invalidProgram}; + + const auto& part = m_parts[_part]; + + return getNeighbourPreset(part.getPatch(), part.getSearchHandle(), _offset); + } + + std::pair<pluginLib::patchDB::PatchPtr, uint32_t> State::getNeighbourPreset(const pluginLib::patchDB::PatchKey& _patch, const pluginLib::patchDB::SearchHandle _searchHandle, const int _offset) const + { + const auto result = getPatchesAndIndex(_patch, _searchHandle); + + const auto& patches = result.first; + const auto index = result.second; + + if(index == pluginLib::patchDB::g_invalidProgram) + return {nullptr, pluginLib::patchDB::g_invalidProgram}; + + if(patches.size() <= 1) + return {nullptr, pluginLib::patchDB::g_invalidProgram}; + + const auto count = static_cast<int>(patches.size()); + auto i = static_cast<int>(index) + _offset; + + if(i < 0) + i += count; + if(i >= count) + i -= count; + + return {patches[i], i}; + } + + pluginLib::patchDB::PatchKey State::getPatch(const uint32_t _part) const + { + if(_part >= m_parts.size()) + return {}; + return m_parts[_part].getPatch(); + } + + pluginLib::patchDB::SearchHandle State::getSearchHandle(const uint32_t _part) const + { + if(_part >= m_parts.size()) + return pluginLib::patchDB::g_invalidSearchHandle; + return m_parts[_part].getSearchHandle(); + } + + bool State::isValid(const uint32_t _part) const + { + if(_part >= m_parts.size()) + return false; + return m_parts[_part].isValid(); + } + + std::pair<std::vector<pluginLib::patchDB::PatchPtr>, uint32_t> State::getPatchesAndIndex(const pluginLib::patchDB::PatchKey& _patch, const pluginLib::patchDB::SearchHandle _searchHandle) const + { + const auto& search = m_patchManager.getSearch(_searchHandle); + + if(!search) + return {{}, pluginLib::patchDB::g_invalidProgram}; + + std::vector<pluginLib::patchDB::PatchPtr> patches; + + { + std::shared_lock lock(search->resultsMutex); + patches.assign(search->results.begin(), search->results.end()); + } + + List::sortPatches(patches, search->getSourceType()); + + uint32_t index = pluginLib::patchDB::g_invalidProgram; + + for(uint32_t i=0; i<patches.size(); ++i) + { + if(pluginLib::patchDB::PatchKey(*patches[i]) == _patch) + { + index = i; + break; + } + } + + return {patches, index}; + } + + void State::setConfig(pluginLib::PluginStream& _s) + { + const auto version = _s.read<uint32_t>(); + if(version != 1) + return; + + const auto numParts = _s.read<uint32_t>(); + + for(size_t i=0; i<numParts; ++i) + { + if(i < m_parts.size()) + { + m_parts[i].setConfig(_s); + } + else + { + PartState unused; + unused.setConfig(_s); + } + } + } + + void State::getConfig(pluginLib::PluginStream& _s) const + { + _s.write<uint32_t>(1); // version + _s.write<uint32_t>((uint32_t)m_parts.size()); + + for (const auto& part : m_parts) + part.getConfig(_s); + } + + void State::clear(const uint32_t _part) + { + if(_part >= m_parts.size()) + return; + m_parts[_part].clear(); + } + + void State::copy(const uint8_t _target, const uint8_t _source) + { + m_parts[_target] = m_parts[_source]; + } +} diff --git a/source/jucePluginEditorLib/patchmanager/state.h b/source/jucePluginEditorLib/patchmanager/state.h @@ -0,0 +1,67 @@ +#pragma once + +#include <array> + +#include "../../jucePluginLib/patchdb/patch.h" +#include "../../jucePluginLib/types.h" + +namespace jucePluginEditorLib::patchManager +{ + class State; + class PatchManager; + + class PartState + { + public: + void setSelectedPatch(const pluginLib::patchDB::PatchKey& _patch, uint32_t _searchHandle); + + const auto& getPatch() const { return m_patch; } + const auto& getSearchHandle() const { return m_searchHandle; } + + bool isValid() const { return m_patch.isValid() && m_searchHandle != pluginLib::patchDB::g_invalidSearchHandle; } + + void setConfig(pluginLib::PluginStream& _s); + void getConfig(pluginLib::PluginStream& _s) const; + + void clear(); + + private: + pluginLib::patchDB::PatchKey m_patch; + pluginLib::patchDB::SearchHandle m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle; + }; + + class State + { + public: + explicit State(PatchManager& _patchManager) : m_patchManager(_patchManager), m_parts({}) + { + } + + void setSelectedPatch(const uint32_t _part, const pluginLib::patchDB::PatchKey& _patch, uint32_t _searchHandle); + + std::pair<pluginLib::patchDB::PatchPtr, uint32_t> getNeighbourPreset(uint32_t _part, int _offset) const; + std::pair<pluginLib::patchDB::PatchPtr, uint32_t> getNeighbourPreset(const pluginLib::patchDB::PatchKey& _patch, pluginLib::patchDB::SearchHandle _searchHandle, int _offset) const; + + pluginLib::patchDB::PatchKey getPatch(uint32_t _part) const; + pluginLib::patchDB::SearchHandle getSearchHandle(uint32_t _part) const; + + bool isValid(uint32_t _part) const; + + std::pair<std::vector<pluginLib::patchDB::PatchPtr>, uint32_t> getPatchesAndIndex(const pluginLib::patchDB::PatchKey& _patch, pluginLib::patchDB::SearchHandle _searchHandle) const; + + void setConfig(pluginLib::PluginStream& _s); + void getConfig(pluginLib::PluginStream& _s) const; + + uint32_t getPartCount() const + { + return static_cast<uint32_t>(m_parts.size()); + } + + void clear(const uint32_t _part); + void copy(uint8_t _target, uint8_t _source); + + private: + PatchManager& m_patchManager; + std::array<PartState, 16> m_parts; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/status.cpp b/source/jucePluginEditorLib/patchmanager/status.cpp @@ -0,0 +1,52 @@ +#include "status.h" +#include "defaultskin.h" + +namespace jucePluginEditorLib::patchManager +{ + Status::Status() + { + setColour(textColourId, juce::Colour(defaultSkin::colors::statusText)); + setColour(backgroundColourId, juce::Colour(defaultSkin::colors::background)); + } + + void Status::setScanning(bool _scanning) + { + if(m_isScanning == _scanning) + return; + + m_isScanning = _scanning; + updateText(); + } + + void Status::setListStatus(uint32_t _selected, uint32_t _total) + { + if(m_listSelected == _selected && m_listTotal == _total) + return; + + m_listSelected = _selected; + m_listTotal = _total; + updateText(); + } + + void Status::updateText() + { + if(m_isScanning) + { + setText("Scanning..."); + } + else if(m_listSelected > 0 || m_listTotal > 0) + { + std::string t = std::to_string(m_listTotal) + " Patches"; + if(m_listSelected > 0) + t += ", " + std::to_string(m_listSelected) + " selected"; + setText(t); + } + else + setText({}); + } + + void Status::setText(const std::string& _text) + { + juce::Label::setText(_text, juce::dontSendNotification); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/status.h b/source/jucePluginEditorLib/patchmanager/status.h @@ -0,0 +1,23 @@ +#pragma once + +#include "juce_gui_basics/juce_gui_basics.h" + +namespace jucePluginEditorLib::patchManager +{ + class Status : public juce::Label + { + public: + Status(); + + void setScanning(bool _scanning); + void setListStatus(uint32_t _selected, uint32_t _total); + + private: + void updateText(); + void setText(const std::string& _text); + + bool m_isScanning = false; + uint32_t m_listSelected = 0; + uint32_t m_listTotal = 0; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/tagstree.cpp b/source/jucePluginEditorLib/patchmanager/tagstree.cpp @@ -0,0 +1,30 @@ +#include "tagstree.h" + +#include "notagtreeitem.h" + +namespace jucePluginEditorLib::patchManager +{ + TagsTree::TagsTree(PatchManager& _pm) : Tree(_pm) + { + addGroup(GroupType::Categories); + m_uncategorized = new NoTagTreeItem(_pm, pluginLib::patchDB::TagType::Category, "Uncategorized"); + getRootItem()->addSubItem(m_uncategorized); + addGroup(GroupType::Tags); + + setMultiSelectEnabled(true); + } + + void TagsTree::onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _searchRequest) + { + Tree::onParentSearchChanged(_searchRequest); + + m_uncategorized->onParentSearchChanged(_searchRequest); + } + + void TagsTree::processDirty(const pluginLib::patchDB::Dirty& _dirty) + { + Tree::processDirty(_dirty); + + m_uncategorized->processDirty(_dirty.searches); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/tagstree.h b/source/jucePluginEditorLib/patchmanager/tagstree.h @@ -0,0 +1,20 @@ +#pragma once + +#include "tree.h" + +namespace jucePluginEditorLib::patchManager +{ + class NoTagTreeItem; + + class TagsTree : public Tree + { + public: + explicit TagsTree(PatchManager& _pm); + + void onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _searchRequest) override; + void processDirty(const pluginLib::patchDB::Dirty& _dirty) override; + + private: + NoTagTreeItem* m_uncategorized = nullptr; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/tagtreeitem.cpp b/source/jucePluginEditorLib/patchmanager/tagtreeitem.cpp @@ -0,0 +1,118 @@ +#include "tagtreeitem.h" + +#include "patchmanager.h" +#include "tree.h" +#include "juce_gui_extra/misc/juce_ColourSelector.h" + +namespace jucePluginEditorLib::patchManager +{ + TagTreeItem::TagTreeItem(PatchManager& _pm, const GroupType _type, const std::string& _tag) : TreeItem(_pm, _tag), m_group(_type), m_tag(_tag) + { + const auto tagType = toTagType(getGroupType()); + + if(tagType == pluginLib::patchDB::TagType::Favourites) + { + pluginLib::patchDB::SearchRequest sr; + sr.tags.add(tagType, getTag()); + + search(std::move(sr)); + } + } + + bool TagTreeItem::isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) + { + return TreeItem::isInterestedInDragSource(dragSourceDetails) && hasSearch(); + } + + bool TagTreeItem::isInterestedInPatchList(const List* _list, const juce::Array<juce::var>& _indices) + { + return hasSearch() && toTagType(getGroupType()) != pluginLib::patchDB::TagType::Invalid; + } + + void TagTreeItem::patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) + { + const auto tagType = toTagType(getGroupType()); + + if (tagType == pluginLib::patchDB::TagType::Invalid) + return; + + pluginLib::patchDB::TypedTags tags; + if (juce::ModifierKeys::currentModifiers.isShiftDown()) + tags.addRemoved(tagType, getTag()); + else + tags.add(tagType, getTag()); + + getPatchManager().modifyTags(_patches, tags); + getPatchManager().repaint(); + } + + void TagTreeItem::onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _parentSearchRequest) + { + const auto tagType = toTagType(getGroupType()); + + if(tagType == pluginLib::patchDB::TagType::Invalid) + return; + + pluginLib::patchDB::SearchRequest sr = _parentSearchRequest; + sr.tags.add(tagType, getTag()); + + search(std::move(sr)); + } + + void TagTreeItem::itemClicked(const juce::MouseEvent& _mouseEvent) + { + if(_mouseEvent.mods.isPopupMenu()) + { + const auto tagType = toTagType(getGroupType()); + + if(tagType != pluginLib::patchDB::TagType::Invalid) + { + juce::PopupMenu menu; + const auto& s = getPatchManager().getSearch(getSearchHandle()); + if(s && !s->getResultSize()) + { + menu.addItem("Remove", [this, tagType] + { + getPatchManager().removeTag(tagType, m_tag); + }); + } + menu.addItem("Set Color...", [this, tagType] + { + juce::ColourSelector* cs = new juce::ColourSelector(juce::ColourSelector::showColourAtTop | juce::ColourSelector::showSliders | juce::ColourSelector::showColourspace); + + cs->getProperties().set("tagType", static_cast<int>(tagType)); + cs->getProperties().set("tag", juce::String(getTag())); + + cs->setSize(400,300); + cs->setCurrentColour(juce::Colour(getColor())); + cs->addChangeListener(&getPatchManager()); + + const auto treeRect = getTree()->getScreenBounds(); + const auto itemRect = getItemPosition(true); + auto rect = itemRect; + rect.translate(treeRect.getX(), treeRect.getY()); + + juce::CallOutBox::launchAsynchronously(std::unique_ptr<juce::Component>(cs), rect, nullptr); + }); + if(getColor() != pluginLib::patchDB::g_invalidColor) + { + menu.addItem("Clear Color", [this, tagType] + { + getPatchManager().setTagColor(tagType, getTag(), pluginLib::patchDB::g_invalidColor); + getPatchManager().repaint(); + }); + } + + menu.showMenuAsync({}); + } + } + } + + pluginLib::patchDB::Color TagTreeItem::getColor() const + { + const auto tagType = toTagType(getGroupType()); + if(tagType != pluginLib::patchDB::TagType::Invalid) + return getPatchManager().getTagColor(tagType, getTag()); + return TreeItem::getColor(); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/tagtreeitem.h b/source/jucePluginEditorLib/patchmanager/tagtreeitem.h @@ -0,0 +1,34 @@ +#pragma once + +#include "treeitem.h" +#include "types.h" + +namespace jucePluginEditorLib::patchManager +{ + class TagTreeItem : public TreeItem + { + public: + TagTreeItem(PatchManager& _pm, GroupType _type, const std::string& _tag); + + bool mightContainSubItems() override + { + return false; + } + + auto getGroupType() const { return m_group; } + + bool isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) override; + bool isInterestedInPatchList(const List* _list, const juce::Array<juce::var>& _indices) override; + void patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) override; + void onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _parentSearchRequest) override; + + const auto& getTag() const { return m_tag; } + + void itemClicked(const juce::MouseEvent&) override; + + pluginLib::patchDB::Color getColor() const override; + private: + const GroupType m_group; + const std::string m_tag; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/tree.cpp b/source/jucePluginEditorLib/patchmanager/tree.cpp @@ -0,0 +1,213 @@ +#include "tree.h" + +#include <set> + +#include "grouptreeitem.h" +#include "patchmanager.h" +#include "roottreeitem.h" +#include "treeitem.h" +#include "defaultskin.h" + +#include "../../juceUiLib/uiObject.h" +#include "../pluginEditor.h" + +namespace jucePluginEditorLib::patchManager +{ + constexpr const char* const g_groupNames[] = + { + "Invalid", + "Data Sources", + "User", + "Factory", + "Categories", + "Tags", + "Favourites", + "CustomA", + "CustomB", + "CustomC" + }; + + static_assert(std::size(g_groupNames) == static_cast<uint32_t>(GroupType::Count)); + + Tree::Tree(PatchManager& _patchManager) : m_patchManager(_patchManager) + { + // some very basic defaults if no style is available + setColour(backgroundColourId, juce::Colour(defaultSkin::colors::background)); +// setColour(backgroundColourId, juce::Colour(0)); + setColour(linesColourId, juce::Colour(0xffffffff)); + setColour(dragAndDropIndicatorColourId, juce::Colour(0xff00ff00)); + setColour(selectedItemBackgroundColourId, juce::Colour(defaultSkin::colors::selectedItem)); +// setColour(oddItemsColourId, juce::Colour(0xff333333)); +// setColour(evenItemsColourId, juce::Colour(0xff555555)); + + if(const auto t = _patchManager.getTemplate("pm_treeview")) + { + t->apply(_patchManager.getEditor(), *this); + } + + auto *rootItem = new RootTreeItem(m_patchManager); + setRootItem(rootItem); + setRootItemVisible(false); + + getViewport()->setScrollBarsShown(true, true); + + if(const auto t = _patchManager.getTemplate("pm_scrollbar")) + { + t->apply(_patchManager.getEditor(), getViewport()->getVerticalScrollBar()); + t->apply(_patchManager.getEditor(), getViewport()->getHorizontalScrollBar()); + } + else + { + getViewport()->getVerticalScrollBar().setColour(juce::ScrollBar::thumbColourId, juce::Colour(defaultSkin::colors::scrollbar)); + getViewport()->getVerticalScrollBar().setColour(juce::ScrollBar::trackColourId, juce::Colour(defaultSkin::colors::scrollbar)); + getViewport()->getHorizontalScrollBar().setColour(juce::ScrollBar::thumbColourId, juce::Colour(defaultSkin::colors::scrollbar)); + getViewport()->getHorizontalScrollBar().setColour(juce::ScrollBar::trackColourId, juce::Colour(defaultSkin::colors::scrollbar)); + } + } + + Tree::~Tree() + { + deleteRootItem(); + } + + void Tree::updateDataSources() + { + auto* itemDs = getItem(GroupType::DataSources); + auto* itemLocalStorage = getItem(GroupType::LocalStorage); + auto* itemFactory = getItem(GroupType::Factory); + + if (!itemDs || !itemLocalStorage || !itemFactory) + return; + + std::vector<pluginLib::patchDB::DataSourceNodePtr> allDataSources; + + std::vector<pluginLib::patchDB::DataSourceNodePtr> readOnlyDataSources; + std::vector<pluginLib::patchDB::DataSourceNodePtr> storageDataSources; + std::vector<pluginLib::patchDB::DataSourceNodePtr> factoryDataSources; + + m_patchManager.getDataSources(allDataSources); + + readOnlyDataSources.reserve(allDataSources.size()); + storageDataSources.reserve(allDataSources.size()); + factoryDataSources.reserve(allDataSources.size()); + + for (const auto& ds : allDataSources) + { + if (ds->type == pluginLib::patchDB::SourceType::LocalStorage) + storageDataSources.push_back(ds); + else if (ds->type == pluginLib::patchDB::SourceType::Rom) + factoryDataSources.push_back(ds); + else + readOnlyDataSources.push_back(ds); + } + + itemDs->updateFromDataSources(readOnlyDataSources); + itemLocalStorage->updateFromDataSources(storageDataSources); + itemFactory->updateFromDataSources(factoryDataSources); + } + + void Tree::updateTags(const GroupType _type) + { + const auto tagType = toTagType(_type); + if (tagType == pluginLib::patchDB::TagType::Invalid) + return; + auto* item = getItem(_type); + if (!item) + return; + std::set<pluginLib::patchDB::Tag> tags; + m_patchManager.getTags(tagType, tags); + item->updateFromTags(tags); + } + + void Tree::updateTags(const pluginLib::patchDB::TagType _type) + { + const auto groupType = toGroupType(_type); + if (groupType == GroupType::Invalid) + return; + updateTags(groupType); + } + + void Tree::processDirty(const pluginLib::patchDB::Dirty& _dirty) + { + if (_dirty.dataSources) + updateDataSources(); + + for (const auto& tagType : _dirty.tags) + updateTags(tagType); + + if (!_dirty.searches.empty()) + { + for (const auto& it : m_groupItems) + it.second->processDirty(_dirty.searches); + } + } + + void Tree::paint(juce::Graphics& g) + { + if (findColour(backgroundColourId).getAlpha() > 0) + TreeView::paint(g); + } + + bool Tree::keyPressed(const juce::KeyPress& _key) + { + if(_key.getKeyCode() == juce::KeyPress::F2Key) + { + if(getNumSelectedItems() == 1) + { + juce::TreeViewItem* item = getSelectedItem(0); + auto* myItem = dynamic_cast<TreeItem*>(item); + + if(myItem) + return myItem->beginEdit(); + } + } + return TreeView::keyPressed(_key); + } + + void Tree::setFilter(const std::string& _filter) + { + if (m_filter == _filter) + return; + + m_filter = _filter; + + for (const auto& it : m_groupItems) + it.second->setFilter(_filter); + } + + DatasourceTreeItem* Tree::getItem(const pluginLib::patchDB::DataSource& _ds) + { + const auto it = m_groupItems.find(toGroupType(_ds.type)); + if(it == m_groupItems.end()) + return nullptr; + const auto* item = it->second; + return item->getItem(_ds); + } + + void Tree::onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _searchRequest) + { + for (const auto& groupItem : m_groupItems) + { + groupItem.second->setParentSearchRequest(_searchRequest); + } + } + + void Tree::addGroup(GroupType _type, const std::string& _name) + { + auto* groupItem = new GroupTreeItem(m_patchManager, _type, _name); + getRootItem()->addSubItem(groupItem); + m_groupItems.insert({ _type, groupItem }); + groupItem->setFilter(m_filter); + } + + void Tree::addGroup(const GroupType _type) + { + addGroup(_type, g_groupNames[static_cast<uint32_t>(_type)]); + } + + GroupTreeItem* Tree::getItem(const GroupType _type) + { + const auto it = m_groupItems.find(_type); + return it == m_groupItems.end() ? nullptr : it->second; + } +} diff --git a/source/jucePluginEditorLib/patchmanager/tree.h b/source/jucePluginEditorLib/patchmanager/tree.h @@ -0,0 +1,53 @@ +#pragma once + +#include "juce_gui_basics/juce_gui_basics.h" + +#include "types.h" + +namespace pluginLib::patchDB +{ + struct SearchRequest; + struct DataSource; + struct Dirty; +} + +namespace jucePluginEditorLib::patchManager +{ + class DatasourceTreeItem; + class PatchManager; + class GroupTreeItem; + + class Tree : public juce::TreeView + { + public: + Tree(PatchManager& _patchManager); + ~Tree() override; + + void updateDataSources(); + void updateTags(GroupType _type); + void updateTags(pluginLib::patchDB::TagType _type); + + virtual void processDirty(const pluginLib::patchDB::Dirty& _dirty); + + void paint(juce::Graphics& g) override; + + bool keyPressed(const juce::KeyPress& _key) override; + + void setFilter(const std::string& _filter); + + DatasourceTreeItem* getItem(const pluginLib::patchDB::DataSource& _ds); + + virtual void onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _searchRequest); + + void addGroup(GroupType _type, const std::string& _name); + + GroupTreeItem* getItem(GroupType _type); + protected: + void addGroup(GroupType _type); + + private: + PatchManager& m_patchManager; + std::map<GroupType, GroupTreeItem*> m_groupItems; + std::string m_filter; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/treeitem.cpp b/source/jucePluginEditorLib/patchmanager/treeitem.cpp @@ -0,0 +1,270 @@ +#include "treeitem.h" + +#include "list.h" +#include "patchmanager.h" +#include "savepatchdesc.h" +#include "tree.h" + +#include "../../jucePluginLib/patchdb/patchdbtypes.h" +#include "../../juceUiLib/treeViewStyle.h" + +namespace jucePluginEditorLib::patchManager +{ + TreeItem::TreeItem(PatchManager& _patchManager, const std::string& _title, const uint32_t _count/* = g_invalidCount*/) : m_patchManager(_patchManager), m_count(_count) + { + setTitle(_title); + } + + TreeItem::~TreeItem() + { + getPatchManager().removeSelectedItem(getTree(), this); + + if(m_searchHandle != pluginLib::patchDB::g_invalidSearchHandle) + { + getPatchManager().cancelSearch(m_searchHandle); + m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle; + } + } + + void TreeItem::setTitle(const std::string& _title) + { + if (m_title == _title) + return; + m_title = _title; + updateText(); + } + + void TreeItem::setCount(const uint32_t _count) + { + if (m_count == _count) + return; + m_count = _count; + updateText(); + } + + void TreeItem::processDirty(const std::set<pluginLib::patchDB::SearchHandle>& _dirtySearches) + { + if (_dirtySearches.find(m_searchHandle) == _dirtySearches.end()) + return; + + const auto search = getPatchManager().getSearch(m_searchHandle); + if (!search) + return; + + processSearchUpdated(*search); + } + + bool TreeItem::beginEdit(const std::string& _initialText, FinishedEditingCallback&& _callback) + { + auto pos = getItemPosition(true); + pos.setHeight(getItemHeight()); + + return Editable::beginEdit(getOwnerView(), pos, _initialText, std::move(_callback)); + } + + bool TreeItem::hasSearch() const + { + return m_searchHandle != pluginLib::patchDB::g_invalidSearchHandle; + } + + Tree* TreeItem::getTree() const + { + return dynamic_cast<Tree*>(getOwnerView()); + } + + void TreeItem::removeFromParent(const bool _destroy) const + { + auto* parent = getParentItem(); + if (!parent) + { + if (_destroy) + delete this; + return; + } + const auto idx = getIndexInParent(); + parent->removeSubItem(idx, _destroy); + } + + void TreeItem::setParent(TreeViewItem* _parent, const bool _sorted/* = false*/) + { + const auto* parentExisting = getParentItem(); + + if (_parent == parentExisting) + return; + + removeFromParent(false); + + if (_parent) + { + if(_sorted) + _parent->addSubItemSorted(*this, this); + else + _parent->addSubItem(this); + } + } + + void TreeItem::itemSelectionChanged(const bool _isNowSelected) + { + TreeViewItem::itemSelectionChanged(_isNowSelected); + + if(getTree()->isMultiSelectEnabled()) + { + if (_isNowSelected) + getPatchManager().addSelectedItem(getTree(), this); + else + getPatchManager().removeSelectedItem(getTree(), this); + } + else + { + if (_isNowSelected) + getPatchManager().setSelectedItem(getTree(), this); + } + } + + void TreeItem::itemDropped(const juce::DragAndDropTarget::SourceDetails& dragSourceDetails, int insertIndex) + { + if (dynamic_cast<List*>(dragSourceDetails.sourceComponent.get())) + { + const auto patches = List::getPatchesFromDragSource(dragSourceDetails); + + if(!patches.empty()) + patchesDropped(patches); + } + else + { + const auto* desc = SavePatchDesc::fromDragSource(dragSourceDetails); + + if(!desc) + return; + + if(auto patch = getPatchManager().requestPatchForPart(desc->getPart())) + patchesDropped({patch}); + } + } + + bool TreeItem::isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& _dragSourceDetails) + { + const auto* list = dynamic_cast<List*>(_dragSourceDetails.sourceComponent.get()); + + if (list) + { + const auto* arr = _dragSourceDetails.description.getArray(); + if (!arr) + return false; + + for (const auto& var : *arr) + { + if (!var.isInt()) + return false; + } + + return isInterestedInPatchList(list, *arr); + } + + if(const auto* desc = SavePatchDesc::fromDragSource(_dragSourceDetails)) + return isInterestedInSavePatchDesc(*desc); + + return false; + } + + void TreeItem::search(pluginLib::patchDB::SearchRequest&& _request) + { + cancelSearch(); + setCount(g_unknownCount); + m_searchRequest = _request; + m_searchHandle = getPatchManager().search(std::move(_request)); + } + + void TreeItem::processSearchUpdated(const pluginLib::patchDB::Search& _search) + { + setCount(static_cast<uint32_t>(_search.getResultSize())); + } + + void TreeItem::setText(const std::string& _text) + { + if (m_text == _text) + return; + m_text = _text; + repaintItem(); + } + + void TreeItem::updateText() + { + if (m_count == g_invalidCount) + setText(m_title); + else if (m_count == g_unknownCount) + setText(m_title + " (?)"); + else + setText(m_title + " (" + std::to_string(m_count) + ')'); + } + + void TreeItem::paintItem(juce::Graphics& _g, const int _width, const int _height) + { + getTree()->setColour(juce::TreeView::dragAndDropIndicatorColourId, juce::Colour(juce::ModifierKeys::currentModifiers.isShiftDown() ? 0xffff0000 : 0xff00ff00)); + + const auto* style = dynamic_cast<const genericUI::TreeViewStyle*>(&getOwnerView()->getLookAndFeel()); + + const auto color = getColor(); + + _g.setColour(color != pluginLib::patchDB::g_invalidColor ? juce::Colour(color) : style ? style->getColor() : juce::Colour(0xffffffff)); + + bool haveFont = false; + if(style) + { + if (auto f = style->getFont()) + { + f->setBold(getParentItem() == getTree()->getRootItem()); + _g.setFont(*f); + haveFont = true; + } + } + if(!haveFont) + { + auto fnt = _g.getCurrentFont(); + fnt.setBold(getParentItem() == getTree()->getRootItem()); + _g.setFont(fnt); + } + + const juce::String t(m_text); + _g.drawText(t, 0, 0, _width, _height, style ? style->getAlign() : juce::Justification(juce::Justification::centredLeft)); + TreeViewItem::paintItem(_g, _width, _height); + } + + int TreeItem::compareElements(const TreeViewItem* _a, const TreeViewItem* _b) + { + const auto* a = dynamic_cast<const TreeItem*>(_a); + const auto* b = dynamic_cast<const TreeItem*>(_b); + + if(a && b) + return a->getText().compare(b->getText()); + + if (_a < _b) + return -1; + if (_a > _b) + return 1; + return 0; + } + + void TreeItem::setParentSearchRequest(const pluginLib::patchDB::SearchRequest& _parentSearch) + { + if(_parentSearch == m_parentSearchRequest) + return; + m_parentSearchRequest = _parentSearch; + onParentSearchChanged(m_parentSearchRequest); + } + + void TreeItem::cancelSearch() + { + if(m_searchHandle == pluginLib::patchDB::g_invalidSearchHandle) + return; + + getPatchManager().cancelSearch(m_searchHandle); + m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle; + } + + void TreeItem::patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) + { + for (const auto& patch : _patches) + patchDropped(patch); + } +} diff --git a/source/jucePluginEditorLib/patchmanager/treeitem.h b/source/jucePluginEditorLib/patchmanager/treeitem.h @@ -0,0 +1,96 @@ +#pragma once + +#include "editable.h" +#include "list.h" +#include "savepatchdesc.h" +#include "juce_gui_basics/juce_gui_basics.h" + +#include "../../jucePluginLib/patchdb/patchdbtypes.h" +#include "../../jucePluginLib/patchdb/search.h" + +namespace pluginLib::patchDB +{ + struct Search; + struct SearchRequest; +} + +namespace jucePluginEditorLib::patchManager +{ + class Tree; + static constexpr uint32_t g_invalidCount = ~0; + static constexpr uint32_t g_unknownCount = g_invalidCount - 1; + + class PatchManager; + + class TreeItem : public juce::TreeViewItem, protected Editable + { + public: + TreeItem(PatchManager& _patchManager, const std::string& _title, uint32_t _count = g_invalidCount); + ~TreeItem() override; + + PatchManager& getPatchManager() const { return m_patchManager; } + + void setTitle(const std::string& _title); + virtual void setCount(uint32_t _count); + + auto getSearchHandle() const { return m_searchHandle; } + const auto& getSearchRequest() const { return m_searchRequest; } + + virtual void processDirty(const std::set<pluginLib::patchDB::SearchHandle>& _dirtySearches); + + virtual bool beginEdit() { return false; } + bool beginEdit(const std::string& _initialText, FinishedEditingCallback&& _callback); + + virtual void patchDropped(const pluginLib::patchDB::PatchPtr& _patch) {} + virtual void patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches); + + bool hasSearch() const; + + Tree* getTree() const; + + void removeFromParent(bool _destroy) const; + void setParent(TreeViewItem* _parent, bool _sorted = false); + + const std::string& getText() const { return m_text; } + + // TreeViewItem + void itemSelectionChanged(bool _isNowSelected) override; + void itemDropped(const juce::DragAndDropTarget::SourceDetails& dragSourceDetails, int insertIndex) override; + + bool isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& _dragSourceDetails) override; + + virtual bool isInterestedInPatchList(const List* _list, const juce::Array<juce::var>& _indices) { return false; } + virtual bool isInterestedInSavePatchDesc(const SavePatchDesc& _desc) { return false; } + + virtual int compareElements(const TreeViewItem* _a, const TreeViewItem* _b); + + virtual void setParentSearchRequest(const pluginLib::patchDB::SearchRequest& _parentSearch); + + virtual pluginLib::patchDB::Color getColor() const { return pluginLib::patchDB::g_invalidColor; } + + protected: + void cancelSearch(); + void search(pluginLib::patchDB::SearchRequest&& _request); + virtual void processSearchUpdated(const pluginLib::patchDB::Search& _search); + virtual void onParentSearchChanged(const pluginLib::patchDB::SearchRequest& _parentSearchRequest) {} + + private: + bool mightContainSubItems() override { return true; } + + void setText(const std::string& _text); + void updateText(); + void paintItem(juce::Graphics& _g, int _width, int _height) override; + + PatchManager& m_patchManager; + + std::string m_title; + uint32_t m_count = g_invalidCount; + + std::string m_text; + + pluginLib::patchDB::SearchRequest m_parentSearchRequest; + + pluginLib::patchDB::SearchRequest m_searchRequest; + uint32_t m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle; + }; +} diff --git a/source/jucePluginEditorLib/patchmanager/types.cpp b/source/jucePluginEditorLib/patchmanager/types.cpp @@ -0,0 +1,62 @@ +#include "types.h" + +#include "../../jucePluginLib/patchdb/patchdbtypes.h" + +namespace jucePluginEditorLib::patchManager +{ + pluginLib::patchDB::TagType toTagType(const GroupType _groupType) + { + switch (_groupType) + { + case GroupType::DataSources: + case GroupType::LocalStorage: + case GroupType::Factory: return pluginLib::patchDB::TagType::Invalid; + case GroupType::Categories: return pluginLib::patchDB::TagType::Category; + case GroupType::Tags: return pluginLib::patchDB::TagType::Tag; + case GroupType::Favourites: return pluginLib::patchDB::TagType::Favourites; + case GroupType::CustomA: return pluginLib::patchDB::TagType::CustomA; + case GroupType::CustomB: return pluginLib::patchDB::TagType::CustomB; + case GroupType::CustomC: return pluginLib::patchDB::TagType::CustomC; + default: return pluginLib::patchDB::TagType::Invalid; + } + } + + GroupType toGroupType(const pluginLib::patchDB::TagType _tagType) + { + switch (_tagType) + { + case pluginLib::patchDB::TagType::Category: return GroupType::Categories; + case pluginLib::patchDB::TagType::Tag: return GroupType::Tags; + case pluginLib::patchDB::TagType::Favourites: return GroupType::Favourites; + case pluginLib::patchDB::TagType::CustomA: return GroupType::CustomA; + case pluginLib::patchDB::TagType::CustomB: return GroupType::CustomB; + case pluginLib::patchDB::TagType::CustomC: return GroupType::CustomC; + default: return GroupType::Invalid; + } + } + + GroupType toGroupType(const pluginLib::patchDB::SourceType _sourceType) + { + switch (_sourceType) + { + case pluginLib::patchDB::SourceType::Rom: return GroupType::Factory; + case pluginLib::patchDB::SourceType::LocalStorage: return GroupType::LocalStorage; + case pluginLib::patchDB::SourceType::Folder: + case pluginLib::patchDB::SourceType::File: return GroupType::DataSources; + case pluginLib::patchDB::SourceType::Invalid: + case pluginLib::patchDB::SourceType::Count: + default: return GroupType::Invalid; + } + } + + pluginLib::patchDB::SourceType toSourceType(const GroupType _groupType) + { + switch (_groupType) + { + case GroupType::DataSources: return pluginLib::patchDB::SourceType::File; + case GroupType::LocalStorage: return pluginLib::patchDB::SourceType::LocalStorage; + case GroupType::Factory: return pluginLib::patchDB::SourceType::Rom; + default: return pluginLib::patchDB::SourceType::Invalid; + } + } +} diff --git a/source/jucePluginEditorLib/patchmanager/types.h b/source/jucePluginEditorLib/patchmanager/types.h @@ -0,0 +1,30 @@ +#pragma once + +namespace pluginLib::patchDB +{ + enum class SourceType; + enum class TagType; +} + +namespace jucePluginEditorLib::patchManager +{ + enum class GroupType + { + Invalid, + DataSources, + LocalStorage, + Factory, + Categories, + Tags, + Favourites, + CustomA, + CustomB, + CustomC, + Count + }; + + pluginLib::patchDB::TagType toTagType(GroupType _groupType); + GroupType toGroupType(pluginLib::patchDB::TagType _tagType); + GroupType toGroupType(pluginLib::patchDB::SourceType _sourceType); + pluginLib::patchDB::SourceType toSourceType(GroupType _groupType); +} diff --git a/source/jucePluginEditorLib/pluginEditor.cpp b/source/jucePluginEditorLib/pluginEditor.cpp @@ -7,6 +7,8 @@ #include "../synthLib/os.h" #include "../synthLib/sysexToMidi.h" +#include "patchmanager/patchmanager.h" + namespace jucePluginEditorLib { Editor::Editor(Processor& _processor, pluginLib::ParameterBinding& _binding, std::string _skinFolder) @@ -17,6 +19,8 @@ namespace jucePluginEditorLib { } + Editor::~Editor() = default; + void Editor::loadPreset(const std::function<void(const juce::File&)>& _callback) { const auto path = m_processor.getConfig().getValue("load_path", ""); @@ -24,7 +28,7 @@ namespace jucePluginEditorLib m_fileChooser = std::make_unique<juce::FileChooser>( "Choose syx/midi banks to import", path.isEmpty() ? juce::File::getSpecialLocation(juce::File::currentApplicationFile).getParentDirectory() : path, - "*.syx,*.mid,*.midi", true); + "*.syx,*.mid,*.midi,*.vstpreset,*.fxb,*.cpr", true); constexpr auto flags = juce::FileBrowserComponent::openMode | juce::FileBrowserComponent::FileChooserFlags::canSelectFiles; @@ -44,12 +48,13 @@ namespace jucePluginEditorLib void Editor::savePreset(const std::function<void(const juce::File&)>& _callback) { +#if !SYNTHLIB_DEMO_MODE const auto path = m_processor.getConfig().getValue("save_path", ""); m_fileChooser = std::make_unique<juce::FileChooser>( "Save preset(s) as syx or mid", path.isEmpty() ? juce::File::getSpecialLocation(juce::File::currentApplicationFile).getParentDirectory() : path, - "*.syx,*.mid,*.midi", true); + "*.syx,*.mid", true); constexpr auto flags = juce::FileBrowserComponent::saveMode | juce::FileBrowserComponent::FileChooserFlags::canSelectFiles; @@ -67,8 +72,12 @@ namespace jucePluginEditorLib } }; m_fileChooser->launchAsync(flags, onFileChosen); +#else + showDemoRestrictionMessageBox(); +#endif } +#if !SYNTHLIB_DEMO_MODE bool Editor::savePresets(const FileType _type, const std::string& _pathName, const std::vector<std::vector<uint8_t>>& _presets) const { if (_presets.empty()) @@ -95,6 +104,7 @@ namespace jucePluginEditorLib fclose(hFile); return true; } +#endif std::string Editor::createValidFilename(FileType& _type, const juce::File& _file) { @@ -110,6 +120,48 @@ namespace jucePluginEditorLib return file; } + void Editor::showDemoRestrictionMessageBox() const + { + const auto &[title, msg] = getDemoRestrictionText(); + juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, title, msg); + } + + void Editor::setPatchManager(patchManager::PatchManager* _patchManager) + { + m_patchManager.reset(_patchManager); + + if(_patchManager && !m_instanceConfig.empty()) + m_patchManager->setPerInstanceConfig(m_instanceConfig); + } + + void Editor::setPerInstanceConfig(const std::vector<uint8_t>& _data) + { + m_instanceConfig = _data; + + if(m_patchManager) + m_patchManager->setPerInstanceConfig(_data); + } + + void Editor::getPerInstanceConfig(std::vector<uint8_t>& _data) + { + if(m_patchManager) + { + m_instanceConfig.clear(); + m_patchManager->getPerInstanceConfig(m_instanceConfig); + } + + if(!m_instanceConfig.empty()) + _data.insert(_data.end(), m_instanceConfig.begin(), m_instanceConfig.end()); + } + + void Editor::setCurrentPart(uint8_t _part) + { + genericUI::Editor::setCurrentPart(_part); + + if(m_patchManager) + m_patchManager->setCurrentPart(_part); + } + const char* Editor::getResourceByFilename(const std::string& _name, uint32_t& _dataSize) { if(!m_skinFolder.empty()) diff --git a/source/jucePluginEditorLib/pluginEditor.h b/source/jucePluginEditorLib/pluginEditor.h @@ -2,6 +2,10 @@ #include "../juceUiLib/editor.h" +#include "../synthLib/buildconfig.h" + +#include "types.h" + namespace pluginLib { class ParameterBinding; @@ -9,26 +13,51 @@ namespace pluginLib namespace jucePluginEditorLib { + namespace patchManager + { + class PatchManager; + } + class Processor; class Editor : public genericUI::Editor, genericUI::EditorInterface { public: - enum class FileType - { - Syx, - Mid - }; - Editor(Processor& _processor, pluginLib::ParameterBinding& _binding, std::string _skinFolder); + ~Editor() override; + + Editor(const Editor&) = delete; + Editor(Editor&&) = delete; + Editor& operator = (const Editor&) = delete; + Editor& operator = (Editor&&) = delete; virtual const char* findResourceByFilename(const std::string& _filename, uint32_t& _size) = 0; void loadPreset(const std::function<void(const juce::File&)>& _callback); void savePreset(const std::function<void(const juce::File&)>& _callback); +#if !SYNTHLIB_DEMO_MODE bool savePresets(FileType _type, const std::string& _pathName, const std::vector<std::vector<uint8_t>>& _presets) const; +#endif static std::string createValidFilename(FileType& _type, const juce::File& _file); + virtual std::pair<std::string, std::string> getDemoRestrictionText() const = 0; + + void showDemoRestrictionMessageBox() const; + + Processor& getProcessor() const { return m_processor; } + + void setPatchManager(patchManager::PatchManager* _patchManager); + + patchManager::PatchManager* getPatchManager() const + { + return m_patchManager.get(); + } + + void setPerInstanceConfig(const std::vector<uint8_t>& _data) override; + void getPerInstanceConfig(std::vector<uint8_t>& _data) override; + + void setCurrentPart(uint8_t _part) override; + private: const char* getResourceByFilename(const std::string& _name, uint32_t& _dataSize) override; int getParameterIndexByName(const std::string& _name) override; @@ -45,5 +74,7 @@ namespace jucePluginEditorLib std::map<std::string, std::vector<char>> m_fileCache; std::unique_ptr<juce::FileChooser> m_fileChooser; + std::unique_ptr<patchManager::PatchManager> m_patchManager; + std::vector<uint8_t> m_instanceConfig; }; } diff --git a/source/jucePluginEditorLib/pluginEditorState.cpp b/source/jucePluginEditorLib/pluginEditorState.cpp @@ -15,12 +15,12 @@ PluginEditorState::PluginEditorState(Processor& _processor, pluginLib::Controlle int PluginEditorState::getWidth() const { - return m_virusEditor ? m_virusEditor->getWidth() : 0; + return m_editor ? m_editor->getWidth() : 0; } int PluginEditorState::getHeight() const { - return m_virusEditor ? m_virusEditor->getHeight() : 0; + return m_editor ? m_editor->getHeight() : 0; } const std::vector<PluginEditorState::Skin>& PluginEditorState::getIncludedSkins() @@ -30,7 +30,7 @@ const std::vector<PluginEditorState::Skin>& PluginEditorState::getIncludedSkins( juce::Component* PluginEditorState::getUiRoot() const { - return m_virusEditor.get(); + return m_editor.get(); } void PluginEditorState::disableBindings() @@ -55,6 +55,26 @@ void PluginEditorState::loadDefaultSkin() loadSkin(skin); } +void PluginEditorState::setPerInstanceConfig(const std::vector<uint8_t>& _data) +{ + m_instanceConfig = _data; + + if(m_editor && !m_instanceConfig.empty()) + getEditor()->setPerInstanceConfig(m_instanceConfig); +} + +void PluginEditorState::getPerInstanceConfig(std::vector<uint8_t>& _data) +{ + if(m_editor) + { + m_instanceConfig.clear(); + getEditor()->getPerInstanceConfig(m_instanceConfig); + } + + if(!m_instanceConfig.empty()) + _data.insert(_data.end(), m_instanceConfig.begin(), m_instanceConfig.end()); +} + void PluginEditorState::loadSkin(const Skin& _skin) { if(m_currentSkin == _skin) @@ -63,15 +83,18 @@ void PluginEditorState::loadSkin(const Skin& _skin) m_currentSkin = _skin; writeSkinToConfig(_skin); - if (m_virusEditor) + if (m_editor) { + m_instanceConfig.clear(); + getEditor()->getPerInstanceConfig(m_instanceConfig); + m_parameterBinding.clearBindings(); - auto* parent = m_virusEditor->getParentComponent(); + auto* parent = m_editor->getParentComponent(); - if(parent && parent->getIndexOfChildComponent(m_virusEditor.get()) > -1) - parent->removeChildComponent(m_virusEditor.get()); - m_virusEditor.reset(); + if(parent && parent->getIndexOfChildComponent(m_editor.get()) > -1) + parent->removeChildComponent(m_editor.get()); + m_editor.reset(); } m_rootScale = 1.0f; @@ -79,20 +102,23 @@ void PluginEditorState::loadSkin(const Skin& _skin) try { auto* editor = createEditor(_skin, [this] { openMenu(); }); - m_virusEditor.reset(editor); + m_editor.reset(editor); m_rootScale = editor->getScale(); - m_virusEditor->setTopLeftPosition(0, 0); + m_editor->setTopLeftPosition(0, 0); if(evSkinLoaded) - evSkinLoaded(m_virusEditor.get()); + evSkinLoaded(m_editor.get()); + + if(!m_instanceConfig.empty()) + getEditor()->setPerInstanceConfig(m_instanceConfig); } 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(); + m_editor.reset(); loadSkin(m_includedSkins[0]); } @@ -104,6 +130,11 @@ void PluginEditorState::setGuiScale(int _scale) const evSetGuiScale(_scale); } +genericUI::Editor* PluginEditorState::getEditor() const +{ + return static_cast<genericUI::Editor*>(m_editor.get()); +} + void PluginEditorState::openMenu() { const auto& config = m_processor.getConfig(); @@ -155,9 +186,9 @@ void PluginEditorState::openMenu() } } - if(m_virusEditor && m_currentSkin.folder.empty()) + if(m_editor && m_currentSkin.folder.empty()) { - auto* editor = m_virusEditor.get(); + auto* editor = m_editor.get(); if(editor) { skinMenu.addSeparator(); @@ -192,15 +223,87 @@ void PluginEditorState::openMenu() initContextMenu(menu); + juce::PopupMenu lockRegions; + + auto& regions = m_processor.getController().getParameterDescriptions().getRegions(); + + lockRegions.addItem("Unlock All", [&] + { + for (const auto& region : regions) + m_processor.getController().unlockRegion(region.first); + }); + + lockRegions.addItem("Lock All", [&] + { + for (const auto& region : regions) + m_processor.getController().lockRegion(region.first); + }); + + lockRegions.addSeparator(); + + uint32_t count = 0; + + std::map<std::string, pluginLib::ParameterRegion> sortedRegions; + for (const auto& region : regions) + sortedRegions.insert(region); + + for (const auto& region : sortedRegions) + { + lockRegions.addItem(region.second.getName(), true, m_processor.getController().isRegionLocked(region.first), [this, id=region.first] + { + if(m_processor.getController().isRegionLocked(id)) + m_processor.getController().unlockRegion(id); + else + m_processor.getController().lockRegion(id); + }); + + if(++count == 16) + { + lockRegions.addColumnBreak(); + count = 0; + } + } + + menu.addSubMenu("Lock Regions...", lockRegions); + + { + const auto allowAdvanced = config.getBoolValue("allow_advanced_options", false); + + juce::PopupMenu advancedMenu; + advancedMenu.addItem("Enable Advanced Options", true, allowAdvanced, [this, allowAdvanced] + { + if(!allowAdvanced) + { + if(juce::NativeMessageBox::showOkCancelBox(juce::AlertWindow::WarningIcon, "Warning", + "Changing these settings may cause instability of the plugin.\n" + "\n" + "Please confirm to continue.") + ) + m_processor.getConfig().setValue("allow_advanced_options", true); + } + else + { + m_processor.getConfig().setValue("allow_advanced_options", juce::var(false)); + } + }); + + advancedMenu.addSeparator(); + + if(initAdvancedContextMenu(advancedMenu, allowAdvanced)) + { + menu.addSubMenu("Advanced...", advancedMenu); + } + } + menu.showMenuAsync(juce::PopupMenu::Options()); } void PluginEditorState::exportCurrentSkin() const { - if(!m_virusEditor) + if(!m_editor) return; - auto* editor = dynamic_cast<genericUI::Editor*>(m_virusEditor.get()); + const auto* editor = dynamic_cast<const genericUI::Editor*>(m_editor.get()); if(!editor) return; @@ -209,7 +312,7 @@ void PluginEditorState::exportCurrentSkin() const if(!res.empty()) { - juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Export failed", "Failed to export skin:\n\n" + res, "OK", m_virusEditor.get()); + juce::AlertWindow::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Export failed", "Failed to export skin:\n\n" + res, "OK", m_editor.get()); } else { diff --git a/source/jucePluginEditorLib/pluginEditorState.h b/source/jucePluginEditorLib/pluginEditorState.h @@ -38,10 +38,15 @@ namespace jucePluginEditorLib } }; - explicit PluginEditorState(Processor& _processor, pluginLib::Controller& _controller, - std::vector<Skin> _includedSkins); + explicit PluginEditorState(Processor& _processor, pluginLib::Controller& _controller, std::vector<Skin> _includedSkins); virtual ~PluginEditorState() = default; + PluginEditorState(PluginEditorState&&) = delete; + PluginEditorState(const PluginEditorState&) = delete; + + PluginEditorState& operator = (PluginEditorState&&) = delete; + PluginEditorState& operator = (const PluginEditorState&) = delete; + void exportCurrentSkin() const; Skin readSkinFromConfig() const; void writeSkinToConfig(const Skin& _skin) const; @@ -66,7 +71,11 @@ namespace jucePluginEditorLib void loadDefaultSkin(); - virtual void initContextMenu(juce::PopupMenu& _menu) {}; + virtual void initContextMenu(juce::PopupMenu& _menu) {} + virtual bool initAdvancedContextMenu(juce::PopupMenu& _menu, bool _enabled) { return false; } + + void setPerInstanceConfig(const std::vector<uint8_t>& _data); + void getPerInstanceConfig(std::vector<uint8_t>& _data); protected: virtual genericUI::Editor* createEditor(const Skin& _skin, std::function<void()> _openMenuCallback) = 0; @@ -78,9 +87,12 @@ namespace jucePluginEditorLib void loadSkin(const Skin& _skin); void setGuiScale(int _scale) const; - std::unique_ptr<juce::Component> m_virusEditor; + genericUI::Editor* getEditor() const; + + std::unique_ptr<juce::Component> m_editor; Skin m_currentSkin; float m_rootScale = 1.0f; std::vector<Skin> m_includedSkins; + std::vector<uint8_t> m_instanceConfig; }; } diff --git a/source/jucePluginEditorLib/pluginEditorWindow.cpp b/source/jucePluginEditorLib/pluginEditorWindow.cpp @@ -1,6 +1,8 @@ #include "pluginEditorWindow.h" #include "pluginEditorState.h" +#include "patchmanager/patchmanager.h" + namespace jucePluginEditorLib { @@ -71,12 +73,21 @@ void EditorWindow::mouseDown(const juce::MouseEvent& 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(event.eventComponent) + { + // file browsers have their own menu, do not display two menus at once + if(event.eventComponent->findParentComponentOfClass<juce::FileBrowserComponent>()) + return; + + // patch manager has its own context menu, too + if (event.eventComponent->findParentComponentOfClass<patchManager::PatchManager>()) + return; + } if(dynamic_cast<juce::TextEditor*>(event.eventComponent)) return; + if(dynamic_cast<juce::Button*>(event.eventComponent)) + return; m_state.openMenu(); } diff --git a/source/jucePluginEditorLib/pluginProcessor.cpp b/source/jucePluginEditorLib/pluginProcessor.cpp @@ -1,9 +1,15 @@ #include "pluginProcessor.h" +#include "pluginEditorState.h" +#include "pluginEditorWindow.h" + +#include "../synthLib/binarystream.h" + namespace jucePluginEditorLib { Processor::Processor(const BusesProperties& _busesProperties, const juce::PropertiesFile::Options& _configOptions) : pluginLib::Processor(_busesProperties) + , m_configOptions(_configOptions) , m_config(_configOptions) { } @@ -23,4 +29,72 @@ namespace jucePluginEditorLib { return true; // (change this to false if you choose to not supply an editor) } + + juce::AudioProcessorEditor* Processor::createEditor() + { + assert(hasEditor() && "not supposed to be called as we declared not providing an editor"); + + if(!hasEditor()) + return nullptr; + + if(!m_editorState) + { + m_editorState.reset(createEditorState()); + if(!m_editorStateData.empty()) + m_editorState->setPerInstanceConfig(m_editorStateData); + } + + return new EditorWindow(*this, *m_editorState, getConfig()); + } + + void Processor::loadCustomData(const std::vector<uint8_t>& _sourceBuffer) + { + // Compatibility with old Vavra versions that only wrote gain parameters + if(_sourceBuffer.size() == sizeof(float) * 2 + sizeof(uint32_t)) + { + pluginLib::Processor::loadCustomData(_sourceBuffer); + return; + } + + pluginLib::Processor::loadCustomData(_sourceBuffer); + + synthLib::BinaryStream s(_sourceBuffer); + + synthLib::ChunkReader cr(s); + cr.add("EDST", 1, [this](synthLib::BinaryStream& _binaryStream, unsigned _version) + { + _binaryStream.read(m_editorStateData); + }); + + // if there is no chunk in the data, it's an old non-Vavra chunk that only carries the editor state + if(!cr.tryRead() || cr.numRead() == 0) + { + m_editorStateData = _sourceBuffer; + } + + if(m_editorState) + m_editorState->setPerInstanceConfig(m_editorStateData); + } + + void Processor::saveCustomData(std::vector<uint8_t>& _targetBuffer) + { + pluginLib::Processor::saveCustomData(_targetBuffer); + + if(m_editorState) + { + m_editorStateData.clear(); + m_editorState->getPerInstanceConfig(m_editorStateData); + } + + if(!m_editorStateData.empty()) + { + synthLib::BinaryStream s; + { + synthLib::ChunkWriter cw(s, "EDST", 1); + s.write(m_editorStateData); + } + + s.toVector(_targetBuffer, true); + } + } } diff --git a/source/jucePluginEditorLib/pluginProcessor.h b/source/jucePluginEditorLib/pluginProcessor.h @@ -4,18 +4,32 @@ namespace jucePluginEditorLib { + class PluginEditorState; + class Processor : public pluginLib::Processor { public: Processor(const BusesProperties& _busesProperties, const juce::PropertiesFile::Options& _configOptions); + juce::PropertiesFile::Options& getConfigOptions() { return m_configOptions; } juce::PropertiesFile& getConfig() { return m_config; } bool setLatencyBlocks(uint32_t _blocks) override; bool hasEditor() const override; + juce::AudioProcessorEditor* createEditor() override; + + virtual PluginEditorState* createEditorState() = 0; + + void loadCustomData(const std::vector<uint8_t>& _sourceBuffer) override; + void saveCustomData(std::vector<uint8_t>& _targetBuffer) override; private: + std::unique_ptr<PluginEditorState> m_editorState; + + juce::PropertiesFile::Options m_configOptions; juce::PropertiesFile m_config; + + std::vector<uint8_t> m_editorStateData; }; } diff --git a/source/jucePluginEditorLib/types.h b/source/jucePluginEditorLib/types.h @@ -0,0 +1,10 @@ +#pragma once + +namespace jucePluginEditorLib +{ + enum class FileType + { + Syx, + Mid + }; +} diff --git a/source/jucePluginLib/CMakeLists.txt b/source/jucePluginLib/CMakeLists.txt @@ -11,13 +11,29 @@ set(SOURCES parameterdescription.cpp parameterdescription.h parameterdescriptions.cpp parameterdescriptions.h parameterlink.cpp parameterlink.h + parameterregion.cpp parameterregion.h processor.cpp processor.h + types.h +) + +set(SOURCES_PATCHDB + patchdb/datasource.cpp patchdb/datasource.h + patchdb/db.cpp patchdb/db.h + patchdb/jobqueue.cpp patchdb/jobqueue.h + patchdb/patch.cpp patchdb/patch.h + patchdb/patchdbtypes.cpp patchdb/patchdbtypes.h + patchdb/patchhistory.cpp patchdb/patchhistory.h + patchdb/patchmodifications.cpp patchdb/patchmodifications.h + patchdb/search.cpp patchdb/search.h + patchdb/serialization.cpp patchdb/serialization.h + patchdb/tags.cpp patchdb/tags.h ) add_library(jucePluginLib STATIC) -target_sources(jucePluginLib PRIVATE ${SOURCES}) +target_sources(jucePluginLib PRIVATE ${SOURCES} ${SOURCES_PATCHDB}) source_group("source" FILES ${SOURCES}) +source_group("source\\patchdb" FILES ${SOURCES_PATCHDB}) target_link_libraries(jucePluginLib PUBLIC juceUiLib synthLib) target_include_directories(jucePluginLib PUBLIC ../JUCE/modules) diff --git a/source/jucePluginLib/controller.cpp b/source/jucePluginLib/controller.cpp @@ -272,32 +272,64 @@ namespace pluginLib return m_descriptions.getMidiPacket(_name); } - bool Controller::createMidiDataFromPacket(std::vector<uint8_t>& _sysex, const std::string& _packetName, const std::map<MidiDataType, uint8_t>& _params, uint8_t _part) const + bool Controller::createNamedParamValues(MidiPacket::NamedParamValues& _params, const std::string& _packetName, const uint8_t _part) const { const auto* m = getMidiPacket(_packetName); assert(m && "midi packet not found"); if(!m) return false; - MidiPacket::NamedParamValues paramValues; - MidiPacket::ParamIndices indices; m->getParameterIndices(indices, m_descriptions); - if(!indices.empty()) - { - for (const auto& index : indices) - { - auto* p = getParameter(index.second, _part); - if(!p) - return false; + if(indices.empty()) + return true; - const auto v = getParameterValue(p); - paramValues.insert(std::make_pair(std::make_pair(index.first, p->getDescription().name), v)); - } - } + for (const auto& index : indices) + { + auto* p = getParameter(index.second, _part); + if(!p) + return false; + + const auto v = getParameterValue(p); + _params.insert(std::make_pair(std::make_pair(index.first, p->getDescription().name), v)); + } + + return true; + } + + bool Controller::createNamedParamValues(MidiPacket::NamedParamValues& _dest, const MidiPacket::AnyPartParamValues& _source) const + { + for(uint32_t i=0; i<_source.size(); ++i) + { + const auto& v = _source[i]; + if(!v) + continue; + const auto* p = getParameter(i); + assert(p); + if(!p) + return false; + const auto key = std::make_pair(MidiPacket::AnyPart, p->getDescription().name); + _dest.insert(std::make_pair(key, *v)); + } + return true; + } + + bool Controller::createMidiDataFromPacket(std::vector<uint8_t>& _sysex, const std::string& _packetName, const std::map<MidiDataType, uint8_t>& _data, uint8_t _part) const + { + MidiPacket::NamedParamValues paramValues; + + if(!createNamedParamValues(paramValues, _packetName, _part)) + return false; + + return createMidiDataFromPacket(_sysex, _packetName, _data, paramValues); + } + + bool Controller::createMidiDataFromPacket(std::vector<uint8_t>& _sysex, const std::string& _packetName, const std::map<MidiDataType, uint8_t>& _data, const MidiPacket::NamedParamValues& _values) const + { + const auto* m = getMidiPacket(_packetName); - if(!m->create(_sysex, _params, paramValues)) + if(!m->create(_sysex, _data, _values)) { assert(false && "failed to create midi packet"); _sysex.clear(); @@ -306,6 +338,14 @@ namespace pluginLib return true; } + bool Controller::createMidiDataFromPacket(std::vector<uint8_t>& _sysex, const std::string& _packetName, const std::map<MidiDataType, uint8_t>& _data, const MidiPacket::AnyPartParamValues& _values) const + { + MidiPacket::NamedParamValues namedParams; + if(!createNamedParamValues(namedParams, _values)) + return false; + return createMidiDataFromPacket(_sysex, _packetName, _data, namedParams); + } + bool Controller::parseMidiPacket(const MidiPacket& _packet, MidiPacket::Data& _data, MidiPacket::ParamValues& _parameterValues, const std::vector<uint8_t>& _src) const { _data.clear(); @@ -313,6 +353,19 @@ namespace pluginLib return _packet.parse(_data, _parameterValues, m_descriptions, _src); } + bool Controller::parseMidiPacket(const MidiPacket& _packet, MidiPacket::Data& _data, MidiPacket::AnyPartParamValues& _parameterValues, const std::vector<uint8_t>& _src) const + { + _data.clear(); + _parameterValues.clear(); + return _packet.parse(_data, _parameterValues, m_descriptions, _src); + } + + bool Controller::parseMidiPacket(const MidiPacket& _packet, MidiPacket::Data& _data, const std::function<void(MidiPacket::ParamIndex, uint8_t)>& _parameterValues, const std::vector<uint8_t>& _src) const + { + _data.clear(); + return _packet.parse(_data, _parameterValues, m_descriptions, _src); + } + bool Controller::parseMidiPacket(const std::string& _name, MidiPacket::Data& _data, MidiPacket::ParamValues& _parameterValues, const std::vector<uint8_t>& _src) const { auto* m = getMidiPacket(_name); @@ -350,6 +403,54 @@ namespace pluginLib m_pluginMidiOut.clear(); } + bool Controller::lockRegion(const std::string& _id) + { + if(m_lockedRegions.find(_id) != m_lockedRegions.end()) + return true; + + if(m_descriptions.getRegions().find(_id) == m_descriptions.getRegions().end()) + return false; + + m_lockedRegions.insert(_id); + return true; + } + + bool Controller::unlockRegion(const std::string& _id) + { + return m_lockedRegions.erase(_id); + } + + const std::set<std::string>& Controller::getLockedRegions() const + { + return m_lockedRegions; + } + + bool Controller::isRegionLocked(const std::string& _id) + { + return m_lockedRegions.find(_id) != m_lockedRegions.end(); + } + + std::unordered_set<std::string> Controller::getLockedParameters() const + { + if(m_lockedRegions.empty()) + return {}; + + std::unordered_set<std::string> result; + + for (const auto& name : m_lockedRegions) + { + const auto& it = m_descriptions.getRegions().find(name); + if(it == m_descriptions.getRegions().end()) + continue; + + const auto& region = it->second; + for (const auto& itParam : region.getParams()) + result.insert(itParam.first); + } + + return result; + } + 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 @@ -7,6 +7,12 @@ #include <string> +namespace juce +{ + class AudioProcessor; + class Value; +} + namespace pluginLib { class Processor; @@ -30,9 +36,15 @@ namespace pluginLib const MidiPacket* getMidiPacket(const std::string& _name) const; - bool createMidiDataFromPacket(std::vector<uint8_t>& _sysex, const std::string& _packetName, const std::map<MidiDataType, uint8_t>& _params, uint8_t _part) const; + bool createNamedParamValues(MidiPacket::NamedParamValues& _params, const std::string& _packetName, uint8_t _part) const; + bool createNamedParamValues(MidiPacket::NamedParamValues& _dest, const MidiPacket::AnyPartParamValues& _source) const; + bool createMidiDataFromPacket(std::vector<uint8_t>& _sysex, const std::string& _packetName, const std::map<MidiDataType, uint8_t>& _data, uint8_t _part) const; + bool createMidiDataFromPacket(std::vector<uint8_t>& _sysex, const std::string& _packetName, const std::map<MidiDataType, uint8_t>& _data, const MidiPacket::NamedParamValues& _values) const; + bool createMidiDataFromPacket(std::vector<uint8_t>& _sysex, const std::string& _packetName, const std::map<MidiDataType, uint8_t>& _data, const MidiPacket::AnyPartParamValues& _values) const; bool parseMidiPacket(const MidiPacket& _packet, MidiPacket::Data& _data, MidiPacket::ParamValues& _parameterValues, const std::vector<uint8_t>& _src) const; + bool parseMidiPacket(const MidiPacket& _packet, MidiPacket::Data& _data, MidiPacket::AnyPartParamValues& _parameterValues, const std::vector<uint8_t>& _src) const; + bool parseMidiPacket(const MidiPacket& _packet, MidiPacket::Data& _data, const std::function<void(MidiPacket::ParamIndex, uint8_t)>& _parameterValues, const std::vector<uint8_t>& _src) const; bool parseMidiPacket(const std::string& _name, MidiPacket::Data& _data, MidiPacket::ParamValues& _parameterValues, const std::vector<uint8_t>& _src) const; bool parseMidiPacket(std::string& _name, MidiPacket::Data& _data, MidiPacket::ParamValues& _parameterValues, const std::vector<uint8_t>& _src) const; @@ -48,6 +60,14 @@ namespace pluginLib void addPluginMidiOut(const std::vector<synthLib::SMidiEvent>&); void getPluginMidiOut(std::vector<synthLib::SMidiEvent>&); + bool lockRegion(const std::string& _id); + bool unlockRegion(const std::string& _id); + const std::set<std::string>& getLockedRegions() const; + bool isRegionLocked(const std::string& _id); + std::unordered_set<std::string> getLockedParameters() const; + + const ParameterDescriptions& getParameterDescriptions() const { return m_descriptions; } + protected: virtual Parameter* createParameter(Controller& _controller, const Description& _desc, uint8_t _part, int _uid); void registerParams(juce::AudioProcessor& _processor); @@ -105,5 +125,6 @@ namespace pluginLib std::map<ParamIndex, ParameterList> m_synthParams; // exposed and managed by audio processor std::array<ParameterList, 16> m_paramsByParamType; std::vector<std::unique_ptr<Parameter>> m_synthInternalParamList; + std::set<std::string> m_lockedRegions; }; } diff --git a/source/jucePluginLib/dummydevice.h b/source/jucePluginLib/dummydevice.h @@ -9,11 +9,17 @@ namespace pluginLib public: float getSamplerate() const override { return 44100.0f; } bool isValid() const override { return false; } +#if !SYNTHLIB_DEMO_MODE bool getState(std::vector<uint8_t>& _state, synthLib::StateType _type) override { return false; } bool setState(const std::vector<uint8_t>& _state, synthLib::StateType _type) override { return false; } +#endif uint32_t getChannelCountIn() override { return 2; } uint32_t getChannelCountOut() override { return 2; } + bool setDspClockPercent(uint32_t _percent) override { return false; } + uint32_t getDspClockPercent() const override { return 100; } + uint64_t getDspClockHz() const override { return 100000000; } + protected: void readMidiOut(std::vector<synthLib::SMidiEvent>& _midiOut) override {} void processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _samples) override; diff --git a/source/jucePluginLib/midipacket.cpp b/source/jucePluginLib/midipacket.cpp @@ -15,10 +15,15 @@ namespace pluginLib m_byteToDefinitionIndex.reserve(m_definitions.size()); + std::set<uint32_t> usedParts; + for(uint32_t i=0; i<m_definitions.size(); ++i) { const auto& d = m_definitions[i]; + if(d.paramPart != AnyPart) + usedParts.insert(d.paramPart); + if(d.type == MidiDataType::Parameter) m_hasParameters = true; @@ -42,6 +47,8 @@ namespace pluginLib } m_byteSize = byteIndex + 1; + + m_numDifferentPartsUsedInParameters = static_cast<uint32_t>(usedParts.size()); } bool MidiPacket::create(std::vector<uint8_t>& _dst, const Data& _data, const NamedParamValues& _paramValues) const @@ -115,13 +122,40 @@ namespace pluginLib return create(_dst, _data, {}); } - bool MidiPacket::parse(Data& _data, ParamValues& _parameterValues, const ParameterDescriptions& _parameters, const Sysex& _src, bool _ignoreChecksumErrors/* = true*/) const + bool MidiPacket::parse(Data& _data, AnyPartParamValues& _parameterValues, const ParameterDescriptions& _parameters, const Sysex& _src, bool _ignoreChecksumErrors) const { - if(_src.size() != size()) + if(m_numDifferentPartsUsedInParameters > 0) + { + LOG("Failed to parse midi packet " << m_name << " with parameters for " << m_numDifferentPartsUsedInParameters << " different parts into parameter values that are not part-aware"); return false; + } + + _parameterValues.reserve(_src.size()); + return parse(_data, [&](ParamIndex _paramIndex, uint8_t _value) + { + const auto idx = _paramIndex.second; + if(_parameterValues.size() <= idx) + _parameterValues.resize(idx + 1); + _parameterValues[idx] = _value; + }, _parameters, _src, _ignoreChecksumErrors); + } + + bool MidiPacket::parse(Data& _data, ParamValues& _parameterValues, const ParameterDescriptions& _parameters, const Sysex& _src, bool _ignoreChecksumErrors/* = true*/) const + { _parameterValues.reserve(_src.size() << 1); + return parse(_data, [&](ParamIndex _paramIndex, uint8_t _value) + { + _parameterValues.insert(std::make_pair(_paramIndex, _value)); + }, _parameters, _src, _ignoreChecksumErrors); + } + + bool MidiPacket::parse(Data& _data, const std::function<void(ParamIndex, uint8_t)>& _addParamValueCallback, const ParameterDescriptions& _parameters, const Sysex& _src, bool _ignoreChecksumErrors) const + { + if(_src.size() != size()) + return false; + for(size_t i=0; i<_src.size(); ++i) { const auto s = _src[i]; @@ -173,7 +207,7 @@ namespace pluginLib return false; } const auto sMasked = (s >> d.paramShift) & d.paramMask; - _parameterValues.insert(std::make_pair(std::make_pair(d.paramPart, idx), sMasked)); + _addParamValueCallback(std::make_pair(d.paramPart, idx), static_cast<uint8_t>(sMasked)); } break; default: diff --git a/source/jucePluginLib/midipacket.h b/source/jucePluginLib/midipacket.h @@ -1,7 +1,9 @@ #pragma once #include <cstdint> +#include <functional> #include <map> +#include <optional> #include <set> #include <string> #include <unordered_map> @@ -72,7 +74,8 @@ namespace pluginLib using ParamIndices = std::set<ParamIndex>; 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 AnyPartParamValues = std::vector<std::optional<uint8_t>>; // index => value + using NamedParamValues = std::map<std::pair<uint8_t,std::string>, uint8_t>; // part, name => value using Sysex = std::vector<uint8_t>; MidiPacket() = default; @@ -83,7 +86,9 @@ namespace pluginLib bool create(std::vector<uint8_t>& _dst, const Data& _data, const NamedParamValues& _paramValues) const; bool create(std::vector<uint8_t>& _dst, const Data& _data) const; + bool parse(Data& _data, AnyPartParamValues& _parameterValues, const ParameterDescriptions& _parameters, const Sysex& _src, bool _ignoreChecksumErrors = true) const; bool parse(Data& _data, ParamValues& _parameterValues, const ParameterDescriptions& _parameters, const Sysex& _src, bool _ignoreChecksumErrors = true) const; + bool parse(Data& _data, const std::function<void(ParamIndex, uint8_t)>& _addParamValueCallback, const ParameterDescriptions& _parameters, const Sysex& _src, bool _ignoreChecksumErrors = true) const; bool getParameterIndices(ParamIndices& _indices, const ParameterDescriptions& _parameters) const; bool getDefinitionsForByteIndex(std::vector<const MidiDataDefinition*>& _result, uint32_t _byteIndex) const; bool getParameterIndicesForByteIndex(std::vector<ParamIndex>& _result, const ParameterDescriptions& _parameters, uint32_t _byteIndex) const; @@ -94,7 +99,10 @@ namespace pluginLib bool updateChecksums(Sysex& _data) const; + bool hasPartDependentParameters() const { return m_numDifferentPartsUsedInParameters; } + private: + static uint8_t calcChecksum(const MidiDataDefinition& _d, const Sysex& _src); const std::string m_name; @@ -103,5 +111,6 @@ namespace pluginLib std::vector<std::vector<uint32_t>> m_byteToDefinitionIndex; uint32_t m_byteSize = 0; bool m_hasParameters = false; + uint32_t m_numDifferentPartsUsedInParameters = 0; }; } diff --git a/source/jucePluginLib/parameter.cpp b/source/jucePluginLib/parameter.cpp @@ -22,24 +22,33 @@ namespace pluginLib func.second(); } - void Parameter::setDerivedValue(const int _value, ChangedBy _origin) + void Parameter::setDerivedValue(const int _value, ChangedBy _origin, bool _notifyHost) { const int newValue = juce::roundToInt(m_range.getRange().clipValue(static_cast<float>(_value))); if (newValue == m_lastValue) return; - _origin = ChangedBy::Derived; - m_lastValue = newValue; - m_lastValueOrigin = _origin; + m_lastValueOrigin = ChangedBy::Derived; - if(getDescription().isPublic) + if(_notifyHost && getDescription().isPublic) { - beginChangeGesture(); const float v = convertTo0to1(static_cast<float>(newValue)); - setValueNotifyingHost(v, _origin); - endChangeGesture(); + + switch (_origin) + { + case ChangedBy::ControlChange: + case ChangedBy::HostAutomation: + case ChangedBy::Derived: + setValue(v, ChangedBy::Derived); + break; + default: + beginChangeGesture(); + setValueNotifyingHost(v, ChangedBy::Derived); + endChangeGesture(); + break; + } } else { @@ -97,7 +106,7 @@ namespace pluginLib for (const auto& parameter : m_derivedParameters) { if(!parameter->m_changingDerivedValues) - parameter->setDerivedValue(m_value.getValue(), _origin); + parameter->setDerivedValue(m_value.getValue(), _origin, true); } m_changingDerivedValues = false; @@ -131,7 +140,7 @@ namespace pluginLib m_changingDerivedValues = true; for (const auto& p : m_derivedParameters) - p->setDerivedValue(newValue, _origin); + p->setDerivedValue(newValue, _origin, notifyHost); m_changingDerivedValues = false; } diff --git a/source/jucePluginLib/parameter.h b/source/jucePluginLib/parameter.h @@ -1,11 +1,11 @@ #pragma once -#include <juce_audio_processors/juce_audio_processors.h> - #include <set> #include "parameterdescription.h" +#include "juce_audio_processors/juce_audio_processors.h" + namespace pluginLib { struct Description; @@ -39,6 +39,7 @@ namespace pluginLib bool isMetaParameter() const override; float getValue() const override { return convertTo0to1(m_value.getValue()); } + int getUnnormalizedValue() const { return juce::roundToInt(m_value.getValue()); } void setValue(float _newValue) override; void setValue(float _newValue, ChangedBy _origin); void setValueFromSynth(int newValue, bool notifyHost, ChangedBy _origin); @@ -85,7 +86,7 @@ namespace pluginLib private: static juce::String genId(const Description &d, int part, int uniqueId); void valueChanged(juce::Value &) override; - void setDerivedValue(int _value, ChangedBy _origin); + void setDerivedValue(int _value, ChangedBy _origin, bool _notifyHost); void sendToSynth(); Controller &m_ctrl; diff --git a/source/jucePluginLib/parameterbinding.cpp b/source/jucePluginLib/parameterbinding.cpp @@ -27,6 +27,11 @@ namespace pluginLib m_param->setValueNotifyingHost(m_param->convertTo0to1(static_cast<float>(m_slider->getValue())), Parameter::ChangedBy::ControlChange); } + void ParameterBinding::MouseListener::mouseDoubleClick(const juce::MouseEvent& event) + { + m_param->setValueNotifyingHost(m_param->getDefaultValue(), Parameter::ChangedBy::ControlChange); + } + ParameterBinding::~ParameterBinding() { clearBindings(); @@ -89,20 +94,62 @@ namespace pluginLib _combo.onChange = nullptr; _combo.clear(); - int idx = 1; - uint32_t count = 0; - for (const auto& vs : v->getAllValueStrings()) + using Entry = std::pair<uint8_t, std::string>; + + std::vector<Entry> sortedValues; + + const auto& allValues = v->getAllValueStrings(); + uint8_t i = 0; + for (const auto& vs : allValues) { if(vs.isNotEmpty()) + sortedValues.emplace_back(i, vs.toStdString()); + ++i; + } + /* + std::sort(sortedValues.begin(), sortedValues.end(), [](const Entry& _a, const Entry& _b) + { + const auto aOff =_a.second == "Off" || _a.second == "-"; + const auto bOff =_b.second == "Off" || _b.second == "-"; + + if(aOff && !bOff) + return true; + + if(!aOff && bOff) + return false; + + auto noDigitsString = [](const std::string& _s) { - _combo.addItem(vs, idx); - if(++count == 16) + std::string s; + s.reserve(_s.size()); + for (const char c : _s) { - _combo.getRootMenu()->addColumnBreak(); - count = 0; + if(isdigit(c)) + break; + s += c; } + return s; + }; + + const auto a = noDigitsString(_a.second); + const auto b = noDigitsString(_b.second); + + if(a == b) + return _a.first < _b.first; + + return a < b; + }); + */ + uint32_t count = 0; + + for (const auto& vs : sortedValues) + { + _combo.addItem(vs.second, vs.first + 1); + if(++count == 16) + { + _combo.getRootMenu()->addColumnBreak(); + count = 0; } - idx++; } _combo.setSelectedId(static_cast<int>(v->getValueObject().getValueSource().getValue()) + 1, juce::dontSendNotification); @@ -130,11 +177,11 @@ namespace pluginLib const auto listenerId = m_nextListenerId++; - v->onValueChanged.emplace_back(std::make_pair(listenerId, [this, &_combo, v]() + v->onValueChanged.emplace_back(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); diff --git a/source/jucePluginLib/parameterbinding.h b/source/jucePluginLib/parameterbinding.h @@ -1,9 +1,12 @@ #pragma once -#include <juce_audio_processors/juce_audio_processors.h> +#include "juce_gui_basics/juce_gui_basics.h" namespace juce { + class Button; + class ComboBox; + class Component; class MouseEvent; class Slider; } @@ -22,6 +25,7 @@ namespace pluginLib void mouseDown(const juce::MouseEvent& event) override; void mouseUp(const juce::MouseEvent& event) override; void mouseDrag(const juce::MouseEvent& event) override; + void mouseDoubleClick(const juce::MouseEvent& event) override; private: pluginLib::Parameter *m_param; diff --git a/source/jucePluginLib/parameterdescription.h b/source/jucePluginLib/parameterdescription.h @@ -2,8 +2,9 @@ #include <cstdint> #include <functional> +#include <map> -#include <juce_audio_processors/juce_audio_processors.h> +#include "juce_core/juce_core.h" namespace pluginLib { diff --git a/source/jucePluginLib/parameterdescriptions.cpp b/source/jucePluginLib/parameterdescriptions.cpp @@ -331,6 +331,9 @@ namespace pluginLib const auto parameterLinks = json["parameterlinks"].getArray(); parseParameterLinks(errors, parameterLinks); + const auto regions = json["regions"].getArray(); + parseParameterRegions(errors, regions); + auto res = errors.str(); if(!res.empty()) @@ -527,7 +530,7 @@ namespace pluginLib m_midiPackets.insert(std::make_pair(_key, packet)); } - void ParameterDescriptions::parseParameterLinks(std::stringstream& _errors, juce::Array<juce::var>* _links) + void ParameterDescriptions::parseParameterLinks(std::stringstream& _errors, const juce::Array<juce::var>* _links) { if(!_links) return; @@ -613,4 +616,117 @@ namespace pluginLib m_parameterLinks.push_back(link); } + + void ParameterDescriptions::parseParameterRegions(std::stringstream& _errors, const juce::Array<juce::var>* _regions) + { + if(!_regions) + return; + + for (const auto& _region : *_regions) + parseParameterRegion(_errors, _region); + } + + void ParameterDescriptions::parseParameterRegion(std::stringstream& _errors, const juce::var& _value) + { + const auto id = _value["id"].toString().toStdString(); + const auto name = _value["name"].toString().toStdString(); + const auto parameters = _value["parameters"].getArray(); + const auto regions = _value["regions"].getArray(); + + if(id.empty()) + { + _errors << "region needs to have an id\n"; + return; + } + + if(m_regions.find(id) != m_regions.end()) + { + _errors << "region with id '" << id << "' already exists\n"; + return; + } + + if(name.empty()) + { + _errors << "region with id " << id << " needs to have a name\n"; + return; + } + + if(!parameters && !regions) + { + _errors << "region with id " << id << " needs to at least one parameter or region\n"; + return; + } + + std::unordered_map<std::string, const Description*> paramMap; + + if(parameters) + { + const auto& params = *parameters; + + for (const auto& i : params) + { + const auto& param = i.toString().toStdString(); + + if(param.empty()) + { + _errors << "Empty parameter name in parameter list for region " << id << '\n'; + return; + } + + uint32_t idx = 0; + + if(!getIndexByName(idx, param)) + { + _errors << "Parameter with name '" << param << "' not found for region " << id << '\n'; + return; + } + + const auto* desc = &m_descriptions[idx]; + + if(paramMap.find(param) != paramMap.end()) + { + _errors << "Parameter with name '" << param << "' has been specified more than once for region " << id << '\n'; + return; + } + + paramMap.insert({param, desc}); + } + } + + if(regions) + { + const auto& regs = *regions; + + for (const auto& i : regs) + { + const auto& reg = i.toString().toStdString(); + + if(reg.empty()) + { + _errors << "Empty region specified in region '" << id << "'\n"; + return; + } + + const auto it = m_regions.find(reg); + + if(it == m_regions.end()) + { + _errors << "Region with id '" << reg << "' not found for region '" << id << "'\n"; + return; + } + + const auto& region = it->second; + + const auto& regParams = region.getParams(); + + for (const auto& itParam : regParams) + { + if(paramMap.find(itParam.first) == paramMap.end()) + paramMap.insert(itParam); + } + } + } + + m_regions.insert({id, ParameterRegion(id, name, std::move(paramMap))}); + } } diff --git a/source/jucePluginLib/parameterdescriptions.h b/source/jucePluginLib/parameterdescriptions.h @@ -7,6 +7,15 @@ #include "midipacket.h" #include "parameterdescription.h" #include "parameterlink.h" +#include "parameterregion.h" + +#include "juce_core/juce_core.h" + +namespace juce +{ + class var; + class DynamicObject; +} namespace pluginLib { @@ -28,18 +37,24 @@ namespace pluginLib const std::unordered_map<std::string, MidiPacket>& getMidiPackets() const { return m_midiPackets; } + const auto& getRegions() const { return m_regions; } + private: std::string loadJson(const std::string& _jsonString); void parseMidiPackets(std::stringstream& _errors, juce::DynamicObject* _packets); void parseMidiPacket(std::stringstream& _errors, const std::string& _key, const juce::var& _value); - void parseParameterLinks(std::stringstream& _errors, juce::Array<juce::var>* _links); + void parseParameterLinks(std::stringstream& _errors, const juce::Array<juce::var>* _links); void parseParameterLink(std::stringstream& _errors, const juce::var& _value); + void parseParameterRegions(std::stringstream& _errors, const juce::Array<juce::var>* _regions); + void parseParameterRegion(std::stringstream& _errors, const juce::var& _value); + std::unordered_map<std::string, ValueList> m_valueLists; std::vector<Description> m_descriptions; std::unordered_map<std::string, uint32_t> m_nameToIndex; std::unordered_map<std::string, MidiPacket> m_midiPackets; std::vector<ParameterLink> m_parameterLinks; + std::unordered_map<std::string, ParameterRegion> m_regions; }; } diff --git a/source/jucePluginLib/parameterregion.cpp b/source/jucePluginLib/parameterregion.cpp @@ -0,0 +1,8 @@ +#include "parameterregion.h" + +namespace pluginLib +{ + ParameterRegion::ParameterRegion(std::string _id, std::string _name, std::unordered_map<std::string, const Description*>&& _params) : m_id(std::move(_id)), m_name(std::move(_name)), m_params(std::move(_params)) + { + } +} diff --git a/source/jucePluginLib/parameterregion.h b/source/jucePluginLib/parameterregion.h @@ -0,0 +1,23 @@ +#pragma once + +#include <string> + +#include "parameterdescription.h" + +namespace pluginLib +{ + class ParameterRegion + { + public: + ParameterRegion(std::string _id, std::string _name, std::unordered_map<std::string, const Description*>&& _params); + + const auto& getId() const { return m_id; } + const auto& getName() const { return m_name; } + const auto& getParams() const { return m_params; } + + private: + const std::string m_id; + const std::string m_name; + const std::unordered_map<std::string, const Description*> m_params; + }; +} diff --git a/source/jucePluginLib/patchdb/datasource.cpp b/source/jucePluginLib/patchdb/datasource.cpp @@ -0,0 +1,207 @@ +#include "datasource.h" + +#include <algorithm> +#include <sstream> +#include <memory> + +#include "patch.h" + +namespace pluginLib::patchDB +{ + bool DataSource::createConsecutiveProgramNumbers() + { + if(patches.empty()) + return false; + + // note that this does NOT sort the patches member, it is a set that cannot be sorted, we only generate consecutive program numbers here + + std::vector patchesVector(patches.begin(), patches.end()); + + sortByProgram(patchesVector); + + return createConsecutiveProgramNumbers(patchesVector); + } + + bool DataSource::createConsecutiveProgramNumbers(const std::vector<PatchPtr>& _patches) + { + bool dirty = false; + uint32_t program = 0; + + for (const auto& patch : _patches) + { + const auto p = program++; + + if(patch->program == p) + continue; + + patch->program = p; + dirty = true; + } + + return dirty; + } + + bool DataSource::makeSpaceForNewPatches(const uint32_t _insertPosition, const uint32_t _count) const + { + bool dirty = true; + + for (const auto& patch : patches) + { + if(patch->program >= _insertPosition) + { + patch->program += _count; + dirty = true; + } + } + return dirty; + } + + std::pair<uint32_t, uint32_t> DataSource::getProgramNumberRange() const + { + if(patches.empty()) + return {g_invalidProgram, g_invalidProgram}; + + uint32_t min = std::numeric_limits<uint32_t>::max(); + uint32_t max = std::numeric_limits<uint32_t>::min(); + + for (const auto& patch : patches) + { + min = std::min(patch->program, min); + max = std::max(patch->program, max); + } + + return {min, max}; + } + + uint32_t DataSource::getMaxProgramNumber() const + { + return getProgramNumberRange().second; + } + + void DataSource::sortByProgram(std::vector<PatchPtr>& _patches) + { + std::sort(_patches.begin(), _patches.end(), [&](const PatchPtr& _a, const PatchPtr& _b) + { + return _a->program < _b->program; + }); + } + + bool DataSource::contains(const PatchPtr& _patch) const + { + return patches.find(_patch) != patches.end(); + } + + bool DataSource::movePatchesTo(const uint32_t _position, const std::vector<PatchPtr>& _patches) + { + std::vector patchesVec(patches.begin(), patches.end()); + sortByProgram(patchesVec); + + createConsecutiveProgramNumbers(patchesVec); + + uint32_t targetPosition = _position; + + // insert position has to be decremented by 1 for each patch that is reinserted that has a position less than the target position + for (const auto& patch : _patches) + { + if(patch->program < _position) + --targetPosition; + } + + if(!remove(_patches)) + return false; + + patchesVec.assign(patches.begin(), patches.end()); + sortByProgram(patchesVec); + + if(targetPosition >= patchesVec.size()) + patchesVec.insert(patchesVec.end(), _patches.begin(), _patches.end()); + else + patchesVec.insert(patchesVec.begin() + targetPosition, _patches.begin(), _patches.end()); + + createConsecutiveProgramNumbers(patchesVec); + + for (const auto& patch : _patches) + patches.insert(patch); + + return true; + } + + bool DataSource::remove(const PatchPtr& _patch) + { + return patches.erase(_patch); + } + + std::string DataSource::toString() const + { + std::stringstream ss; + + ss << "type|" << patchDB::toString(type); + ss << "|name|" << name; + if (bank != g_invalidBank) + ss << "|bank|" << bank; +// if (program != g_invalidProgram) +// ss << "|prog|" << program; + return ss.str(); + } + + DataSourceNode::DataSourceNode(const DataSource& _ds) : DataSource(_ds) + { + } + + DataSourceNode::~DataSourceNode() + { + setParent(nullptr); + removeAllChildren(); + } + + void DataSourceNode::setParent(const DataSourceNodePtr& _parent) + { + if (getParent() == _parent) + return; + + if(m_parent) + { + // we MUST NOT create a new ptr to this here as we may be called from our destructor, in which case there shouldn't be a pointer in there anyway + for(uint32_t i=0; i<static_cast<uint32_t>(m_parent->m_children.size()); ++i) + { + auto& child = m_parent->m_children[i]; + auto ptr = child.lock(); + if (ptr && ptr.get() == this) + { + m_parent->m_children.erase(m_parent->m_children.begin() + i); + break; + } + } + } + + m_parent = _parent; + + if(_parent) + _parent->m_children.emplace_back(shared_from_this()); + } + + bool DataSourceNode::isChildOf(const DataSourceNode* _ds) const + { + auto node = this; + + while(node) + { + if (_ds == node) + return true; + node = node->m_parent.get(); + } + return false; + } + + void DataSourceNode::removeAllChildren() + { + while(!m_children.empty()) + { + const auto& c = m_children.back().lock(); + if (c) + c->setParent(nullptr); + else + m_children.pop_back(); + } + } +} diff --git a/source/jucePluginLib/patchdb/datasource.h b/source/jucePluginLib/patchdb/datasource.h @@ -0,0 +1,137 @@ +#pragma once + +#include <string> + +#include "patchdbtypes.h" + +namespace pluginLib::patchDB +{ + struct DataSource + { + SourceType type = SourceType::Invalid; + DataSourceOrigin origin = DataSourceOrigin::Invalid; + std::string name; + uint32_t bank = g_invalidBank; +// uint32_t program = g_invalidProgram; + Timestamp timestamp; + std::set<PatchPtr> patches; + + virtual ~DataSource() = default; + + bool createConsecutiveProgramNumbers(); // returns true if any patch was modified + static bool createConsecutiveProgramNumbers(const std::vector<PatchPtr>& _patches); // returns true if any patch was modified + + bool makeSpaceForNewPatches(uint32_t _insertPosition, uint32_t _count) const; + + std::pair<uint32_t, uint32_t> getProgramNumberRange() const; + uint32_t getMaxProgramNumber() const; + static void sortByProgram(std::vector<PatchPtr>& _patches); + + bool contains(const PatchPtr& _patch) const; + + template<typename T> + bool containsAll(const T& _patches) const + { + for (auto p : _patches) + { + if(!contains(p)) + return false; + } + return true; + } + + template<typename T> + bool containsAny(const T& _patches) const + { + for (auto p : _patches) + { + if(contains(p)) + return true; + } + return false; + } + + bool movePatchesTo(uint32_t _position, const std::vector<PatchPtr>& _patches); + + template<typename T> + bool remove(const T& _patches) + { + if(!containsAll(_patches)) + return false; + + for (const auto& patch : _patches) + patches.erase(patch); + + return true; + } + + bool remove(const PatchPtr& _patch); + + bool operator == (const DataSource& _ds) const + { + return type == _ds.type && name == _ds.name && bank == _ds.bank;//&& program == _ds.program; + } + + bool operator != (const DataSource& _ds) const + { + return !(*this == _ds); + } + + bool operator < (const DataSource& _ds) const + { +// if (parent < _ds.parent) return true; +// if (parent > _ds.parent) return false; + + if (type < _ds.type) return true; + if (type > _ds.type) return false; + + if (bank < _ds.bank) return true; + if (bank > _ds.bank) return false; + + /* + if (program < _ds.program) return true; + if (program > _ds.program) return false; + */ + + if (name < _ds.name) return true; + if (name > _ds.name) return false; + + return false; + } + + bool operator > (const DataSource& _ds) const + { + return _ds < *this; + } + + std::string toString() const; + }; + + struct DataSourceNode final : DataSource, std::enable_shared_from_this<DataSourceNode> + { + DataSourceNode() = default; + DataSourceNode(const DataSourceNode&) = delete; + + explicit DataSourceNode(const DataSource& _ds); + explicit DataSourceNode(DataSourceNode&&) = delete; + + ~DataSourceNode() override; + + DataSourceNode& operator = (const DataSourceNode&) = delete; + DataSourceNode& operator = (DataSourceNode&& _other) noexcept = delete; + + auto& getParent() const { return m_parent; } + auto hasParent() const { return getParent() != nullptr; } + const auto& getChildren() const { return m_children; } + + void setParent(const DataSourceNodePtr& _parent); + + bool isChildOf(const DataSourceNode* _ds) const; + void removeAllChildren(); + + private: + DataSourceNodePtr m_parent; + + std::vector<std::weak_ptr<DataSourceNode>> m_children; + }; +} diff --git a/source/jucePluginLib/patchdb/db.cpp b/source/jucePluginLib/patchdb/db.cpp @@ -0,0 +1,1635 @@ +#include "db.h" + +#include <cassert> + +#include "datasource.h" +#include "patch.h" +#include "patchmodifications.h" + +#include "../../synthLib/os.h" +#include "../../synthLib/midiToSysex.h" +#include "../../synthLib/hybridcontainer.h" + +#include "dsp56kEmu/logging.h" + +namespace pluginLib::patchDB +{ + namespace + { + std::string createValidFilename(const std::string& _name) + { + std::string result; + result.reserve(_name.size()); + + for (const char c : _name) + { + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) + result += c; + else + result += '_'; + } + return result; + } + } + DB::DB(juce::File _dir) + : m_settingsDir(std::move(_dir)) + , m_jsonFileName(m_settingsDir.getChildFile("patchmanagerdb.json")) + , m_loader("PatchLoader", false, dsp56k::ThreadPriority::Lowest) + { + } + + DB::~DB() + { + assert(m_loader.destroyed() && "stopLoaderThread() needs to be called by derived class in destructor"); + stopLoaderThread(); + } + + DataSourceNodePtr DB::addDataSource(const DataSource& _ds) + { + return addDataSource(_ds, true); + } + + bool DB::writePatchesToFile(const juce::File& _file, const std::vector<PatchPtr>& _patches) + { + std::vector<uint8_t> sysexBuffer; + sysexBuffer.reserve(_patches.front()->sysex.size() * _patches.size()); + + for (const auto& patch : _patches) + { + const auto patchSysex = patch->sysex; + + if(!patchSysex.empty()) + sysexBuffer.insert(sysexBuffer.end(), patchSysex.begin(), patchSysex.end()); + } + + return _file.replaceWithData(sysexBuffer.data(), sysexBuffer.size()); + } + + DataSourceNodePtr DB::addDataSource(const DataSource& _ds, const bool _save) + { + const auto needsSave = _save && _ds.origin == DataSourceOrigin::Manual && _ds.type != SourceType::Rom; + + auto ds = std::make_shared<DataSourceNode>(_ds); + + runOnLoaderThread([this, ds, needsSave] + { + addDataSource(ds); + if(needsSave) + saveJson(); + }); + + return ds; + } + + void DB::removeDataSource(const DataSource& _ds, bool _save/* = true*/) + { + runOnLoaderThread([this, _ds, _save] + { + std::unique_lock lockDs(m_dataSourcesMutex); + + const auto it = m_dataSources.find(_ds); + if (it == m_dataSources.end()) + return; + + const auto ds = it->second; + + // if a DS is removed that is of type Manual, and it has a parent, switch it to Autogenerated but don't remove it + if (ds->origin == DataSourceOrigin::Manual && ds->hasParent()) + { + ds->origin = DataSourceOrigin::Autogenerated; + + std::unique_lock lockUi(m_uiMutex); + m_dirty.dataSources = true; + return; + } + + std::set<DataSourceNodePtr> removedDataSources{it->second}; + std::vector<PatchPtr> removedPatches; + + // remove all datasources that are a child of the one being removed + std::function<void(const DataSourceNodePtr&)> removeChildren = [&](const DataSourceNodePtr& _parent) + { + for (auto& child : _parent->getChildren()) + { + const auto c = child.lock(); + + if (!c || c->origin == DataSourceOrigin::Manual) + continue; + + removedDataSources.insert(c); + removeChildren(c); + } + }; + + removeChildren(ds); + + for (const auto& removed : removedDataSources) + { + removedPatches.insert(removedPatches.end(), removed->patches.begin(), removed->patches.end()); + m_dataSources.erase(*removed); + } + + lockDs.unlock(); + + const auto patchesChanged = !removedPatches.empty(); + + removePatchesFromSearches(removedPatches); + + { + std::unique_lock lockUi(m_uiMutex); + + m_dirty.dataSources = true; + if (patchesChanged) + m_dirty.patches = true; + } + + for (auto& removedDataSource : removedDataSources) + { + removedDataSource->setParent(nullptr); + removedDataSource->removeAllChildren(); + removedDataSource->patches.clear(); + } + removedDataSources.clear(); + + if(_save) + saveJson(); + }); + } + + void DB::refreshDataSource(const DataSourceNodePtr& _ds) + { + auto parent = _ds->getParent(); + + removeDataSource(*_ds, false); + + runOnLoaderThread([this, parent, _ds] + { + _ds->setParent(parent); + addDataSource(_ds); + }); + } + + void DB::renameDataSource(const DataSourceNodePtr& _ds, const std::string& _newName) + { + if(_ds->type != SourceType::LocalStorage) + return; + + if(_newName.empty()) + return; + + runOnLoaderThread([this, _ds, _newName] + { + { + std::unique_lock lockDs(m_dataSourcesMutex); + const auto it = m_dataSources.find(*_ds); + + if(it == m_dataSources.end()) + return; + + const auto ds = it->second; + + if(ds->name == _newName) + return; + + for (const auto& [_, d] : m_dataSources) + { + if(d->type == SourceType::LocalStorage && d->name == _newName) + return; + } + + ds->name = _newName; + + m_dataSources.erase(it); + m_dataSources.insert({*ds, ds}); + } + + std::unique_lock lockUi(m_uiMutex); + m_dirty.dataSources = true; + + saveJson(); + }); + } + + bool DB::setTagColor(const TagType _type, const Tag& _tag, const Color _color) + { + std::shared_lock lock(m_patchesMutex); + if(_color == g_invalidColor) + { + const auto itType = m_tagColors.find(_type); + + if(itType == m_tagColors.end()) + return false; + + if(!itType->second.erase(_tag)) + return false; + } + else + { + if(m_tagColors[_type][_tag] == _color) + return false; + m_tagColors[_type][_tag] = _color; + } + + std::unique_lock lockUi(m_uiMutex); + m_dirty.tags.insert(_type); + + // TODO: this might spam saving if this function is called too often + runOnLoaderThread([this] + { + saveJson(); + }); + return true; + } + + Color DB::getTagColor(const TagType _type, const Tag& _tag) const + { + std::shared_lock lock(m_patchesMutex); + return getTagColorInternal(_type, _tag); + } + + Color DB::getPatchColor(const PatchPtr& _patch, const TypedTags& _tagsToIgnore) const + { + const auto& tags = _patch->getTags(); + + for (const auto& itType : tags.get()) + { + for (const auto& tag : itType.second.getAdded()) + { + if(_tagsToIgnore.containsAdded(itType.first, tag)) + continue; + + const auto c = getTagColor(itType.first, tag); + if(c != g_invalidColor) + return c; + } + } + + return g_invalidColor; + } + + bool DB::addTag(const TagType _type, const std::string& _tag) + { + { + std::unique_lock lock(m_patchesMutex); + if (!internalAddTag(_type, _tag)) + return false; + } + saveJson(); + return true; + } + + bool DB::removeTag(TagType _type, const Tag& _tag) + { + { + std::unique_lock lock(m_patchesMutex); + if (!internalRemoveTag(_type, _tag)) + return false; + } + saveJson(); + return true; + } + + void DB::uiProcess(Dirty& _dirty) + { + std::list<std::function<void()>> uiFuncs; + { + std::scoped_lock lock(m_uiMutex); + std::swap(uiFuncs, m_uiFuncs); + _dirty = m_dirty; + m_dirty = {}; + } + + for (const auto& func : uiFuncs) + func(); + } + + uint32_t DB::search(SearchRequest&& _request, SearchCallback&& _callback) + { + const auto handle = m_nextSearchHandle++; + + auto s = std::make_shared<Search>(); + + s->handle = handle; + s->request = std::move(_request); + s->callback = std::move(_callback); + + { + std::unique_lock lock(m_searchesMutex); + m_searches.insert({ s->handle, s }); + } + + runOnLoaderThread([this, s] + { + executeSearch(*s); + }); + + return handle; + } + + SearchHandle DB::findDatasourceForPatch(const PatchPtr& _patch, SearchCallback&& _callback) + { + SearchRequest req; + req.patch = _patch; + return search(std::move(req), std::move(_callback)); + } + + void DB::cancelSearch(const uint32_t _handle) + { + std::unique_lock lock(m_searchesMutex); + m_cancelledSearches.insert(_handle); + m_searches.erase(_handle); + } + + std::shared_ptr<Search> DB::getSearch(const SearchHandle _handle) + { + std::shared_lock lock(m_searchesMutex); + const auto it = m_searches.find(_handle); + if (it == m_searches.end()) + return {}; + return it->second; + } + + std::shared_ptr<Search> DB::getSearch(const DataSource& _dataSource) + { + std::shared_lock lock(m_searchesMutex); + + for (const auto& it : m_searches) + { + const auto& search = it.second; + if(!search->request.sourceNode) + continue; + if(*search->request.sourceNode == _dataSource) + return search; + } + return nullptr; + } + + void DB::copyPatchesTo(const DataSourceNodePtr& _ds, const std::vector<PatchPtr>& _patches, int _insertRow/* = -1*/) + { + if (_ds->type != SourceType::LocalStorage) + return; + + runOnLoaderThread([this, _ds, _patches, _insertRow] + { + { + std::shared_lock lockDs(m_dataSourcesMutex); + const auto itDs = m_dataSources.find(*_ds); + if (itDs == m_dataSources.end()) + return; + } + + // filter out all patches that are already part of _ds + std::vector<PatchPtr> patchesToAdd; + patchesToAdd.reserve(_patches.size()); + + for (const auto& patch : _patches) + { + if (_ds->contains(patch)) + continue; + + patchesToAdd.push_back(patch); + } + + if(patchesToAdd.empty()) + return; + + std::vector<PatchPtr> newPatches; + newPatches.reserve(patchesToAdd.size()); + + uint32_t newPatchProgramNumber = _insertRow >= 0 ? static_cast<uint32_t>(_insertRow) : _ds->getMaxProgramNumber() + 1; + + if(newPatchProgramNumber > _ds->getMaxProgramNumber() + 1) + newPatchProgramNumber = _ds->getMaxProgramNumber() + 1; + + _ds->makeSpaceForNewPatches(newPatchProgramNumber, static_cast<uint32_t>(patchesToAdd.size())); + + for (const auto& patch : patchesToAdd) + { + auto [newPatch, newMods] = patch->createCopy(_ds); + + newPatch->program = newPatchProgramNumber++; + + newPatches.push_back(newPatch); + } + + addPatches(newPatches); + + createConsecutiveProgramNumbers(_ds); + + saveJson(); + }); + } + + void DB::removePatches(const DataSourceNodePtr& _ds, const std::vector<PatchPtr>& _patches) + { + if (_ds->type != SourceType::LocalStorage) + return; + + runOnLoaderThread([this, _ds, _patches] + { + { + std::shared_lock lockDs(m_dataSourcesMutex); + const auto itDs = m_dataSources.find(*_ds); + if (itDs == m_dataSources.end()) + return; + } + + { + std::vector<PatchPtr> removedPatches; + removedPatches.reserve(_patches.size()); + + std::unique_lock lock(m_patchesMutex); + + for (const auto& patch : _patches) + { + if(_ds->patches.erase(patch)) + removedPatches.emplace_back(patch); + } + + if (removedPatches.empty()) + return; + + removePatchesFromSearches(removedPatches); + + { + std::unique_lock lockUi(m_uiMutex); + m_dirty.patches = true; + } + } + + saveJson(); + }); + } + + bool DB::movePatchesTo(const uint32_t _position, const std::vector<PatchPtr>& _patches) + { + if(_patches.empty()) + return false; + + { + std::unique_lock lock(m_patchesMutex); + + const auto ds = _patches.front()->source.lock(); + + if(!ds || ds->type != SourceType::LocalStorage) + return false; + + if(!ds->movePatchesTo(_position, _patches)) + return false; + } + + { + std::unique_lock lockUi(m_uiMutex); + m_dirty.dataSources = true; + } + + runOnLoaderThread([this] + { + saveJson(); + }); + + return true; + } + + bool DB::isValid(const PatchPtr& _patch) + { + if (!_patch) + return false; + if (_patch->getName().empty()) + return false; + if (_patch->sysex.empty()) + return false; + if (_patch->sysex.front() != 0xf0) + return false; + if (_patch->sysex.back() != 0xf7) + return false; + return true; + } + + PatchPtr DB::requestPatchForPart(const uint32_t _part) + { + Data data; + requestPatchForPart(data, _part); + return initializePatch(std::move(data)); + } + + void DB::getTags(const TagType _type, std::set<Tag>& _tags) + { + _tags.clear(); + + std::shared_lock lock(m_patchesMutex); + const auto it = m_tags.find(_type); + if (it == m_tags.end()) + return; + + _tags = it->second; + } + + bool DB::modifyTags(const std::vector<PatchPtr>& _patches, const TypedTags& _tags) + { + if(_tags.empty()) + return false; + + std::vector<PatchPtr> changed; + changed.reserve(_patches.size()); + + std::unique_lock lock(m_patchesMutex); + + for (const auto& patch : _patches) + { + if(patch->source.expired()) + continue; + + const auto key = PatchKey(*patch); + + auto mods = patch->modifications; + + if(!mods) + { + mods = std::make_shared<PatchModifications>(); + mods->patch = patch; + patch->modifications = mods; + } + + if (!mods->modifyTags(_tags)) + continue; + + changed.push_back(patch); + } + + if(!changed.empty()) + { + updateSearches(changed); + } + + lock.unlock(); + + if(!changed.empty()) + saveJson(); + + return true; + } + + bool DB::renamePatch(const PatchPtr& _patch, const std::string& _name) + { + if(_patch->getName() == _name) + return false; + + if(_name.empty()) + return false; + + { + std::unique_lock lock(m_patchesMutex); + + const auto ds = _patch->source.lock(); + if(!ds) + return false; + + auto mods = _patch->modifications; + if(!mods) + { + mods = std::make_shared<PatchModifications>(); + mods->patch = _patch; + _patch->modifications = mods; + } + + mods->name = _name; + + mods->updateCache(); + + updateSearches({_patch}); + } + + runOnLoaderThread([this] + { + saveJson(); + }); + + return true; + } + + bool DB::replacePatch(const PatchPtr& _existing, const PatchPtr& _new) + { + if(!_existing || !_new) + return false; + + if(_existing == _new) + return false; + + const auto ds = _existing->source.lock(); + + if(!ds || ds->type != SourceType::LocalStorage) + return false; + + std::unique_lock lock(m_patchesMutex); + + _existing->replaceData(*_new); + + if(_existing->modifications) + _existing->modifications->name.clear(); + + updateSearches({_existing}); + + runOnLoaderThread([this] + { + saveJson(); + }); + + return true; + } + + SearchHandle DB::search(SearchRequest&& _request) + { + return search(std::move(_request), [](const Search&) {}); + } + + bool DB::loadData(DataList& _results, const DataSourceNodePtr& _ds) + { + return loadData(_results, *_ds); + } + + bool DB::loadData(DataList& _results, const DataSource& _ds) + { + switch (_ds.type) + { + case SourceType::Rom: + return loadRomData(_results, _ds.bank, g_invalidProgram); + case SourceType::File: + return loadFile(_results, _ds.name); + case SourceType::Invalid: + case SourceType::Folder: + case SourceType::Count: + return false; + case SourceType::LocalStorage: + return loadLocalStorage(_results, _ds); + } + return false; + } + + bool DB::loadFile(DataList& _results, const std::string& _file) + { + const auto size = synthLib::getFileSize(_file); + + // unlikely that a 4mb file has useful data for us, skip + if (!size || size >= static_cast<size_t>(4 * 1024 * 1024)) + return false; + + Data data; + if (!synthLib::readFile(data, _file) || data.empty()) + return false; + + return parseFileData(_results, data); + } + + bool DB::loadLocalStorage(DataList& _results, const DataSource& _ds) + { + const auto file = getLocalStorageFile(_ds); + + std::vector<uint8_t> data; + if (!synthLib::readFile(data, file.getFullPathName().toStdString())) + return false; + + synthLib::MidiToSysex::splitMultipleSysex(_results, data); + return !_results.empty(); + } + + bool DB::loadFolder(const DataSourceNodePtr& _folder) + { + assert(_folder->type == SourceType::Folder); + + std::vector<std::string> files; + synthLib::findFiles(files, _folder->name, {}, 0, 0); + + for (const auto& file : files) + { + const auto child = std::make_shared<DataSourceNode>(); + child->setParent(_folder); + child->name = file; + child->origin = DataSourceOrigin::Autogenerated; + + if(synthLib::isDirectory(file)) + child->type = SourceType::Folder; + else + child->type = SourceType::File; + + addDataSource(child); + } + + return !files.empty(); + } + + bool DB::parseFileData(DataList& _results, const Data& _data) + { + return synthLib::MidiToSysex::extractSysexFromData(_results, _data); + } + + void DB::startLoaderThread() + { + m_loader.start(); + + runOnLoaderThread([this] + { + loadJson(); + }); + } + + void DB::stopLoaderThread() + { + m_loader.destroy(); + } + + void DB::runOnLoaderThread(std::function<void()>&& _func) + { + m_loader.add([this, f = std::move(_func)] + { + f(); + + if(isLoading() && !m_loader.pending()) + { + runOnUiThread([this] + { + m_loading = false; + onLoadFinished(); + }); + } + }); + } + + void DB::runOnUiThread(const std::function<void()>& _func) + { + m_uiFuncs.push_back(_func); + } + + void DB::addDataSource(const DataSourceNodePtr& _ds) + { + if (m_loader.destroyed()) + return; + + auto ds = _ds; + + bool dsExists; + + { + std::unique_lock lockDs(m_dataSourcesMutex); + + const auto itExisting = m_dataSources.find(*ds); + + dsExists = itExisting != m_dataSources.end(); + + if (dsExists) + { + // two things can happen here: + // * a child DS already exists and the one being added has a parent that was previously unknown to the existing DS + // * a DS is added again (for example manually requested by a user) even though it already exists because of a parent DS added earlier + + ds = itExisting->second; + + if(_ds->origin == DataSourceOrigin::Manual) + { + // user manually added a DS that already exists as a child + assert(!_ds->hasParent()); + ds->origin = _ds->origin; + } + else if(_ds->hasParent() && !ds->hasParent()) + { + // a parent datasource is added and this DS previously didn't have a parent + ds->setParent(_ds->getParent()); + } + else + { + // nothing to be done + assert(_ds->getParent().get() == ds->getParent().get()); + return; + } + + std::unique_lock lockUi(m_uiMutex); + m_dirty.dataSources = true; + } + } + + auto addDsToList = [&] + { + if (dsExists) + return; + + std::unique_lock lockDs(m_dataSourcesMutex); + + m_dataSources.insert({ *ds, ds }); + std::unique_lock lockUi(m_uiMutex); + m_dirty.dataSources = true; + + dsExists = true; + }; + + if (ds->type == SourceType::Folder) + { + addDsToList(); + loadFolder(ds); + return; + } + + // always add DS if manually requested by user + if (ds->origin == DataSourceOrigin::Manual) + addDsToList(); + + std::vector<std::vector<uint8_t>> data; + + if(loadData(data, ds) && !data.empty()) + { + std::vector<PatchPtr> patches; + patches.reserve(data.size()); + + for (uint32_t p = 0; p < data.size(); ++p) + { + if (const auto patch = initializePatch(std::move(data[p]))) + { + patch->source = ds->weak_from_this(); + + if(isValid(patch)) + { + patch->program = p; + patches.push_back(patch); + ds->patches.insert(patch); + } + } + } + + if (!patches.empty()) + { + addDsToList(); + loadPatchModifications(ds, patches); + addPatches(patches); + } + } + } + + bool DB::addPatches(const std::vector<PatchPtr>& _patches) + { + if (_patches.empty()) + return false; + + std::unique_lock lock(m_patchesMutex); + + for (const auto& patch : _patches) + { + const auto key = PatchKey(*patch); + + // find modification and apply it to the patch + const auto itMod = m_patchModifications.find(key); + if (itMod != m_patchModifications.end()) + { + patch->modifications = itMod->second; + + m_patchModifications.erase(itMod); + + patch->modifications->patch = patch; + patch->modifications->updateCache(); + } + + // add to all known categories, tags, etc + for (const auto& it : patch->getTags().get()) + { + const auto type = it.first; + const auto& tags = it.second; + + for (const auto& tag : tags.getAdded()) + internalAddTag(type, tag); + } + } + + // add to ongoing searches + updateSearches(_patches); + + return true; + } + + bool DB::removePatch(const PatchPtr& _patch) + { + std::unique_lock lock(m_patchesMutex); + + const auto itDs = m_dataSources.find(*_patch->source.lock()); + + if(itDs == m_dataSources.end()) + return false; + + const auto& ds = itDs->second; + auto& patches = ds->patches; + + const auto it = patches.find(_patch); + if (it == patches.end()) + return false; + + auto mods = _patch->modifications; + + if(mods && !mods->empty()) + { + mods->patch.reset(); + m_patchModifications.insert({PatchKey(*_patch), mods}); + } + + patches.erase(it); + + removePatchesFromSearches({ _patch }); + + std::unique_lock lockUi(m_uiMutex); + m_dirty.patches = true; + + return true; + } + + bool DB::internalAddTag(TagType _type, const Tag& _tag) + { + const auto itType = m_tags.find(_type); + + if (itType == m_tags.end()) + { + m_tags.insert({ _type, {_tag} }); + + std::unique_lock lockUi(m_uiMutex); + m_dirty.tags.insert(_type); + return true; + } + + auto& tags = itType->second; + + if (tags.find(_tag) != tags.end()) + return false; + + tags.insert(_tag); + std::unique_lock lockUi(m_uiMutex); + m_dirty.tags.insert(_type); + + return true; + } + + bool DB::internalRemoveTag(const TagType _type, const Tag& _tag) + { + const auto& itType = m_tags.find(_type); + + if (itType == m_tags.end()) + return false; + + auto& tags = itType->second; + const auto itTag = tags.find(_tag); + + if (itTag == tags.end()) + return false; + + tags.erase(itTag); + + std::unique_lock lockUi(m_uiMutex); + m_dirty.tags.insert(_type); + + return true; + } + + bool DB::executeSearch(Search& _search) + { + _search.state = SearchState::Running; + + const auto reqPatch = _search.request.patch; + if(reqPatch) + { + // we're searching by patch content to find patches within datasources + SearchResult results; + + std::shared_lock lockDs(m_dataSourcesMutex); + + for (const auto& [_, ds] : m_dataSources) + { + for (const auto& patch : ds->patches) + { + if(patch->hash == reqPatch->hash) + results.insert(patch); + else if(patch->sysex.size() == reqPatch->sysex.size() && patch->getName() == reqPatch->getName()) + { + // if patches are not 100% identical, they might still be the same patch as unknown/unused data in dumps might have different values + if(equals(patch, reqPatch)) + results.insert(patch); + } + } + } + + if(!results.empty()) + { + std::unique_lock searchLock(_search.resultsMutex); + std::swap(_search.results, results); + } + + _search.setCompleted(); + + std::unique_lock lockUi(m_uiMutex); + m_dirty.searches.insert(_search.handle); + return true; + } + + auto searchInDs = [&](const DataSourceNodePtr& _ds) + { + if(!_search.request.sourceNode && _search.getSourceType() != SourceType::Invalid) + { + if(_ds->type != _search.request.sourceType) + return true; + } + + bool isCancelled; + { + std::shared_lock lockSearches(m_searchesMutex); + const auto it = m_cancelledSearches.find(_search.handle); + isCancelled = it != m_cancelledSearches.end(); + if(isCancelled) + m_cancelledSearches.erase(it); + } + + if(isCancelled) + { + _search.state = SearchState::Cancelled; + std::unique_lock lockUi(m_uiMutex); + m_dirty.searches.insert(_search.handle); + return false; + } + + for (const auto& patchPtr : _ds->patches) + { + const auto* patch = patchPtr.get(); + assert(patch); + + if(_search.request.match(*patch)) + { + std::unique_lock searchLock(_search.resultsMutex); + _search.results.insert(patchPtr); + } + } + return true; + }; + + if(_search.request.sourceNode && (_search.getSourceType() == SourceType::File || _search.getSourceType() == SourceType::LocalStorage)) + { + const auto& it = m_dataSources.find(*_search.request.sourceNode); + + if(it == m_dataSources.end()) + { + _search.setCompleted(); + return false; + } + + if(!searchInDs(it->second)) + return false; + } + else + { + for (const auto& it : m_dataSources) + { + if(!searchInDs(it.second)) + return false; + } + } + + _search.setCompleted(); + + { + std::unique_lock lockUi(m_uiMutex); + m_dirty.searches.insert(_search.handle); + } + + return true; + } + + void DB::updateSearches(const std::vector<PatchPtr>& _patches) + { + std::shared_lock lockSearches(m_searchesMutex); + + std::set<SearchHandle> dirtySearches; + + for (const auto& it : m_searches) + { + const auto handle = it.first; + auto& search = it.second; + + bool searchDirty = false; + + for (const auto& patch : _patches) + { + const auto match = search->request.match(*patch); + + bool countChanged; + + { + std::unique_lock lock(search->resultsMutex); + const auto oldCount = search->results.size(); + + if (match) + search->results.insert(patch); + else + search->results.erase(patch); + + const auto newCount = search->results.size(); + countChanged = newCount != oldCount; + } + + if (countChanged) + searchDirty = true; + } + if (searchDirty) + dirtySearches.insert(handle); + } + + if (dirtySearches.empty()) + return; + + std::unique_lock lockUi(m_uiMutex); + + for (SearchHandle dirtySearch : dirtySearches) + m_dirty.searches.insert(dirtySearch); + } + + bool DB::removePatchesFromSearches(const std::vector<PatchPtr>& _keys) + { + bool res = false; + + std::shared_lock lockSearches(m_searchesMutex); + + for (auto& itSearches : m_searches) + { + const auto& search = itSearches.second; + + bool countChanged; + { + std::unique_lock lockResults(search->resultsMutex); + const auto oldCount = search->results.size(); + + for (const auto& key : _keys) + search->results.erase(key); + + const auto newCount = search->results.size(); + countChanged = newCount != oldCount; + } + + if (countChanged) + { + res = true; + std::unique_lock lockUi(m_uiMutex); + m_dirty.searches.insert(itSearches.first); + } + } + return res; + } + + bool DB::createConsecutiveProgramNumbers(const DataSourceNodePtr& _ds) + { + std::unique_lock lockPatches(m_patchesMutex); + return _ds->createConsecutiveProgramNumbers(); + } + + Color DB::getTagColorInternal(const TagType _type, const Tag& _tag) const + { + const auto itType = m_tagColors.find(_type); + if(itType == m_tagColors.end()) + return 0; + const auto itTag = itType->second.find(_tag); + if(itTag == itType->second.end()) + return 0; + return itTag->second; + } + + bool DB::loadJson() + { + bool success = true; + + const auto json = juce::JSON::parse(m_jsonFileName); + const auto* datasources = json["datasources"].getArray(); + + if(datasources) + { + for(int i=0; i<datasources->size(); ++i) + { + const auto var = datasources->getUnchecked(i); + + DataSource ds; + + ds.type = toSourceType(var["type"].toString().toStdString()); + ds.name = var["name"].toString().toStdString(); + ds.origin = DataSourceOrigin::Manual; + + if (ds.type != SourceType::Invalid && !ds.name.empty()) + { + addDataSource(ds, false); + } + else + { + LOG("Unexpected data source type " << toString(ds.type) << " with name '" << ds.name << "'"); + success = false; + } + } + } + + { + std::unique_lock lockPatches(m_patchesMutex); + + if(auto* tags = json["tags"].getDynamicObject()) + { + const auto& props = tags->getProperties(); + for (const auto& it : props) + { + const auto strType = it.name.toString().toStdString(); + const auto type = toTagType(strType); + + const auto* tagsArray = it.value.getArray(); + if(tagsArray) + { + std::set<Tag> newTags; + for(int i=0; i<tagsArray->size(); ++i) + { + const auto tag = tagsArray->getUnchecked(i).toString().toStdString(); + newTags.insert(tag); + } + m_tags.insert({ type, newTags }); + m_dirty.tags.insert(type); + } + else + { + LOG("Unexpected empty tags for tag type " << strType); + success = false; + } + } + } + + if(auto* tagColors = json["tagColors"].getDynamicObject()) + { + const auto& props = tagColors->getProperties(); + + for (const auto& it : props) + { + const auto strType = it.name.toString().toStdString(); + const auto type = toTagType(strType); + + auto* colors = it.value.getDynamicObject(); + if(colors) + { + std::unordered_map<Tag, Color> newTags; + for (auto itCol : colors->getProperties()) + { + const auto tag = itCol.name.toString().toStdString(); + const auto col = static_cast<juce::int64>(itCol.value); + if(!tag.empty() && col != g_invalidColor && col >= std::numeric_limits<Color>::min() && col <= std::numeric_limits<Color>::max()) + newTags[tag] = static_cast<Color>(col); + } + m_tagColors[type] = newTags; + m_dirty.tags.insert(type); + } + else + { + LOG("Unexpected empty tags for tag type " << strType); + success = false; + } + } + } + + if(!loadPatchModifications(m_patchModifications, json)) + success = false; + } + + return success; + } + + bool DB::loadPatchModifications(const DataSourceNodePtr& _ds, const std::vector<PatchPtr>& _patches) + { + if(_patches.empty()) + return true; + + const auto file = getJsonFile(*_ds); + if(file.getFileName().isEmpty()) + return false; + + if(!file.exists()) + return true; + + const auto json = juce::JSON::parse(file); + + std::map<PatchKey, PatchModificationsPtr> patchModifications; + + if(!loadPatchModifications(patchModifications, json, _ds)) + return false; + + // apply modifications to patches + for (const auto& patch : _patches) + { + const auto key = PatchKey(*patch); + const auto it = patchModifications.find(key); + if(it != patchModifications.end()) + { + patch->modifications = it->second; + patch->modifications->patch = patch; + patch->modifications->updateCache(); + + patchModifications.erase(it); + + if(patchModifications.empty()) + break; + } + } + + if(!patchModifications.empty()) + { + // any patch modification that we couldn't apply to a patch is added to the global modifications + for (const auto& patchModification : patchModifications) + m_patchModifications.insert(patchModification); + } + + return true; + } + + bool DB::loadPatchModifications(std::map<PatchKey, PatchModificationsPtr>& _patchModifications, const juce::var& _parentNode, const DataSourceNodePtr& _dataSource/* = nullptr*/) + { + auto* patches = _parentNode["patches"].getDynamicObject(); + if(!patches) + return true; + + bool success = true; + + const auto& props = patches->getProperties(); + for (const auto& it : props) + { + const auto strKey = it.name.toString().toStdString(); + const auto var = it.value; + + auto key = PatchKey::fromString(strKey, _dataSource); + + auto mods = std::make_shared<PatchModifications>(); + + if (!mods->deserialize(var)) + { + LOG("Failed to parse patch modifications for key " << strKey); + success = false; + continue; + } + + if(!key.isValid()) + { + LOG("Failed to parse patch key from string " << strKey); + success = false; + } + + _patchModifications.insert({ key, mods }); + } + + return success; + } + + bool DB::saveJson() + { + if (!m_jsonFileName.hasWriteAccess()) + { + pushError("No write access to file:\n" + m_jsonFileName.getFullPathName().toStdString()); + return false; + } + + auto* json = new juce::DynamicObject(); + + { + std::shared_lock lockDs(m_dataSourcesMutex); + std::shared_lock lockP(m_patchesMutex); + + auto patchModifications = m_patchModifications; + + juce::Array<juce::var> dss; + + for (const auto& it : m_dataSources) + { + const auto& dataSource = it.second; + + // if we cannot save patch modifications to a separate file, add them to the global file + if(!saveJson(dataSource)) + { + for (const auto& patch : dataSource->patches) + { + if(!patch->modifications || patch->modifications->empty()) + continue; + + patchModifications.insert({PatchKey(*patch), patch->modifications}); + } + } + if (dataSource->origin != DataSourceOrigin::Manual) + continue; + + if (dataSource->type == SourceType::Rom) + continue; + + auto* o = new juce::DynamicObject(); + + o->setProperty("type", juce::String(toString(dataSource->type))); + o->setProperty("name", juce::String(dataSource->name)); + + dss.add(o); + } + json->setProperty("datasources", dss); + + saveLocalStorage(); + + auto* tagTypes = new juce::DynamicObject(); + + for (const auto& it : m_tags) + { + const auto type = it.first; + const auto& tags = it.second; + + if(tags.empty()) + continue; + + juce::Array<juce::var> tagsArray; + for (const auto& tag : tags) + tagsArray.add(juce::String(tag)); + + tagTypes->setProperty(juce::String(toString(type)), tagsArray); + } + + json->setProperty("tags", tagTypes); + + auto* tagColors = new juce::DynamicObject(); + + for (const auto& it : m_tagColors) + { + const auto type = it.first; + const auto& tags = it.second; + + if(tags.empty()) + continue; + + auto* colors = new juce::DynamicObject(); + for (const auto& [tag, col] : tags) + colors->setProperty(juce::String(tag), static_cast<juce::int64>(col)); + + tagColors->setProperty(juce::String(toString(type)), colors); + } + + json->setProperty("tagColors", tagColors); + + auto* patchMods = new juce::DynamicObject(); + + for (const auto& it : patchModifications) + { + const auto& key = it.first; + const auto& mods = it.second; + + if (mods->empty()) + continue; + + auto* obj = mods->serialize(); + + patchMods->setProperty(juce::String(key.toString()), obj); + } + + json->setProperty("patches", patchMods); + } + + return saveJson(m_jsonFileName, json); + } + + juce::File DB::getJsonFile(const DataSource& _ds) const + { + if(_ds.type == SourceType::LocalStorage) + return {getLocalStorageFile(_ds).getFullPathName() + ".json"}; + if(_ds.type == SourceType::File) + return {_ds.name + ".json"}; + return {}; + } + + bool DB::saveJson(const DataSourceNodePtr& _ds) + { + if(!_ds) + return false; + + auto filename = getJsonFile(*_ds); + + if(filename.getFileName().isEmpty()) + return _ds->patches.empty(); + + if(!juce::File::isAbsolutePath(filename.getFullPathName())) + filename = m_settingsDir.getChildFile(filename.getFullPathName()); + + if(!filename.hasWriteAccess()) + { + pushError("No write access to file:\n" + filename.getFullPathName().toStdString()); + return false; + } + + if(_ds->patches.empty()) + { + filename.deleteFile(); + return true; + } + + juce::DynamicObject* patchMods = nullptr; + + for (const auto& patch : _ds->patches) + { + const auto mods = patch->modifications; + + if(!mods || mods->empty()) + continue; + + auto* obj = mods->serialize(); + + if(!patchMods) + patchMods = new juce::DynamicObject(); + + const auto key = PatchKey(*patch); + + patchMods->setProperty(juce::String(key.toString(false)), obj); + } + + if(!patchMods) + { + filename.deleteFile(); + return true; + } + + auto* json = new juce::DynamicObject(); + + json->setProperty("patches", patchMods); + + return saveJson(filename, json); + } + + bool DB::saveJson(const juce::File& _target, juce::DynamicObject* _src) + { + if (!_target.hasWriteAccess()) + { + pushError("No write access to file:\n" + _target.getFullPathName().toStdString()); + return false; + } + const auto tempFile = juce::File(_target.getFullPathName() + "_tmp.json"); + if (!tempFile.hasWriteAccess()) + { + pushError("No write access to file:\n" + tempFile.getFullPathName().toStdString()); + return false; + } + const auto jsonText = juce::JSON::toString(juce::var(_src), false); + if (!tempFile.replaceWithText(jsonText)) + { + pushError("Failed to write data to file:\n" + tempFile.getFullPathName().toStdString()); + return false; + } + if (!tempFile.copyFileTo(_target)) + { + pushError("Failed to copy\n" + tempFile.getFullPathName().toStdString() + "\nto\n" + _target.getFullPathName().toStdString()); + return false; + } + tempFile.deleteFile(); + return true; + } + + juce::File DB::getLocalStorageFile(const DataSource& _ds) const + { + const auto filename = createValidFilename(_ds.name); + + return m_settingsDir.getChildFile(filename + ".syx"); + } + + bool DB::saveLocalStorage() const + { + std::map<DataSourceNodePtr, std::set<PatchPtr>> localStoragePatches; + + for (const auto& it : m_dataSources) + { + const auto& ds = it.second; + + if (ds->type == SourceType::LocalStorage) + localStoragePatches.insert({ds, ds->patches}); + } + + if (localStoragePatches.empty()) + return false; + + std::vector<PatchPtr> patchesVec; + patchesVec.reserve(128); + + bool res = true; + + for (const auto& it : localStoragePatches) + { + const auto& ds = it.first; + const auto& patches = it.second; + + const auto file = getLocalStorageFile(*ds); + + if(patches.empty()) + { + file.deleteFile(); + } + else + { + patchesVec.assign(patches.begin(), patches.end()); + DataSource::sortByProgram(patchesVec); + if(!writePatchesToFile(file, patchesVec)) + res = false; + } + } + return res; + } + + void DB::pushError(std::string _string) + { + std::unique_lock lockUi(m_uiMutex); + m_dirty.errors.emplace_back(std::move(_string)); + } +} diff --git a/source/jucePluginLib/patchdb/db.h b/source/jucePluginLib/patchdb/db.h @@ -0,0 +1,165 @@ +#pragma once + +#include <functional> +#include <map> +#include <list> +#include <thread> +#include <shared_mutex> + +#include "patch.h" +#include "patchdbtypes.h" +#include "search.h" + +#include "jobqueue.h" + +#include "juce_core/juce_core.h" + +namespace pluginLib::patchDB +{ + struct SearchRequest; + struct Patch; + struct DataSource; + + class DB + { + public: + DB(juce::File _dir); + virtual ~DB(); + + void uiProcess(Dirty& _dirty); + + DataSourceNodePtr addDataSource(const DataSource& _ds); + void removeDataSource(const DataSource& _ds, bool _save = true); + void refreshDataSource(const DataSourceNodePtr& _ds); + void renameDataSource(const DataSourceNodePtr& _ds, const std::string& _newName); + + void getDataSources(std::vector<DataSourceNodePtr>& _dataSources) + { + std::shared_lock lock(m_dataSourcesMutex); + _dataSources.reserve(m_dataSources.size()); + for (const auto& it : m_dataSources) + _dataSources.push_back(it.second); + } + + bool setTagColor(TagType _type, const Tag& _tag, Color _color); + Color getTagColor(TagType _type, const Tag& _tag) const; + Color getPatchColor(const PatchPtr& _patch, const TypedTags& _tagsToIgnore) const; + + bool addTag(TagType _type, const Tag& _tag); + bool removeTag(TagType _type, const Tag& _tag); + + void getTags(TagType _type, std::set<Tag>& _tags); + bool modifyTags(const std::vector<PatchPtr>& _patches, const TypedTags& _tags); + bool renamePatch(const PatchPtr& _patch, const std::string& _name); + bool replacePatch(const PatchPtr& _existing, const PatchPtr& _new); + + SearchHandle search(SearchRequest&& _request); + SearchHandle search(SearchRequest&& _request, SearchCallback&& _callback); + SearchHandle findDatasourceForPatch(const PatchPtr& _patch, SearchCallback&& _callback); + + void cancelSearch(uint32_t _handle); + std::shared_ptr<Search> getSearch(SearchHandle _handle); + std::shared_ptr<Search> getSearch(const DataSource& _dataSource); + + void copyPatchesTo(const DataSourceNodePtr& _ds, const std::vector<PatchPtr>& _patches, int _insertRow = -1); + void removePatches(const DataSourceNodePtr& _ds, const std::vector<PatchPtr>& _patches); + bool movePatchesTo(uint32_t _position, const std::vector<PatchPtr>& _patches); + + static bool isValid(const PatchPtr& _patch); + + PatchPtr requestPatchForPart(uint32_t _part); + virtual bool requestPatchForPart(Data& _data, uint32_t _part) = 0; + + bool isLoading() const { return m_loading; } + bool isScanning() const { return !m_loader.empty(); } + + static bool writePatchesToFile(const juce::File& _file, const std::vector<PatchPtr>& _patches); + + protected: + DataSourceNodePtr addDataSource(const DataSource& _ds, bool _save); + + public: + virtual bool loadData(DataList& _results, const DataSourceNodePtr& _ds); + virtual bool loadData(DataList& _results, const DataSource& _ds); + + virtual bool loadRomData(DataList& _results, uint32_t _bank, uint32_t _program) = 0; + virtual bool loadFile(DataList& _results, const std::string& _file); + virtual bool loadLocalStorage(DataList& _results, const DataSource& _ds); + virtual bool loadFolder(const DataSourceNodePtr& _folder); + virtual PatchPtr initializePatch(Data&& _sysex) = 0; + virtual Data prepareSave(const PatchPtr& _patch) const = 0; + virtual bool parseFileData(DataList& _results, const Data& _data); + virtual bool equals(const PatchPtr& _a, const PatchPtr& _b) const = 0; + + protected: + virtual void onLoadFinished() {} + + void startLoaderThread(); + void stopLoaderThread(); + + void runOnLoaderThread(std::function<void()>&& _func); + void runOnUiThread(const std::function<void()>& _func); + + private: + void addDataSource(const DataSourceNodePtr& _ds); + + bool addPatches(const std::vector<PatchPtr>& _patches); + bool removePatch(const PatchPtr& _patch); + + bool internalAddTag(TagType _type, const Tag& _tag); + bool internalRemoveTag(TagType _type, const Tag& _tag); + + bool executeSearch(Search& _search); + void updateSearches(const std::vector<PatchPtr>& _patches); + bool removePatchesFromSearches(const std::vector<PatchPtr>& _keys); + + bool createConsecutiveProgramNumbers(const DataSourceNodePtr& _ds); + + Color getTagColorInternal(TagType _type, const Tag& _tag) const; + + bool loadJson(); + bool loadPatchModifications(const DataSourceNodePtr& _ds, const std::vector<PatchPtr>& _patches); + static bool loadPatchModifications(std::map<PatchKey, PatchModificationsPtr>& _patchModifications, const juce::var& _parentNode, const DataSourceNodePtr& _dataSource = nullptr); + + bool saveJson(); + bool saveJson(const DataSourceNodePtr& _ds); + bool saveJson(const juce::File& _target, juce::DynamicObject* _src); + + juce::File getJsonFile(const DataSource& _ds) const; + juce::File getLocalStorageFile(const DataSource& _ds) const; + + bool saveLocalStorage() const; + + void pushError(std::string _string); + + // IO + juce::File m_settingsDir; + juce::File m_jsonFileName; + + // loader + JobQueue m_loader; + + // ui + std::mutex m_uiMutex; + std::list<std::function<void()>> m_uiFuncs; + Dirty m_dirty; + + // data + std::shared_mutex m_dataSourcesMutex; + std::map<DataSource, DataSourceNodePtr> m_dataSources; // we need a key to find duplicates, but at the same time we need pointers to do the parent relation + + mutable std::shared_mutex m_patchesMutex; + std::unordered_map<TagType, std::set<Tag>> m_tags; + std::unordered_map<TagType, std::unordered_map<Tag, uint32_t>> m_tagColors; + std::map<PatchKey, PatchModificationsPtr> m_patchModifications; + + // search + std::shared_mutex m_searchesMutex; + std::unordered_map<uint32_t, std::shared_ptr<Search>> m_searches; + std::unordered_set<SearchHandle> m_cancelledSearches; + uint32_t m_nextSearchHandle = 0; + + // state + bool m_loading = true; + }; +} diff --git a/source/jucePluginLib/patchdb/jobqueue.cpp b/source/jucePluginLib/patchdb/jobqueue.cpp @@ -0,0 +1,166 @@ +#include "jobqueue.h" + +#include <shared_mutex> + +#include "dsp56kEmu/threadtools.h" + +namespace pluginLib::patchDB +{ + JobQueue::JobQueue(std::string _name, const bool _start/* = true*/, const dsp56k::ThreadPriority& _prio/* = dsp56k::ThreadPriority::Normal*/, const uint32_t _threadCount/* = 1*/) + : m_name(std::move(_name)) + , m_threadPriority(_prio) + , m_threadCount(_threadCount) + { + if (_start) + start(); + } + + JobQueue::~JobQueue() + { + destroy(); + } + + void JobQueue::start() + { + if (!m_threads.empty()) + return; + + m_destroy = false; + + m_threads.reserve(m_threadCount); + + for(size_t i=0; i<m_threadCount; ++i) + { + size_t idx = i; + m_threads.emplace_back(new std::thread([this, idx] + { + if (!m_name.empty()) + dsp56k::ThreadTools::setCurrentThreadName(m_name + std::to_string(idx)); + dsp56k::ThreadTools::setCurrentThreadPriority(m_threadPriority); + threadFunc(); + })); + } + } + + void JobQueue::destroy() + { + if (m_destroy) + return; + + { + std::unique_lock lock(m_mutexFuncs); + m_destroy = true; + m_funcs.emplace_back([]{}); + } + m_cv.notify_all(); + + for (const auto& thread : m_threads) + thread->join(); + m_threads.clear(); + + m_funcs.clear(); + m_emptyCv.notify_all(); + } + + void JobQueue::add(std::function<void()>&& _func) + { + { + std::unique_lock lock(m_mutexFuncs); + m_funcs.emplace_back(std::move(_func)); + } + m_cv.notify_one(); + } + + size_t JobQueue::size() const + { + std::unique_lock lock(m_mutexFuncs); + return m_funcs.size() + m_numRunning; + } + + void JobQueue::waitEmpty() + { + std::unique_lock lock(m_mutexFuncs); + + m_emptyCv.wait(lock, [this] {return m_funcs.empty() && !m_numRunning; }); + } + + size_t JobQueue::pending() const + { + std::unique_lock lock(m_mutexFuncs); + return m_funcs.size(); + } + + void JobQueue::threadFunc() + { + while (!m_destroy) + { + std::unique_lock lock(m_mutexFuncs); + + m_cv.wait(lock, [this] {return !m_funcs.empty();}); + + const auto func = m_funcs.front(); + + if (m_destroy) + return; + + ++m_numRunning; + m_funcs.pop_front(); + + lock.unlock(); + func(); + lock.lock(); + + --m_numRunning; + + if (m_funcs.empty() && !m_numRunning) + m_emptyCv.notify_all(); + } + } + + JobGroup::JobGroup(JobQueue& _queue): m_queue(_queue) + { + } + + JobGroup::~JobGroup() + { + wait(); + } + + void JobGroup::add(std::function<void()>&& _func) + { + { + std::unique_lock lockCounts(m_mutexCounts); + ++m_countEnqueued; + } + + auto func = [this, f = std::move(_func)] + { + f(); + onFuncCompleted(); + }; + + m_queue.add(func); + } + + void JobGroup::wait() + { + std::unique_lock l(m_mutexCounts); + m_completedCv.wait(l, [this] + { + return m_countCompleted == m_countEnqueued; + } + ); + } + + void JobGroup::onFuncCompleted() + { + std::unique_lock lockCounts(m_mutexCounts); + ++m_countCompleted; + + if (m_countCompleted == m_countEnqueued) + { + lockCounts.unlock(); + m_completedCv.notify_one(); + } + } +} diff --git a/source/jucePluginLib/patchdb/jobqueue.h b/source/jucePluginLib/patchdb/jobqueue.h @@ -0,0 +1,99 @@ +#pragma once + +#include <deque> +#include <functional> +#include <mutex> +#include <condition_variable> +#include <thread> +#include <future> + +#include "dsp56kEmu/threadtools.h" + +namespace pluginLib::patchDB +{ + class JobGroup; + + class JobQueue final + { + public: + JobQueue(std::string _name = {}, bool _start = true, const dsp56k::ThreadPriority& _prio = dsp56k::ThreadPriority::Normal, uint32_t _threadCount = 1); + ~JobQueue(); + + JobQueue(JobQueue&&) = delete; + JobQueue(const JobQueue&) = delete; + JobQueue& operator = (const JobQueue&) = delete; + JobQueue& operator = (JobQueue&&) = delete; + + void start(); + void destroy(); + + void add(std::function<void()>&& _func); + bool destroyed() const { return m_destroy; } + + size_t size() const; + bool empty() const { return size() == 0; } + void waitEmpty(); + size_t pending() const; + + private: + void threadFunc(); + + std::string m_name; + dsp56k::ThreadPriority m_threadPriority; + uint32_t m_threadCount; + + std::deque<std::function<void()>> m_funcs; + mutable std::mutex m_mutexFuncs; + std::condition_variable m_cv; + + std::condition_variable m_emptyCv; + + bool m_destroy = false; + uint32_t m_numRunning = 0; + + std::vector<std::unique_ptr<std::thread>> m_threads; + }; + + class JobGroup final + { + public: + explicit JobGroup(JobQueue& _queue); + + JobGroup(JobGroup&&) = delete; + JobGroup(const JobGroup&) = delete; + JobGroup& operator = (const JobGroup&) = delete; + JobGroup& operator = (JobGroup&&) = delete; + + ~JobGroup(); + + void add(std::function<void()>&&); + + template<typename T> + void forEach(const T& _container, const std::function<void(const T&)>& _func, bool _wait = true) + { + for (const auto& e : _container) + { + add([&e, &_func] + { + _func(e); + }); + } + + if (_wait) + wait(); + } + + void wait(); + + private: + void onFuncCompleted(); + + JobQueue& m_queue; + + std::mutex m_mutexCounts; + uint32_t m_countEnqueued = 0; + uint32_t m_countCompleted = 0; + + std::condition_variable m_completedCv; + }; +} diff --git a/source/jucePluginLib/patchdb/patch.cpp b/source/jucePluginLib/patchdb/patch.cpp @@ -0,0 +1,133 @@ +#include "patch.h" + +#include <cassert> +#include <sstream> + +#include "patchmodifications.h" +#include "serialization.h" + +#include "juce_core/juce_core.h" + +namespace pluginLib::patchDB +{ + std::pair<PatchPtr, PatchModificationsPtr> Patch::createCopy(const DataSourceNodePtr& _ds) const + { + const auto patchDs = source.lock(); + if (patchDs && *_ds == *patchDs) + return {}; + + auto p = std::shared_ptr<Patch>(new Patch(*this)); + + p->source = _ds->weak_from_this(); + _ds->patches.insert(p); + + PatchModificationsPtr newMods; + + if(const auto mods = modifications) + newMods = std::make_shared<PatchModifications>(*mods); + + if(newMods) + { + p->modifications = newMods; + newMods->patch = p; + } + + return { p, newMods }; + } + + void Patch::replaceData(const Patch& _patch) + { + name = _patch.name; + tags = _patch.tags; + hash = _patch.hash; + sysex = _patch.sysex; + } + + const TypedTags& Patch::getTags() const + { + if (const auto m = modifications) + return m->mergedTags; + return tags; + } + + const Tags& Patch::getTags(const TagType _type) const + { + return getTags().get(_type); + } + + const std::string& Patch::getName() const + { + const auto m = modifications; + if (!m || m->name.empty()) + return name; + return m->name; + } + + std::string PatchKey::toString(const bool _includeDatasource) const + { + if(!isValid()) + return {}; + + std::stringstream ss; + + if (_includeDatasource && source->type != SourceType::Invalid) + ss << source->toString() << '|'; + + if (program != g_invalidProgram) + ss << "prog|" << program << '|'; + + ss << "hash|" << juce::String::toHexString(hash.data(), (int)hash.size(), 0); + + return ss.str(); + } + + PatchKey PatchKey::fromString(const std::string& _string, const DataSourceNodePtr& _dataSource/* = nullptr*/) + { + const std::vector<std::string> elems = Serialization::split(_string, '|'); + + if (elems.size() & 1) + return {}; + + PatchKey patchKey; + + if(_dataSource) + patchKey.source = _dataSource; + else + patchKey.source = std::make_shared<DataSourceNode>(); + + for(size_t i=0; i<elems.size(); i+=2) + { + const auto& key = elems[i]; + const auto& val = elems[i + 1]; + + if (key == "type") + patchKey.source->type = toSourceType(val); + else if (key == "name") + patchKey.source->name = val; + else if (key == "bank") + patchKey.source->bank = ::strtol(val.c_str(), nullptr, 10); + else if (key == "prog") + patchKey.program = ::strtol(val.c_str(), nullptr, 10); + else if (key == "hash") + { + juce::MemoryBlock mb; + mb.loadFromHexString(juce::String(val)); + if(mb.getSize() == std::size(patchKey.hash)) + { + memcpy(patchKey.hash.data(), mb.getData(), std::size(patchKey.hash)); + } + else + { + assert(false && "hash value has invalid length"); + patchKey.hash.fill(0); + } + } + else + { + assert(false && "unknown property key while parsing patch key"); + } + } + + return patchKey; + } +} diff --git a/source/jucePluginLib/patchdb/patch.h b/source/jucePluginLib/patchdb/patch.h @@ -0,0 +1,109 @@ +#pragma once + +#include <cstdint> +#include <string> +#include <vector> +#include <array> + +#include "datasource.h" +#include "tags.h" +#include "patchdbtypes.h" + +namespace pluginLib::patchDB +{ + using PatchHash = std::array<uint8_t, 16>; + + struct Patch + { + Patch() + { + hash.fill(0); + } + virtual ~Patch() = default; + + Patch& operator = (const Patch&) = delete; + Patch& operator = (Patch&&) = delete; + + std::pair<PatchPtr, PatchModificationsPtr> createCopy(const DataSourceNodePtr& _ds) const; + + void replaceData(const Patch& _patch); + + private: + Patch(const Patch&) = default; + Patch(Patch&&) noexcept = default; + + public: + std::string name; + + uint32_t bank = g_invalidBank; + uint32_t program = g_invalidProgram; + + std::weak_ptr<DataSourceNode> source; + + TypedTags tags; + + PatchHash hash; + std::vector<uint8_t> sysex; + + std::shared_ptr<PatchModifications> modifications; + + const TypedTags& getTags() const; + const Tags& getTags(TagType _type) const; + const std::string& getName() const; + }; + + struct PatchKey + { + DataSourceNodePtr source; + PatchHash hash{0}; + uint32_t program = g_invalidProgram; + + PatchKey() = default; + + explicit PatchKey(const Patch& _patch) : source(_patch.source), hash(_patch.hash), program(_patch.program) {} + + bool operator == (const PatchKey& _other) const + { + return equals(source, _other.source) && hash == _other.hash && program == _other.program; + } + + bool operator != (const PatchKey& _other) const + { + return !(*this == _other); + } + + bool operator < (const PatchKey& _other) const + { + if (program < _other.program) + return true; + if (program > _other.program) + return false; + if(!source && _other.source) + return true; + if(source && !_other.source) + return false; + if(source) + { + if (*source < *_other.source) + return true; + if (*source > *_other.source) + return false; + } + if (hash < _other.hash) + return true; + return false; + } + + bool isValid() const { return source && source->type != SourceType::Invalid; } + + static bool equals(const DataSourceNodePtr& _a, const DataSourceNodePtr& _b) + { + if(!_a) + return !_b; + return *_a == *_b; + } + + std::string toString(bool _includeDatasource = true) const; + static PatchKey fromString(const std::string& _string, const DataSourceNodePtr& _dataSource = nullptr); + }; +} diff --git a/source/jucePluginLib/patchdb/patchdbtypes.cpp b/source/jucePluginLib/patchdb/patchdbtypes.cpp @@ -0,0 +1,70 @@ +#include "patchdbtypes.h" + +namespace pluginLib::patchDB +{ + constexpr std::initializer_list<const char*> g_sourceTypes = + { + "invalid", + "rom", + "folder", + "file", + "localstorage" + }; + + static_assert(std::size(g_sourceTypes) == static_cast<uint32_t>(SourceType::Count)); + + constexpr std::initializer_list<const char*> g_tagTypes = + { + "invalid", + "category", + "tag", + "favourites", + "customA", + "customB", + "customC", + }; + + static_assert(std::size(g_tagTypes) == static_cast<uint32_t>(TagType::Count)); + + template<typename Tenum> + const char* toString(Tenum _type, const std::initializer_list<const char*>& _strings) + { + const auto i = static_cast<uint32_t>(_type); + if (i >= _strings.size()) + return ""; + return *(_strings.begin() + i); + } + + std::string toString(const SourceType _type) + { + return toString(_type, g_sourceTypes); + } + + std::string toString(const TagType _type) + { + return toString(_type, g_tagTypes); + } + + template<typename T> + T fromString(const std::string& _string, const std::initializer_list<const char*>& _strings) + { + size_t i = 0; + for (const auto& s : _strings) + { + if (s == _string) + return static_cast<T>(i); + ++i; + } + return T::Invalid; + } + + SourceType toSourceType(const std::string& _string) + { + return fromString<SourceType>(_string, g_sourceTypes); + } + + TagType toTagType(const std::string& _string) + { + return fromString<TagType>(_string, g_tagTypes); + } +} diff --git a/source/jucePluginLib/patchdb/patchdbtypes.h b/source/jucePluginLib/patchdb/patchdbtypes.h @@ -0,0 +1,82 @@ +#pragma once + +#include <memory> +#include <set> +#include <string> +#include <vector> +#include <chrono> + +namespace pluginLib::patchDB +{ + enum class SourceType + { + Invalid, + Rom, + Folder, + File, + LocalStorage, + Count + }; + + enum class DataSourceOrigin + { + Invalid, + Manual, // manually added datasource by user + Autogenerated, // automatically added child as part of a folder being added + }; + + using Tag = std::string; + + enum class TagType + { + Invalid, + Category, + Tag, + Favourites, + CustomA, + CustomB, + CustomC, + + Count + }; + + struct Patch; + using PatchPtr = std::shared_ptr<Patch>; + + struct PatchModifications; + using PatchModificationsPtr = std::shared_ptr<PatchModifications>; + + using Data = std::vector<uint8_t>; + using DataList = std::vector<Data>; + + using SearchHandle = uint32_t; + + static constexpr SearchHandle g_invalidSearchHandle = ~0; + static constexpr uint32_t g_invalidBank = ~0; + static constexpr uint32_t g_invalidProgram = ~0; + + struct Dirty + { + bool dataSources = false; + bool patches = false; + + std::set<TagType> tags; + std::set<SearchHandle> searches; + std::vector<std::string> errors; + }; + + struct DataSource; + struct DataSourceNode; + using DataSourceNodePtr = std::shared_ptr<DataSourceNode>; + + std::string toString(SourceType _type); + std::string toString(TagType _type); + + SourceType toSourceType(const std::string& _string); + TagType toTagType(const std::string& _string); + + using Timestamp = std::chrono::time_point<std::chrono::system_clock>; + + using Color = uint32_t; + static constexpr uint32_t g_invalidColor = 0; +} diff --git a/source/jucePluginLib/patchdb/patchhistory.cpp b/source/jucePluginLib/patchdb/patchhistory.cpp diff --git a/source/jucePluginLib/patchdb/patchhistory.h b/source/jucePluginLib/patchdb/patchhistory.h @@ -0,0 +1,13 @@ +#pragma once + +#include <vector> + +#include "patchdbtypes.h" + +namespace pluginLib::patchDB +{ + class PatchHistory + { + std::vector<PatchPtr> m_patches; + }; +} diff --git a/source/jucePluginLib/patchdb/patchmodifications.cpp b/source/jucePluginLib/patchdb/patchmodifications.cpp @@ -0,0 +1,109 @@ +#include "patchmodifications.h" + +#include "patch.h" + +#include <juce_audio_processors/juce_audio_processors.h> + +namespace pluginLib::patchDB +{ + bool PatchModifications::modifyTags(const TypedTags& _tags) + { + bool res = false; + + for (const auto& it : _tags.get()) + { + const auto type = it.first; + const auto& t = it.second; + + const auto p = patch.lock(); + + for (const auto& tag : t.getAdded()) + { + if (!p->tags.containsAdded(type, tag)) + res |= tags.add(type, tag); + else if(tags.containsRemoved(type, tag)) + res |= tags.erase(type, tag); + } + + for (const auto& tag : t.getRemoved()) + { + if (p->tags.containsAdded(type, tag)) + res |= tags.addRemoved(type, tag); + else if (tags.containsAdded(type, tag)) + res |= tags.erase(type, tag); + } + } + + if (!res) + return false; + + updateCache(); + return true; + } + + void PatchModifications::updateCache() + { + const auto p = patch.lock(); + if (!p) + { + mergedTags = tags; + return; + } + + mergedTags = p->tags; + + for (const auto& it : tags.get()) + { + const auto& type = it.first; + const auto & t = it.second; + + for (const auto& tag: t.getAdded()) + mergedTags.add(type, tag); + + for (const auto& tag : t.getRemoved()) + mergedTags.addRemoved(type, tag); + } + } + + juce::DynamicObject* PatchModifications::serialize() const + { + auto* o = new juce::DynamicObject(); + + auto* doTags = tags.serialize(); + + o->setProperty("tags", doTags); + + if(!name.empty()) + { + const auto p = patch.lock(); + if (!p || name != p->name) + o->setProperty("name", juce::String(name)); + } + + return o; + } + + bool PatchModifications::deserialize(const juce::var& _var) + { + name.clear(); + tags.clear(); + + const auto n = _var["name"].toString(); + if (!n.isEmpty()) + name = n.toStdString(); + + auto* t = _var["tags"].getDynamicObject(); + + if(t) + { + tags.deserialize(t); + } + + return true; + } + + bool PatchModifications::empty() const + { + return name.empty() && tags.empty(); + } +} diff --git a/source/jucePluginLib/patchdb/patchmodifications.h b/source/jucePluginLib/patchdb/patchmodifications.h @@ -0,0 +1,30 @@ +#pragma once + +#include "tags.h" + +namespace juce +{ + class var; + class DynamicObject; +} + +namespace pluginLib::patchDB +{ + struct PatchModifications + { + bool modifyTags(const TypedTags& _tags); + void updateCache(); + + juce::DynamicObject* serialize() const; + bool deserialize(const juce::var& _var); + + bool empty() const; + + std::weak_ptr<Patch> patch; + TypedTags tags; + std::string name; + + // cache + TypedTags mergedTags; + }; +} diff --git a/source/jucePluginLib/patchdb/search.cpp b/source/jucePluginLib/patchdb/search.cpp @@ -0,0 +1,139 @@ +#include "search.h" + +#include "patch.h" + +namespace pluginLib::patchDB +{ + namespace + { + std::string lowercase(const std::string& _src) + { + std::string str(_src); + for (char& i : str) + i = static_cast<char>(tolower(i)); + return str; + } + + bool matchStringsIgnoreCase(const std::string& _test, const std::string& _search) + { + if (_search.empty()) + return true; + + const auto t = lowercase(_test); + return t.find(_search) != std::string::npos; + } + /* + bool matchStrings(const std::string& _test, const std::string& _search) + { + if (_search.empty()) + return true; + + return _test.find(_search) != std::string::npos; + } + */ + bool testTags(const Tags& _tags, const Tags& _search) + { + for (const auto& t : _search.getAdded()) + { + if (!_tags.containsAdded(t)) + return false; + } + + for (const auto& t : _search.getRemoved()) + { + if (_tags.containsAdded(t)) + return false; + } + return true; + } + /* + bool matchDataSource(const DataSourceNode& _source, const DataSource& _search) + { + if (_source.hasParent() && matchDataSource(*_source.getParent(), _search)) + return true; + + if (_search.type != SourceType::Invalid && _source.type != _search.type) + return false; + + if (_search.bank != g_invalidBank && _source.bank != _search.bank) + return false; + + if (!matchStrings(_source.name, _search.name)) + return false; + + return true; + } + */ + bool matchDataSource(const DataSourceNode* _source, const DataSourceNodePtr& _search) + { + if (_source == _search.get()) + return true; + + if (const auto& parent = _source->getParent()) + return matchDataSource(parent.get(), _search); + + return false; + } + } + + bool SearchRequest::match(const Patch& _patch) const + { + // datasource + + const auto patchSource = _patch.source.lock(); + + if(sourceNode) + { + if (!matchDataSource(patchSource.get(), sourceNode)) + return false; + } + else if(sourceType != SourceType::Invalid) + { + if(patchSource->type != sourceType) + return false; + } + + // name + if (!matchStringsIgnoreCase(_patch.getName(), name)) + return false; + +// if (program != g_invalidProgram && _patch.program != program) +// return false; + + // tags, categories, ... + for (const auto& it : tags.get()) + { + const auto type = it.first; + const auto& t = it.second; + + const auto& patchTags = _patch.getTags().get(type); + + if (!testTags(patchTags, t)) + return false; + } + + for (const auto& tagOfType : anyTagOfType) + { + if(_patch.getTags(tagOfType).empty()) + return false; + } + + for (const auto& tagOfType : noTagOfType) + { + if(!_patch.getTags(tagOfType).empty()) + return false; + } + + return true; + } + + bool SearchRequest::isValid() const + { + return !name.empty() || !tags.empty() || sourceNode || patch; + } + + bool SearchRequest::operator==(const SearchRequest& _r) const + { + return name == _r.name && tags == _r.tags && sourceNode == _r.sourceNode && patch == _r.patch && sourceType == _r.sourceType; + } +} diff --git a/source/jucePluginLib/patchdb/search.h b/source/jucePluginLib/patchdb/search.h @@ -0,0 +1,78 @@ +#pragma once + +#include <functional> +#include <string> +#include <shared_mutex> + +#include "datasource.h" +#include "tags.h" + +namespace pluginLib::patchDB +{ + struct Search; + + struct SearchRequest + { + std::string name; + TypedTags tags; + DataSourceNodePtr sourceNode; + PatchPtr patch; // used by the UI to restore selection of a patch, the data source of this request patch will be null, the result will tell the UI which datasource it is in + SourceType sourceType = SourceType::Invalid; + std::set<TagType> anyTagOfType; + std::set<TagType> noTagOfType; + + bool match(const Patch& _patch) const; + bool isValid() const; + bool operator == (const SearchRequest& _r) const; + }; + + using SearchResult = std::set<PatchPtr>; + using SearchCallback = std::function<void(const Search&)>; + + enum class SearchState + { + NotStarted, + Running, + Cancelled, + Completed + }; + + struct Search + { + SearchHandle handle = g_invalidSearchHandle; + + SearchRequest request; + + SearchCallback callback; + + SearchResult results; + + mutable std::shared_mutex resultsMutex; + + SearchState state = SearchState::NotStarted; + + size_t getResultSize() const + { + std::shared_lock searchLock(resultsMutex); + return results.size(); + } + + SourceType getSourceType() const + { + if(request.sourceNode) + return request.sourceNode->type; + return request.sourceType; + } + + void setCompleted() + { + state = SearchState::Completed; + + if(!callback) + return; + + std::shared_lock searchLock(resultsMutex); + callback(*this); + } + }; +} diff --git a/source/jucePluginLib/patchdb/serialization.cpp b/source/jucePluginLib/patchdb/serialization.cpp @@ -0,0 +1,24 @@ +#include "serialization.h" + +namespace pluginLib::patchDB +{ + std::vector<std::string> Serialization::split(const std::string& _string, const char _delim) + { + std::vector<std::string> elems; + + size_t off = 0; + + while (off < _string.size()) + { + auto idx = _string.find(_delim, off); + + if (idx == std::string::npos) + idx = _string.size(); + + elems.push_back(_string.substr(off, idx - off)); + off = idx + 1; + } + + return elems; + } +} diff --git a/source/jucePluginLib/patchdb/serialization.h b/source/jucePluginLib/patchdb/serialization.h @@ -0,0 +1,13 @@ +#pragma once + +#include <string> +#include <vector> + +namespace pluginLib::patchDB +{ + class Serialization + { + public: + static std::vector<std::string> split(const std::string& _string, char _delim); + }; +} diff --git a/source/jucePluginLib/patchdb/tags.cpp b/source/jucePluginLib/patchdb/tags.cpp @@ -0,0 +1,213 @@ +#include "tags.h" + +#include "juce_core/juce_core.h" + +namespace pluginLib::patchDB +{ + bool Tags::operator==(const Tags& _t) const + { + if(m_added.size() != _t.m_added.size()) + return false; + if(m_removed.size() != _t.m_removed.size()) + return false; + + for (auto e : m_added) + { + if(_t.m_added.find(e) == _t.m_added.end()) + return false; + } + + for (auto e : m_removed) + { + if(_t.m_removed.find(e) == _t.m_removed.end()) + return false; + } + return true; + } + + const Tags& TypedTags::get(const TagType _type) const + { + const auto& it = m_tags.find(_type); + if (it != m_tags.end()) + return it->second; + static Tags empty; + return empty; + } + + bool TypedTags::add(const TagType _type, const Tag& _tag) + { + const auto it = m_tags.find(_type); + + if(it == m_tags.end()) + { + Tags t; + t.add(_tag); + m_tags.insert({ _type, t}); + return true; + } + + return it->second.add(_tag); + } + + bool TypedTags::add(const TypedTags& _tags) + { + bool result = false; + + for (const auto& tags : _tags.get()) + { + for (const auto& tag : tags.second.getAdded()) + result |= add(tags.first, tag); + } + + return result; + } + + bool TypedTags::addRemoved(const TagType _type, const Tag& _tag) + { + const auto it = m_tags.find(_type); + + if (it == m_tags.end()) + { + Tags t; + t.addRemoved(_tag); + m_tags.insert({ _type, t }); + return true; + } + + return it->second.addRemoved(_tag); + } + + bool TypedTags::erase(const TagType _type, const Tag& _tag) + { + const auto it = m_tags.find(_type); + + if (it == m_tags.end()) + return false; + + if (!it->second.erase(_tag)) + return false; + + if (it->second.empty()) + m_tags.erase(_type); + + return true; + } + + bool TypedTags::containsAdded(const TagType _type, const Tag& _tag) const + { + const auto itType = m_tags.find(_type); + if (itType == m_tags.end()) + return false; + return itType->second.containsAdded(_tag); + } + + bool TypedTags::containsRemoved(const TagType _type, const Tag& _tag) const + { + const auto itType = m_tags.find(_type); + if (itType == m_tags.end()) + return false; + return itType->second.containsRemoved(_tag); + } + + void TypedTags::clear() + { + m_tags.clear(); + } + + bool TypedTags::empty() const + { + for (const auto& tag : m_tags) + { + if (!tag.second.empty()) + return false; + } + return true; + } + + juce::DynamicObject* TypedTags::serialize() const + { + auto* doTags = new juce::DynamicObject(); + + for (const auto& it : get()) + { + const auto& key = it.first; + const auto& tags = it.second; + + auto* doType = new juce::DynamicObject(); + + const auto& added = tags.getAdded(); + const auto& removed = tags.getRemoved(); + + if (!added.empty()) + { + juce::Array<juce::var> doAdded; + for (const auto& tag : added) + doAdded.add(juce::String(tag)); + doType->setProperty("added", doAdded); + } + + if (!removed.empty()) + { + juce::Array<juce::var> doRemoved; + for (const auto& tag : removed) + doRemoved.add(juce::String(tag)); + doType->setProperty("removed", doRemoved); + } + + doTags->setProperty(juce::String(toString(key)), doType); + } + + return doTags; + } + + void TypedTags::deserialize(juce::DynamicObject* _obj) + { + for (const auto& prop : _obj->getProperties()) + { + const auto type = toTagType(prop.name.toString().toStdString()); + + if (type == TagType::Invalid) + continue; + + const auto* added = prop.value["added"].getArray(); + const auto* removed = prop.value["removed"].getArray(); + + if (added) + { + for (const auto& var : *added) + { + const auto& tag = var.toString().toStdString(); + if (!tag.empty()) + add(type, tag); + } + } + + if (removed) + { + for (const auto& var : *removed) + { + const auto& tag = var.toString().toStdString(); + if (!tag.empty()) + addRemoved(type, tag); + } + } + } + } + + bool TypedTags::operator==(const TypedTags& _tags) const + { + if(m_tags.size() != _tags.m_tags.size()) + return false; + + for (const auto& tags : m_tags) + { + const auto it = _tags.m_tags.find(tags.first); + if(it == _tags.m_tags.end()) + return false; + + if(!(it->second == tags.second)) + return false; + } + return true; + } +} diff --git a/source/jucePluginLib/patchdb/tags.h b/source/jucePluginLib/patchdb/tags.h @@ -0,0 +1,96 @@ +#pragma once + +#include <string> +#include <unordered_map> +#include <unordered_set> + +#include "patchdbtypes.h" + +namespace juce +{ + class DynamicObject; +} + +namespace pluginLib::patchDB +{ + class Tags + { + public: + bool add(const Tag& _tag) + { + const auto cA = m_added.size(); + const auto cR = m_removed.size(); + + m_added.insert(_tag); + m_removed.erase(_tag); + + return cA != m_added.size() || cR != m_removed.size(); + } + + bool erase(const Tag& _tag) + { + const auto cA = m_added.size(); + const auto cR = m_removed.size(); + + m_added.erase(_tag); + m_removed.erase(_tag); + + return cA != m_added.size() || cR != m_removed.size(); + } + + bool addRemoved(const Tag& _tag) + { + const auto cA = m_added.size(); + const auto cR = m_removed.size(); + + m_added.erase(_tag); + m_removed.insert(_tag); + + return cA != m_added.size() || cR != m_removed.size(); + } + + const auto& getAdded() const { return m_added; } + const auto& getRemoved() const { return m_removed; } + + bool containsAdded(const Tag& _tag) const + { + return m_added.find(_tag) != m_added.end(); + } + + bool containsRemoved(const Tag& _tag) const + { + return m_removed.find(_tag) != m_removed.end(); + } + + bool empty() const + { + return m_added.empty() && m_removed.empty(); + } + + bool operator == (const Tags& _t) const; + + private: + std::unordered_set<Tag> m_added; + std::unordered_set<Tag> m_removed; + }; + + class TypedTags + { + public: + const Tags& get(TagType _type) const; + const auto& get() const { return m_tags; } + bool add(TagType _type, const Tag& _tag); + bool add(const TypedTags& _tags); + bool erase(TagType _type, const Tag& _tag); + bool addRemoved(TagType _type, const Tag& _tag); + bool containsAdded(TagType _type, const Tag& _tag) const; + bool containsRemoved(TagType _type, const Tag& _tag) const; + void clear(); + bool empty() const; + juce::DynamicObject* serialize() const; + void deserialize(juce::DynamicObject* _obj); + bool operator == (const TypedTags& _tags) const; + private: + std::unordered_map<TagType, Tags> m_tags; + }; +} diff --git a/source/jucePluginLib/processor.cpp b/source/jucePluginLib/processor.cpp @@ -1,9 +1,11 @@ #include "processor.h" #include "dummydevice.h" +#include "types.h" #include "../synthLib/deviceException.h" #include "../synthLib/os.h" #include "../synthLib/binarystream.h" +#include "dsp56kEmu/fastmath.h" #include "dsp56kEmu/logging.h" @@ -17,8 +19,6 @@ namespace pluginLib constexpr char g_saveMagic[] = "DSP56300"; constexpr uint32_t g_saveVersion = 1; - using PluginStream = synthLib::BinaryStream<uint32_t>; - Processor::Processor(const BusesProperties& _busesProperties) : juce::AudioProcessor(_busesProperties) { } @@ -175,6 +175,8 @@ namespace pluginLib m_device.reset(new DummyDevice()); } + m_device->setDspClockPercent(m_dspClockPercent); + m_plugin.reset(new synthLib::Plugin(m_device.get())); return *m_plugin; @@ -188,6 +190,87 @@ namespace pluginLib return true; } + void Processor::saveCustomData(std::vector<uint8_t>& _targetBuffer) + { + synthLib::BinaryStream s; + + { + synthLib::ChunkWriter cw(s, "GAIN", 1); + s.write<uint32_t>(1); // version + s.write(m_inputGain); + s.write(m_outputGain); + } + + if(m_dspClockPercent != 100) + { + synthLib::ChunkWriter cw(s, "DSPC", 1); + s.write(m_dspClockPercent); + } + + s.toVector(_targetBuffer, true); + } + + void Processor::loadCustomData(const std::vector<uint8_t>& _sourceBuffer) + { + auto readGain = [this](synthLib::BinaryStream& _s) + { + const auto version = _s.read<uint32_t>(); + if (version != 1) + return; + m_inputGain = _s.read<float>(); + m_outputGain = _s.read<float>(); + }; + + // In Vavra, the only data we had was the gain parameters + if(_sourceBuffer.size() == sizeof(float) * 2 + sizeof(uint32_t)) + { + synthLib::BinaryStream ss(_sourceBuffer); + readGain(ss); + return; + } + + synthLib::BinaryStream s(_sourceBuffer); + synthLib::ChunkReader cr(s); + + cr.add("GAIN", 1, [readGain](synthLib::BinaryStream& _binaryStream, uint32_t _version) + { + readGain(_binaryStream); + }); + + cr.add("DSPC", 1, [this](synthLib::BinaryStream& _binaryStream, uint32_t _version) + { + auto p = _binaryStream.read<uint32_t>(); + p = dsp56k::clamp<uint32_t>(p, 50, 200); + setDspClockPercent(p); + }); + + cr.tryRead(); + } + + bool Processor::setDspClockPercent(const uint32_t _percent) + { + if(!m_device) + return false; + if(!m_device->setDspClockPercent(_percent)) + return false; + m_dspClockPercent = _percent; + return true; + } + + uint32_t Processor::getDspClockPercent() const + { + if(!m_device) + return m_dspClockPercent; + return m_device->getDspClockPercent(); + } + + uint64_t Processor::getDspClockHz() const + { + if(!m_device) + return 0; + return m_device->getDspClockHz(); + } + //============================================================================== void Processor::prepareToPlay(double sampleRate, int samplesPerBlock) { @@ -211,7 +294,7 @@ namespace pluginLib // 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. - +#if !SYNTHLIB_DEMO_MODE std::vector<uint8_t> buffer; getPlugin().getState(buffer, synthLib::StateTypeGlobal); @@ -227,27 +310,35 @@ namespace pluginLib ss.toVector(buf); destData.append(buf.data(), buf.size()); +#endif } void Processor::setStateInformation (const void* _data, const int _sizeInBytes) { - // You should use this method to restore your parameters from this memory block, +#if !SYNTHLIB_DEMO_MODE + // 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); +#endif } void Processor::getCurrentProgramStateInformation(juce::MemoryBlock& destData) { +#if !SYNTHLIB_DEMO_MODE std::vector<uint8_t> state; getPlugin().getState(state, synthLib::StateTypeCurrentProgram); destData.append(state.data(), state.size()); +#endif } void Processor::setCurrentProgramStateInformation(const void* data, int sizeInBytes) { +#if !SYNTHLIB_DEMO_MODE setState(data, sizeInBytes); +#endif } +#if !SYNTHLIB_DEMO_MODE void Processor::setState(const void* _data, const size_t _sizeInBytes) { if(_sizeInBytes < 1) @@ -278,12 +369,15 @@ namespace pluginLib getPlugin().setState(buffer); ss.read(buffer); - try - { - loadCustomData(buffer); - } - catch (std::range_error&) + if(!buffer.empty()) { + try + { + loadCustomData(buffer); + } + catch (std::range_error&) + { + } } } catch (std::range_error& e) @@ -300,6 +394,7 @@ namespace pluginLib if (hasController()) getController().onStateLoaded(); } +#endif //============================================================================== diff --git a/source/jucePluginLib/processor.h b/source/jucePluginLib/processor.h @@ -46,8 +46,38 @@ namespace pluginLib virtual bool setLatencyBlocks(uint32_t _blocks); virtual void updateLatencySamples() = 0; - virtual void saveCustomData(std::vector<uint8_t>& _targetBuffer) {} - virtual void loadCustomData(const std::vector<uint8_t>& _sourceBuffer) {} + virtual void saveCustomData(std::vector<uint8_t>& _targetBuffer); + virtual void loadCustomData(const std::vector<uint8_t>& _sourceBuffer); + + template<size_t N> void applyOutputGain(std::array<float*, N>& _buffers, const size_t _numSamples) + { + applyGain(_buffers, _numSamples, getOutputGain()); + } + + template<size_t N> static void applyGain(std::array<float*, N>& _buffers, const size_t _numSamples, const float _gain) + { + if(_gain == 1.0f) + return; + + if(!_numSamples) + return; + + for (float* buf : _buffers) + { + if (buf) + { + for (int i = 0; i < _numSamples; ++i) + buf[i] *= _gain; + } + } + } + + float getOutputGain() const { return m_outputGain; } + void setOutputGain(const float _gain) { m_outputGain = _gain; } + + bool setDspClockPercent(uint32_t _percent = 100); + uint32_t getDspClockPercent() const; + uint64_t getDspClockHz() const; private: void prepareToPlay(double sampleRate, int maximumExpectedSamplesPerBlock) override; @@ -59,7 +89,9 @@ namespace pluginLib void getCurrentProgramStateInformation (juce::MemoryBlock& destData) override; void setCurrentProgramStateInformation (const void* data, int sizeInBytes) override; +#if !SYNTHLIB_DEMO_MODE void setState(const void *_data, size_t _sizeInBytes); +#endif //============================================================================== int getNumPrograms() override; @@ -84,5 +116,10 @@ namespace pluginLib std::unique_ptr<juce::MidiOutput> m_midiOutput{}; std::unique_ptr<juce::MidiInput> m_midiInput{}; std::vector<synthLib::SMidiEvent> m_midiOut{}; + + private: + float m_outputGain = 1.0f; + float m_inputGain = 1.0f; + uint32_t m_dspClockPercent = 100; }; } diff --git a/source/jucePluginLib/types.h b/source/jucePluginLib/types.h @@ -0,0 +1,8 @@ +#pragma once + +#include "../synthLib/binarystream.h" + +namespace pluginLib +{ + using PluginStream = synthLib::BinaryStream; +} diff --git a/source/juceUiLib/CMakeLists.txt b/source/juceUiLib/CMakeLists.txt @@ -2,16 +2,21 @@ cmake_minimum_required(VERSION 3.15) project(juceUiLib VERSION ${CMAKE_PROJECT_VERSION}) set(SOURCES - editor.cpp editor.h - editorInterface.h + button.cpp button.h condition.cpp condition.h controllerlink.cpp controllerlink.h + editor.cpp editor.h + editorInterface.h image.cpp image.h + listBoxStyle.cpp listBoxStyle.h rotaryStyle.cpp rotaryStyle.h comboboxStyle.cpp comboboxStyle.h buttonStyle.cpp buttonStyle.h labelStyle.cpp labelStyle.h + scrollbarStyle.cpp scrollbarStyle.h textbuttonStyle.cpp textbuttonStyle.h + textEditorStyle.cpp textEditorStyle.h + treeViewStyle.cpp treeViewStyle.h hyperlinkbuttonStyle.cpp hyperlinkbuttonStyle.h tabgroup.cpp tabgroup.h uiObject.cpp uiObject.h diff --git a/source/juceUiLib/button.cpp b/source/juceUiLib/button.cpp diff --git a/source/juceUiLib/button.h b/source/juceUiLib/button.h @@ -0,0 +1,72 @@ +#pragma once + +#include "juce_gui_basics/juce_gui_basics.h" + +namespace genericUI +{ + // For whatever reason JUCE allows button clicks with the right mouse button. + // https://forum.juce.com/t/fr-buttons-add-option-to-ignore-clicks-with-right-mouse-button/20723/11 + // And for whatever reason jules thinks this is a brilliant idea, although every other UI does not work like that + // https://forum.juce.com/t/get-rid-of-right-click-clicking/23287/5 + + template<typename T> + class Button : public T + { + public: + using T::T; + using Callback = std::function<bool(const juce::MouseEvent&)>; + + virtual void mouseDown(const juce::MouseEvent& _e) override + { + if(onDown && onDown(_e)) + return; + + if (!allowRightClick() && _e.mods.isPopupMenu()) + return; + + T::mouseDown(_e); + } + + virtual void mouseDrag(const juce::MouseEvent& _e) override + { + if (!allowRightClick() && _e.mods.isPopupMenu()) + return; + + T::mouseDrag(_e); + } + + virtual void mouseUp(const juce::MouseEvent& _e) override + { + if(onUp && onUp(_e)) + return; + + if (!allowRightClick() && _e.mods.isPopupMenu()) + return; + + T::mouseUp(_e); + } + + void focusGained (juce::Component::FocusChangeType _type) override + { + if(!allowRightClick() && _type == juce::Component::focusChangedByMouseClick && juce::ModifierKeys::currentModifiers.isPopupMenu()) + return; + T::focusGained(_type); + } + + void allowRightClick(const bool _allow) + { + m_allowRightClick = _allow; + } + + bool allowRightClick() const + { + return m_allowRightClick; + } + + Callback onDown; + Callback onUp; + + private: + bool m_allowRightClick = false; + }; +} +\ No newline at end of file diff --git a/source/juceUiLib/editor.cpp b/source/juceUiLib/editor.cpp @@ -58,6 +58,7 @@ namespace genericUI m_rootObject->createJuceTree(*this); m_rootObject->createTabGroups(*this); m_rootObject->createControllerLinks(*this); + m_rootObject->registerTemplates(*this); m_scale = m_rootObject->getPropertyFloat("scale", 1.0f); } @@ -213,6 +214,19 @@ namespace genericUI return m_rootObject ? m_rootObject->getControllerLinkCountRecursive() : 0; } + void Editor::registerTemplate(const std::shared_ptr<UiObject>& _value) + { + const auto name = _value->getName(); + + if (name.empty()) + throw std::runtime_error("Every template needs to have a name"); + + if (m_templates.find(name) != m_templates.end()) + throw std::runtime_error("Template with name '" + name + "' exists more than once"); + + m_templates.insert({ name, _value }); + } + void Editor::setEnabled(juce::Component& _component, const bool _enable) { if(_component.getProperties().contains("disabledAlpha")) @@ -232,4 +246,12 @@ namespace genericUI { m_rootObject->setCurrentPart(*this, _part); } + + std::shared_ptr<UiObject> Editor::getTemplate(const std::string& _name) const + { + const auto& it = m_templates.find(_name); + if (it == m_templates.end()) + return {}; + return it->second; + } } diff --git a/source/juceUiLib/editor.h b/source/juceUiLib/editor.h @@ -4,13 +4,14 @@ #include <juce_audio_processors/juce_audio_processors.h> +#include "button.h" #include "uiObject.h" #include "editorInterface.h" namespace genericUI { - class Editor : public juce::Component + class Editor : public juce::Component, public juce::DragAndDropContainer { public: explicit Editor(EditorInterface& _interface); @@ -74,10 +75,26 @@ namespace genericUI size_t getConditionCountRecursive() const; size_t getControllerLinkCountRecursive() const; + void registerTemplate(const std::shared_ptr<UiObject>& _value); static void setEnabled(juce::Component& _component, bool _enable); - void setCurrentPart(uint8_t _part); + virtual void setCurrentPart(uint8_t _part); + + juce::TooltipWindow& getTooltipWindow() { return m_tooltipWindow; } + + std::shared_ptr<UiObject> getTemplate(const std::string& _name) const; + + virtual void setPerInstanceConfig(const std::vector<uint8_t>& _data) {} + virtual void getPerInstanceConfig(std::vector<uint8_t>& _data) {} + + virtual juce::Slider* createJuceComponent(juce::Slider*, UiObject& _object) { return nullptr; } + virtual juce::Component* createJuceComponent(juce::Component*, UiObject& _object) { return nullptr; } + virtual juce::ComboBox* createJuceComponent(juce::ComboBox*, UiObject& _object) { return nullptr; } + virtual juce::Label* createJuceComponent(juce::Label*, UiObject& _object) { return nullptr; } + virtual Button<juce::HyperlinkButton>* createJuceComponent(Button<juce::HyperlinkButton>*, UiObject& _object) { return nullptr; } + virtual Button<juce::DrawableButton>* createJuceComponent(Button<juce::DrawableButton>*, UiObject& _object, const std::string& _name, juce::DrawableButton::ButtonStyle) { return nullptr; } + virtual Button<juce::TextButton>* createJuceComponent(Button<juce::TextButton>*, UiObject& _object) { return nullptr; } private: EditorInterface& m_interface; @@ -91,6 +108,9 @@ namespace genericUI std::map<std::string, std::vector<juce::Component*>> m_componentsByName; std::map<std::string, TabGroup*> m_tabGroupsByName; + std::map<std::string, std::shared_ptr<UiObject>> m_templates; + + juce::TooltipWindow m_tooltipWindow; float m_scale = 1.0f; }; diff --git a/source/juceUiLib/hyperlinkbuttonStyle.cpp b/source/juceUiLib/hyperlinkbuttonStyle.cpp @@ -5,6 +5,9 @@ namespace genericUI void HyperlinkButtonStyle::apply(juce::HyperlinkButton& _target) const { _target.setColour(juce::HyperlinkButton::ColourIds::textColourId, m_color); + const auto f = getFont(); + if (f) + _target.setFont(*f, true); _target.setURL(juce::URL(juce::String(m_url))); TextButtonStyle::apply(_target); diff --git a/source/juceUiLib/labelStyle.cpp b/source/juceUiLib/labelStyle.cpp @@ -7,7 +7,8 @@ namespace genericUI if(m_bgColor.getARGB()) _target.setColour(juce::Label::ColourIds::backgroundColourId, m_bgColor); _target.setColour(juce::Label::ColourIds::textColourId, m_color); - _target.setText(m_text, juce::dontSendNotification); + if(!m_text.empty()) + _target.setText(m_text, juce::dontSendNotification); _target.setJustificationType(m_align); } } diff --git a/source/juceUiLib/listBoxStyle.cpp b/source/juceUiLib/listBoxStyle.cpp @@ -0,0 +1,11 @@ +#include "listBoxStyle.h" + +namespace genericUI +{ + void ListBoxStyle::apply(juce::ListBox& _target) const + { + _target.setColour(juce::ListBox::ColourIds::backgroundColourId, m_bgColor); + _target.setColour(juce::ListBox::ColourIds::outlineColourId, m_outlineColor); + _target.setColour(juce::ListBox::ColourIds::textColourId, m_color); + } +} diff --git a/source/juceUiLib/listBoxStyle.h b/source/juceUiLib/listBoxStyle.h @@ -0,0 +1,15 @@ +#pragma once + +#include "uiObjectStyle.h" + +namespace genericUI +{ + class ListBoxStyle : public UiObjectStyle + { + public: + explicit ListBoxStyle(Editor& _editor) : UiObjectStyle(_editor) {} + + public: + void apply(juce::ListBox& _target) const; + }; +} diff --git a/source/juceUiLib/scrollbarStyle.cpp b/source/juceUiLib/scrollbarStyle.cpp @@ -0,0 +1,17 @@ +#include "scrollbarStyle.h" + +namespace genericUI +{ + void ScrollBarStyle::apply(juce::ScrollBar& _target) const + { + if(m_bgColor.getARGB()) + { + _target.setColour(juce::ScrollBar::ColourIds::backgroundColourId, m_bgColor); + } + if(m_color.getARGB()) + { + _target.setColour(juce::ScrollBar::ColourIds::thumbColourId, m_color); + _target.setColour(juce::ScrollBar::ColourIds::trackColourId, m_color); + } + } +} diff --git a/source/juceUiLib/scrollbarStyle.h b/source/juceUiLib/scrollbarStyle.h @@ -0,0 +1,14 @@ +#pragma once + +#include "uiObjectStyle.h" + +namespace genericUI +{ + class ScrollBarStyle : public UiObjectStyle + { + public: + explicit ScrollBarStyle(Editor& _editor) : UiObjectStyle(_editor) {} + + void apply(juce::ScrollBar& _target) const; + }; +} diff --git a/source/juceUiLib/textEditorStyle.cpp b/source/juceUiLib/textEditorStyle.cpp @@ -0,0 +1,15 @@ +#include "textEditorStyle.h" + +namespace genericUI +{ + void TextEditorStyle::apply(juce::TextEditor& _target) const + { + _target.setColour(juce::TextEditor::ColourIds::backgroundColourId, m_bgColor); + _target.setColour(juce::TextEditor::ColourIds::outlineColourId, m_outlineColor); + _target.setColour(juce::TextEditor::ColourIds::textColourId, m_color); + _target.setColour(juce::TextEditor::ColourIds::focusedOutlineColourId, m_outlineColor); + _target.setColour(juce::TextEditor::ColourIds::highlightedTextColourId, m_color); + + _target.setTextToShowWhenEmpty(m_text, m_color.withAlpha(0.5f)); + } +} diff --git a/source/juceUiLib/textEditorStyle.h b/source/juceUiLib/textEditorStyle.h @@ -0,0 +1,15 @@ +#pragma once + +#include "uiObjectStyle.h" + +namespace genericUI +{ + class TextEditorStyle : public UiObjectStyle + { + public: + explicit TextEditorStyle(Editor& _editor) : UiObjectStyle(_editor) {} + + public: + void apply(juce::TextEditor& _target) const; + }; +} diff --git a/source/juceUiLib/textbuttonStyle.cpp b/source/juceUiLib/textbuttonStyle.cpp @@ -6,6 +6,13 @@ namespace genericUI { } + juce::Font TextButtonStyle::getTextButtonFont(juce::TextButton& _textButton, int buttonHeight) + { + if (const auto f = getFont()) + return *f; + return UiObjectStyle::getTextButtonFont(_textButton, buttonHeight); + } + void TextButtonStyle::apply(juce::Button& _target) const { _target.setColour(juce::TextButton::ColourIds::textColourOffId, m_color); diff --git a/source/juceUiLib/textbuttonStyle.h b/source/juceUiLib/textbuttonStyle.h @@ -11,6 +11,7 @@ namespace genericUI private: void drawButtonBackground(juce::Graphics&, juce::Button&, const juce::Colour& backgroundColour, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override; + juce::Font getTextButtonFont(juce::TextButton&, int buttonHeight) override; public: void apply(juce::Button& _target) const; }; diff --git a/source/juceUiLib/treeViewStyle.cpp b/source/juceUiLib/treeViewStyle.cpp @@ -0,0 +1,14 @@ +#include "treeViewStyle.h" + +namespace genericUI +{ + void TreeViewStyle::apply(juce::TreeView& _target) const + { + applyColorIfNotZero(_target, juce::TreeView::backgroundColourId, juce::Colour(m_bgColor)); + applyColorIfNotZero(_target, juce::TreeView::linesColourId, m_linesColor); + applyColorIfNotZero(_target, juce::TreeView::dragAndDropIndicatorColourId, m_dragAndDropIndicatorColor); + applyColorIfNotZero(_target, juce::TreeView::selectedItemBackgroundColourId, m_selectedItemBgColor); +// applyColorIfNotZero(_target, juce::TreeView::oddItemsColourId, juce::Colour(0xff333333)); +// applyColorIfNotZero(_target, juce::TreeView::evenItemsColourId, juce::Colour(0xff555555)); + } +} diff --git a/source/juceUiLib/treeViewStyle.h b/source/juceUiLib/treeViewStyle.h @@ -0,0 +1,14 @@ +#pragma once + +#include "uiObjectStyle.h" + +namespace genericUI +{ + class TreeViewStyle : public UiObjectStyle + { + public: + TreeViewStyle(Editor& _editor) : UiObjectStyle(_editor) {} + + void apply(juce::TreeView& _target) const; + }; +} diff --git a/source/juceUiLib/uiObject.cpp b/source/juceUiLib/uiObject.cpp @@ -6,16 +6,24 @@ #include "editor.h" -#include "rotaryStyle.h" -#include "comboboxStyle.h" #include "buttonStyle.h" -#include "textbuttonStyle.h" +#include "comboboxStyle.h" #include "hyperlinkbuttonStyle.h" #include "labelStyle.h" +#include "listBoxStyle.h" +#include "rotaryStyle.h" +#include "scrollbarStyle.h" +#include "textbuttonStyle.h" +#include "textEditorStyle.h" +#include "treeViewStyle.h" + +#include <cassert> + +#include "button.h" namespace genericUI { - UiObject::UiObject(const juce::var& _json) + UiObject::UiObject(const juce::var& _json, const bool _isTemplate/* = false*/) : m_isTemplate(_isTemplate) { auto* obj = _json.getDynamicObject(); @@ -78,6 +86,15 @@ namespace genericUI } } + void UiObject::registerTemplates(Editor& _editor) const + { + for(auto& s : m_templates) + _editor.registerTemplate(s); + + for (auto& ch : m_children) + ch->registerTemplates(_editor); + } + void UiObject::apply(Editor& _editor, juce::Component& _target) { const auto x = getPropertyInt("x"); @@ -85,16 +102,18 @@ namespace genericUI const auto w = getPropertyInt("width"); const auto h = getPropertyInt("height"); - if(w < 1 || h < 1) + if(w > 0 && h > 0) + { + _target.setTopLeftPosition(x, y); + _target.setSize(w, h); + } + else if (!m_isTemplate) { std::stringstream ss; ss << "Size " << w << "x" << h << " for object named " << m_name << " is invalid, each side must be > 0"; throw std::runtime_error(ss.str()); } - _target.setTopLeftPosition(x, y); - _target.setSize(w, h); - createCondition(_editor, _target); } @@ -146,25 +165,52 @@ namespace genericUI void UiObject::apply(Editor& _editor, juce::Label& _target) { - apply(_editor, static_cast<juce::Component&>(_target)); - auto* s = new LabelStyle(_editor); - createStyle(_editor, _target, s); - s->apply(_target); + applyT<juce::Label, LabelStyle>(_editor, _target); + } + + void UiObject::apply(Editor& _editor, juce::ScrollBar& _target) + { + applyT<juce::ScrollBar, ScrollBarStyle>(_editor, _target); } void UiObject::apply(Editor& _editor, juce::TextButton& _target) { - apply(_editor, static_cast<juce::Component&>(_target)); - auto* s = new TextButtonStyle(_editor); - createStyle(_editor, _target, s); - s->apply(_target); + applyT<juce::TextButton, TextButtonStyle>(_editor, _target); } void UiObject::apply(Editor& _editor, juce::HyperlinkButton& _target) { + applyT<juce::HyperlinkButton, HyperlinkButtonStyle>(_editor, _target); + } + + void UiObject::apply(Editor& _editor, juce::TreeView& _target) + { + applyT<juce::TreeView, TreeViewStyle>(_editor, _target); + } + + void UiObject::apply(Editor& _editor, juce::ListBox& _target) + { + applyT<juce::ListBox, ListBoxStyle>(_editor, _target); + } + + void UiObject::apply(Editor& _editor, juce::TextEditor& _target) + { + applyT<juce::TextEditor, TextEditorStyle>(_editor, _target); + } + + template <typename TComponent, typename TStyle> void UiObject::applyT(Editor& _editor, TComponent& _target) + { apply(_editor, static_cast<juce::Component&>(_target)); - auto* s = new HyperlinkButtonStyle(_editor); + + TStyle* s = nullptr; + + if (!m_style) + s = new TStyle(_editor); + createStyle(_editor, _target, s); + + s = dynamic_cast<TStyle*>(m_style.get()); + assert(s); s->apply(_target); } @@ -177,6 +223,9 @@ namespace genericUI for (const auto& child : m_children) child->collectVariants(_dst, _property); + + for (const auto& child : m_templates) + child->collectVariants(_dst, _property); } juce::Component* UiObject::createJuceObject(Editor& _editor) @@ -201,15 +250,15 @@ namespace genericUI } else if(hasComponent("button")) { - createJuceObject(_editor, new juce::DrawableButton(m_name, juce::DrawableButton::ImageRaw)); + createJuceObject<Button<juce::DrawableButton>>(_editor, m_name, juce::DrawableButton::ImageRaw); } else if(hasComponent("hyperlinkbutton")) { - createJuceObject<juce::HyperlinkButton>(_editor); + createJuceObject<Button<juce::HyperlinkButton>>(_editor); } else if(hasComponent("textbutton")) { - createJuceObject<juce::TextButton>(_editor); + createJuceObject<Button<juce::TextButton>>(_editor); } else if(hasComponent("label")) { @@ -361,6 +410,14 @@ namespace genericUI } } } + else if (key == "templates") + { + if (const auto children = value.getArray()) + { + for (const auto& c : *children) + m_templates.emplace_back(std::make_shared<UiObject>(c, true)); + } + } else if(key == "tabgroup") { auto buttons = value["buttons"].getArray(); @@ -457,9 +514,16 @@ namespace genericUI _target.getProperties().set(juce::Identifier(prop.first.c_str()), juce::var(prop.second.c_str())); } - template <typename T> T* UiObject::createJuceObject(Editor& _editor) + template <typename T, class... Args> T* UiObject::createJuceObject(Editor& _editor, Args... _args) { - return createJuceObject(_editor, new T()); + T* comp = nullptr; + + comp = _editor.createJuceComponent(comp, *this, _args...); + + if(!comp) + comp = new T(_args...); + + return createJuceObject(_editor, comp); } template <typename T> T* UiObject::createJuceObject(Editor& _editor, T* _object) @@ -507,7 +571,8 @@ namespace genericUI template <typename Target, typename Style> void UiObject::createStyle(Editor& _editor, Target& _target, Style* _style) { - m_style.reset(_style); + if(_style) + m_style.reset(_style); m_style->apply(_editor, *this); _target.setLookAndFeel(m_style.get()); } diff --git a/source/juceUiLib/uiObject.h b/source/juceUiLib/uiObject.h @@ -33,21 +33,30 @@ namespace genericUI class UiObject { public: - explicit UiObject(const juce::var& _json); + explicit UiObject(const juce::var& _json, bool _isTemplate = false); ~UiObject(); void createJuceTree(Editor& _editor); void createChildObjects(Editor& _editor, juce::Component& _parent) const; void createTabGroups(Editor& _editor); void createControllerLinks(Editor& _editor); + void registerTemplates(Editor& _editor) const; void apply(Editor& _editor, juce::Component& _target); void apply(Editor& _editor, juce::Slider& _target); void apply(Editor& _editor, juce::ComboBox& _target); void apply(Editor& _editor, juce::DrawableButton& _target); + void apply(Editor& _editor, juce::Label& _target); + void apply(Editor& _editor, juce::ScrollBar& _target); void apply(Editor& _editor, juce::TextButton& _target); void apply(Editor& _editor, juce::HyperlinkButton& _target); + void apply(Editor& _editor, juce::TreeView& _target); + void apply(Editor& _editor, juce::ListBox& _target); + void apply(Editor& _editor, juce::TextEditor& _target); + + template<typename TComponent, typename TStyle> + void applyT(Editor& _editor, TComponent& _target); void collectVariants(std::set<std::string>& _dst, const std::string& _property) const; @@ -62,9 +71,11 @@ namespace genericUI void setCurrentPart(Editor& _editor, uint8_t _part); + const auto& getName() const { return m_name; } + private: bool hasComponent(const std::string& _component) const; - template<typename T> T* createJuceObject(Editor& _editor); + template<typename T, class... Args> T* createJuceObject(Editor& _editor, Args... _args); template<typename T> T* createJuceObject(Editor& _editor, T* _object); void createCondition(Editor& _editor, juce::Component& _target); @@ -77,10 +88,13 @@ namespace genericUI template<typename Target, typename Style> void createStyle(Editor& _editor, Target& _target, Style* _style); + bool m_isTemplate; std::string m_name; std::map<std::string, std::map<std::string, std::string>> m_components; std::vector<std::unique_ptr<UiObject>> m_children; + std::vector<std::shared_ptr<UiObject>> m_templates; + std::vector<std::unique_ptr<juce::Component>> m_juceObjects; std::unique_ptr<UiObjectStyle> m_style; diff --git a/source/juceUiLib/uiObjectStyle.cpp b/source/juceUiLib/uiObjectStyle.cpp @@ -43,16 +43,28 @@ namespace genericUI { const auto color = _object.getProperty(_prop); - if(color.size() != 8) + uint32_t r,g,b,a; + + if(color.size() == 8) + { + sscanf(color.c_str(), "%02x%02x%02x%02x", &r, &g, &b, &a); + } + else if(color.size() == 6) + { + sscanf(color.c_str(), "%02x%02x%02x", &r, &g, &b); + a = 255; + } + else return; - uint32_t r,g,b,a; - sscanf(color.c_str(), "%02x%02x%02x%02x", &r, &g, &b, &a); _target = juce::Colour(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b), static_cast<uint8_t>(a)); }; parseColor(m_color, "color"); parseColor(m_bgColor, "backgroundColor"); + parseColor(m_linesColor, "linesColor"); + parseColor(m_selectedItemBgColor, "selectedItemBackgroundColor"); + parseColor(m_outlineColor, "outlineColor"); const auto alignH = _object.getProperty("alignH"); if(!alignH.empty()) @@ -94,14 +106,21 @@ namespace genericUI m_offsetB = _object.getPropertyInt("offsetB", -2); } + std::optional<juce::Font> UiObjectStyle::getFont() const + { + if (m_fontFile.empty()) + return {}; + + auto font = juce::Font(m_editor.getFont(m_fontFile).getTypeface()); + applyFontProperties(font); + return font; + } + juce::Font UiObjectStyle::getComboBoxFont(juce::ComboBox& _comboBox) { - if(!m_fontFile.empty()) - { - auto font = juce::Font(m_editor.getFont(m_fontFile).getTypeface()); - applyFontProperties(font); - return font; - } + if (const auto f = getFont()) + return *f; + auto font = LookAndFeel_V4::getComboBoxFont(_comboBox); applyFontProperties(font); return font; @@ -109,12 +128,9 @@ namespace genericUI juce::Font UiObjectStyle::getLabelFont(juce::Label& _label) { - if(!m_fontFile.empty()) - { - auto font = juce::Font(m_editor.getFont(m_fontFile).getTypeface()); - applyFontProperties(font); - return font; - } + if (const auto f = getFont()) + return *f; + auto font = LookAndFeel_V4::getLabelFont(_label); applyFontProperties(font); return font; @@ -129,12 +145,8 @@ namespace genericUI juce::Font UiObjectStyle::getTextButtonFont(juce::TextButton& _textButton, int buttonHeight) { - if(!m_fontFile.empty()) - { - auto font = juce::Font(m_editor.getFont(m_fontFile).getTypeface()); - applyFontProperties(font); - return font; - } + if (const auto f = getFont()) + return *f; auto font = LookAndFeel_V4::getTextButtonFont(_textButton, buttonHeight); applyFontProperties(font); return font; @@ -157,4 +169,10 @@ namespace genericUI _font.setBold(m_bold); _font.setItalic(m_italic); } + + void UiObjectStyle::applyColorIfNotZero(juce::Component& _target, int _id, const juce::Colour& _col) + { + if(_col.getARGB()) + _target.setColour(_id, _col); + } } diff --git a/source/juceUiLib/uiObjectStyle.h b/source/juceUiLib/uiObjectStyle.h @@ -2,6 +2,8 @@ #include <juce_audio_processors/juce_audio_processors.h> +#include <optional> + namespace genericUI { class Editor; @@ -18,6 +20,12 @@ namespace genericUI virtual void apply(Editor& _editor, const UiObject& _object); + const auto& getColor() const { return m_color; } + const auto& getSelectedItemBackgroundColor() const { return m_selectedItemBgColor; } + const auto& getAlign() const { return m_align; } + + std::optional<juce::Font> getFont() const; + protected: juce::Font getComboBoxFont(juce::ComboBox&) override; juce::Font getLabelFont(juce::Label&) override; @@ -28,19 +36,29 @@ namespace genericUI void applyFontProperties(juce::Font& _font) const; + static void applyColorIfNotZero(juce::Component& _target, int _id, const juce::Colour& _col); + Editor& m_editor; int m_tileSizeX = 0; int m_tileSizeY = 0; + juce::Drawable* m_drawable = nullptr; std::string m_fontFile; std::string m_fontName; int m_textHeight = 0; std::string m_text; + juce::Colour m_color = juce::Colour(0xffffffff); juce::Colour m_bgColor = juce::Colour(0); + juce::Colour m_linesColor = juce::Colour(0); + juce::Colour m_selectedItemBgColor = juce::Colour(0); + juce::Colour m_dragAndDropIndicatorColor = juce::Colour(0); + juce::Colour m_outlineColor = juce::Colour(0); + juce::Justification m_align = 0; + bool m_bold = false; bool m_italic = false; int m_offsetL = 0; diff --git a/source/mqJucePlugin/.gitignore b/source/mqJucePlugin/.gitignore @@ -0,0 +1 @@ +version.h diff --git a/source/synthLib/.gitignore b/source/synthLib/.gitignore @@ -0,0 +1 @@ +buildconfig.h diff --git a/source/synthLib/CMakeLists.txt b/source/synthLib/CMakeLists.txt @@ -1,12 +1,17 @@ cmake_minimum_required(VERSION 3.10) project(synthLib) +set(SYNTHLIB_DEMO_MODE OFF CACHE BOOL "Demo Mode" FORCE) + +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/buildconfig.h.in ${CMAKE_CURRENT_SOURCE_DIR}/buildconfig.h) + add_library(synthLib STATIC) set(SOURCES audiobuffer.cpp audiobuffer.h audioTypes.h binarystream.h + buildconfig.h buildconfig.h.in configFile.cpp configFile.h device.cpp device.h deviceException.cpp deviceException.h diff --git a/source/synthLib/binarystream.h b/source/synthLib/binarystream.h @@ -1,16 +1,19 @@ #pragma once #include <cstdint> +#include <functional> #include <iosfwd> #include <sstream> #include <vector> +#include <cstring> namespace synthLib { - template<typename SizeType> class BinaryStream final : std::stringstream { public: + using SizeType = uint32_t; + BinaryStream() = default; template<typename T> explicit BinaryStream(const std::vector<T>& _data) @@ -23,17 +26,29 @@ namespace synthLib // tools // - void toVector(std::vector<uint8_t>& _buffer) + void toVector(std::vector<uint8_t>& _buffer, bool _append = false) { const auto size = tellp(); if(size <= 0) { - _buffer.clear(); + if(!_append) + _buffer.clear(); return; } - _buffer.resize(size); + seekg(0); - std::stringstream::read(reinterpret_cast<char*>(_buffer.data()), size); + + if(_append) + { + const auto currentSize = _buffer.size(); + _buffer.resize(currentSize + size); + std::stringstream::read(reinterpret_cast<char*>(&_buffer[currentSize]), size); + } + else + { + _buffer.resize(size); + std::stringstream::read(reinterpret_cast<char*>(_buffer.data()), size); + } } bool checkString(const std::string& _str) @@ -54,6 +69,41 @@ namespace synthLib return result; } + uint32_t getWritePos() const + { + return (uint32_t)const_cast<BinaryStream&>(*this).tellp(); + } + + uint32_t getReadPos() const + { + return (uint32_t)const_cast<BinaryStream&>(*this).tellg(); + } + + void setWritePos(const uint32_t _pos) + { + seekp(_pos); + } + + void setReadPos(const uint32_t _pos) + { + seekg(_pos); + } + + bool endOfStream() const + { + return eof() || (getReadPos() == size()); + } + + SizeType size() const + { + const auto readPos = getReadPos(); + auto& s = const_cast<BinaryStream&>(*this); + s.seekg(0, std::ios_base::end); + const auto size = s.tellg(); + s.seekg(readPos); + return (SizeType)size; + } + // ___________________________________ // write // @@ -83,6 +133,16 @@ namespace synthLib write(std::string(_value)); } + template<size_t N, std::enable_if_t<N == 5, void*> = nullptr> + void write4CC(char const(&_str)[N]) + { + write(_str[0]); + write(_str[1]); + write(_str[2]); + write(_str[3]); + } + + // ___________________________________ // read // @@ -119,6 +179,30 @@ namespace synthLib return s; } + template<size_t N, std::enable_if_t<N == 5, void*> = nullptr> + void read4CC(char const(&_str)[N]) + { + char res[5]; + read4CC(res); + + return strcmp(res, _str) == 0; + } + + template<size_t N, std::enable_if_t<N == 5, void*> = nullptr> + void read4CC(char (&_str)[N]) + { + _str[0] = 'E'; + _str[1] = 'R'; + _str[2] = 'R'; + _str[3] = 'R'; + _str[4] = 0; + + _str[0] = read<char>(); + _str[1] = read<char>(); + _str[2] = read<char>(); + _str[3] = read<char>(); + } + // ___________________________________ // helpers // @@ -130,4 +214,129 @@ namespace synthLib throw std::range_error("end-of-stream"); } }; + + class ChunkWriter + { + public: + using SizeType = BinaryStream::SizeType; + + template<size_t N, std::enable_if_t<N == 5, void*> = nullptr> + ChunkWriter(BinaryStream& _stream, char const(&_4Cc)[N], const uint32_t _version = 1) : m_stream(_stream) + { + m_stream.write4CC(_4Cc); + m_stream.write(_version); + m_lengthWritePos = m_stream.getWritePos(); + m_stream.write<SizeType>(0); + } + + ~ChunkWriter() + { + const auto currentWritePos = m_stream.getWritePos(); + const SizeType chunkDataLength = currentWritePos - m_lengthWritePos - sizeof(SizeType); + m_stream.setWritePos(m_lengthWritePos); + m_stream.write(chunkDataLength); + m_stream.setWritePos(currentWritePos); + } + + private: + BinaryStream& m_stream; + SizeType m_lengthWritePos = 0; + }; + + class ChunkReader + { + public: + using SizeType = ChunkWriter::SizeType; + using ChunkCallback = std::function<void(BinaryStream&, uint32_t)>; // data, version + + struct Chunk + { + char fourcc[5]; + uint32_t expectedVersion; + ChunkCallback callback; + }; + + ChunkReader(BinaryStream& _stream) : m_stream(_stream) + { + } + + template<size_t N, std::enable_if_t<N == 5, void*> = nullptr> + void add(char const(&_4Cc)[N], const uint32_t _version, const ChunkCallback& _callback) + { + Chunk c; + strcpy(c.fourcc, _4Cc); + c.expectedVersion = _version; + c.callback = _callback; + supportedChunks.emplace_back(std::move(c)); + } + + void read() + { + while(!m_stream.endOfStream()) + { + char fourCC[5]; + m_stream.read4CC(fourCC); + const auto version = m_stream.read<uint32_t>(); + const auto length = m_stream.read<SizeType>(); + + bool hasReadChunk = false; + + ++m_numChunks; + + for (const auto& chunk : supportedChunks) + { + if(0 != strcmp(chunk.fourcc, fourCC)) + continue; + + if(version > chunk.expectedVersion) + break; + + std::vector<uint8_t> chunkData; + chunkData.reserve(length); + for(size_t i=0; i<length; ++i) + chunkData.push_back(m_stream.read<uint8_t>()); + + hasReadChunk = true; + ++m_numRead; + BinaryStream s(chunkData); + chunk.callback(s, version); + break; + } + + if(!hasReadChunk) + m_stream.setReadPos(m_stream.getReadPos() + length); + } + } + + bool tryRead() + { + const auto pos = m_stream.getReadPos(); + try + { + read(); + return true; + } + catch(std::range_error&) + { + m_stream.setReadPos(pos); + return false; + } + } + + uint32_t numRead() const + { + return m_numRead; + } + + uint32_t numChunks() const + { + return m_numChunks; + } + + private: + BinaryStream& m_stream; + std::vector<Chunk> supportedChunks; + uint32_t m_numRead = 0; + uint32_t m_numChunks = 0; + }; } diff --git a/source/synthLib/buildconfig.h.in b/source/synthLib/buildconfig.h.in @@ -0,0 +1,3 @@ +#pragma once + +#cmakedefine01 SYNTHLIB_DEMO_MODE diff --git a/source/synthLib/device.h b/source/synthLib/device.h @@ -1,11 +1,13 @@ #pragma once #include <cstdint> +#include <cstddef> #include "audioTypes.h" #include "deviceTypes.h" -#include "../synthLib/midiTypes.h" +#include "midiTypes.h" +#include "buildconfig.h" namespace synthLib { @@ -24,13 +26,20 @@ namespace synthLib virtual float getSamplerate() const = 0; virtual bool isValid() const = 0; + +#if SYNTHLIB_DEMO_MODE == 0 virtual bool getState(std::vector<uint8_t>& _state, StateType _type) = 0; virtual bool setState(const std::vector<uint8_t>& _state, StateType _type) = 0; - virtual bool setStateFromUnknownCustomData(const std::vector<uint8_t>& _state) { return false; } + virtual bool setStateFromUnknownCustomData(const std::vector<uint8_t> &_state) { return false; } +#endif virtual uint32_t getChannelCountIn() = 0; virtual uint32_t getChannelCountOut() = 0; + virtual bool setDspClockPercent(uint32_t _percent = 100) = 0; + virtual uint32_t getDspClockPercent() const = 0; + virtual uint64_t getDspClockHz() const = 0; + protected: virtual void readMidiOut(std::vector<SMidiEvent>& _midiOut) = 0; virtual void processAudio(const TAudioInputs& _inputs, const TAudioOutputs& _outputs, size_t _samples) = 0; diff --git a/source/synthLib/hybridcontainer.h b/source/synthLib/hybridcontainer.h @@ -1,216 +1,29 @@ #pragma once - +/* #include <vector> +#include <memory_resource> #include <array> -namespace syntLib +namespace synthLib { - template<typename T, size_t S> class HybridVector + template<typename T, size_t S> class BufferResource { public: - HybridVector(const HybridVector& _source) : m_array(_source.m_array), m_size(_source.m_size), m_vector(_source.m_vector ? new std::vector<T>(*_source.m_vector) : nullptr) - { - } - HybridVector(HybridVector&& _source) noexcept : m_array(std::move(_source.m_array)), m_size(_source.m_size), m_vector(_source.m_vector) - { - _source.m_size = 0; - _source.m_vector = nullptr; - } - ~HybridVector() - { - delete m_vector; - } - HybridVector& operator = (HybridVector&& _source) noexcept - { - m_array = _source.m_array; - m_size = _source.m_size; - m_vector = _source.m_vector; - - _source.m_size = 0; - _source.m_vector = nullptr; - - return *this; - } - HybridVector& operator = (const HybridVector& _source) noexcept // NOLINT(bugprone-unhandled-self-assignment) WTF, I handle it?! - { - if(&_source == this) - return *this; - - m_array = _source.m_array; - m_size = _source.m_size; - - if(_source.m_vector) - m_vector = new std::vector<T>(*_source.m_vector); - - return *this; - } - - HybridVector& operator = (const std::vector<T>& _source) noexcept - { - m_size = _source.size(); - - if(_source.size() <= S) - { - std::copy_n(_source.begin(), _source.size(), m_array.begin()); - } - else - { - if(!m_vector) - { - m_vector = new std::vector<T>(_source); - } - else - { - m_vector->clear(); - m_vector->reserve(_source.size()); - m_vector->insert(m_vector->begin(), _source.begin(), _source.end()); - } - } - return *this; - } - - HybridVector& operator = (std::vector<T>&& _source) noexcept - { - m_size = _source.size(); - - if(_source.size() <= S) - { - std::copy_n(_source.begin(), _source.size(), m_array.begin()); - } - else - { - if(!m_vector) - m_vector = new std::vector<T>(_source); - - std::swap(*m_vector, _source); - } - return *this; - } - - size_t size() const - { - return m_size; - } - - bool empty() const - { - return m_size == 0; - } - - void clear() - { - delete m_vector; - m_vector = nullptr; - m_size = 0; - } - - bool isDynamic() const - { - return m_vector != nullptr; - } - - void push_back(const T& _value) - { - makeDynamicIfNeeded(); - - if(m_vector) - m_vector->push_back(_value); - else - m_array[m_size] = _value; - ++m_size; - } - - void push_back(const T* _data, size_t _size) - { - if(!_size) - return; - - makeDynamicIfNeeded(_size); - - if(m_vector) - m_vector->insert(m_vector->end(), _data, _data + _size); - else - std::copy_n(_data, _size, &m_array[m_size]); - m_size += _size; - } - - void push_back(const std::vector<T>& _data) - { - if(_data.empty()) - return; - push_back(&_data[0], _data.size()); - } - - template<typename T2, size_t S2> void push_back(const HybridVector<T2,S2>& _source) - { - push_back(_source.begin(), _source.size()); - } - - T* begin() - { - if(m_vector) - return &m_vector->front(); - return &m_array[0]; - } - - T* end() - { - return begin() + size(); - } - - void pop_back() - { - if(empty()) - return; - - if(m_vector) - m_vector->pop_back(); - --m_size; - } - - const T& front() const - { - if(m_vector) - return m_vector->front(); - return m_array.front(); - } - - const T& back() const - { - if(m_vector) - return m_vector->back(); - return m_array.back(); - } - - void swap(HybridVector& _source) noexcept - { - if(!m_vector || !_source.m_vector) - std::swap(m_array, _source.m_array); - - std::swap(m_size, _source.m_size); - std::swap(m_vector, _source.m_vector); - } - - private: - bool makeDynamicIfNeeded(const size_t _elementCountToAdd = 1) - { - if(size() + _elementCountToAdd <= S) - return false; + auto& getPool() { return m_pool; } + protected: + std::array<T, S> m_buffer; + std::pmr::monotonic_buffer_resource m_pool{ std::data(m_buffer), std::size(m_buffer) }; + }; - makeDynamic(); - return true; - } + template<typename T, size_t S> + class HybridVector final : public BufferResource<T,S>, public std::pmr::vector<T> + { + public: + using Base = std::pmr::vector<T>; - void makeDynamic() + HybridVector() : BufferResource<T, S>(), Base(&static_cast<BufferResource<T, S>&>(*this).getPool()) { - if(!m_vector) - m_vector = new std::vector<T>(); - m_vector->reserve(m_size); - std::copy_n(m_array.begin(), m_size, m_vector->begin()); } - - std::array<T, S> m_array; - size_t m_size = 0; - std::vector<T>* m_vector = nullptr; }; } +*/ +\ No newline at end of file diff --git a/source/synthLib/midiToSysex.cpp b/source/synthLib/midiToSysex.cpp @@ -1,6 +1,7 @@ #include "midiToSysex.h" #include <cstdio> +#include <cstring> // memcmp #include "dsp56kEmu/logging.h" @@ -139,23 +140,63 @@ namespace synthLib return true; } - void MidiToSysex::splitMultipleSysex(std::vector<std::vector<uint8_t>>& _dst, const std::vector<uint8_t>& _src) + void MidiToSysex::splitMultipleSysex(std::vector<std::vector<uint8_t>>& _dst, const std::vector<uint8_t>& _src, const bool _isMidiFileData/* = false*/) { + if(!_isMidiFileData) + { + std::vector<size_t> indices; + + for (size_t i = 0; i < _src.size(); ++i) + { + if (indices.size() & 1) + { + if (_src[i] == 0xf7) + indices.push_back(i); + } + else if (_src[i] == 0xf0) + { + indices.push_back(i); + } + } + + if (indices.size() & 1) + indices.pop_back(); + + for(size_t i=0; i<indices.size(); i += 2) + { + auto& e =_dst.emplace_back(); + e.assign(_src.begin() + indices[i], _src.begin() + indices[i + 1] + 1); + } + return; + } + for (size_t i = 0; i < _src.size(); ++i) { if (_src[i] != 0xf0) continue; - for (size_t j = i + 1; j < _src.size(); ++j) + uint32_t numBytesRead = 0; + uint32_t length = 0; + + readVarLen(numBytesRead, length, &_src[i + 1], _src.size() - i - 1); + + // do some simple validation here, I've seen midi files where sysex is stored without varlength encoding + if (length == 0 || (numBytesRead > 1 && length < 128)) + numBytesRead = 0; + + const auto jStart = i + numBytesRead + 1; + + for(size_t j = jStart; j < _src.size(); ++j) { - if (_src[j] != 0xf7) + if(_src[j] <= 0xf0) continue; std::vector<uint8_t> entry; - entry.insert(entry.begin(), _src.begin() + i, _src.begin() + j + 1); - - _dst.emplace_back(entry); - + entry.reserve(j - jStart + 2); + entry.push_back(0xf0); + entry.insert(entry.end(), _src.begin() + jStart, _src.begin() + j); + entry.push_back(0xf7); + _dst.emplace_back(std::move(entry)); i = j; break; } @@ -174,7 +215,9 @@ namespace synthLib bool MidiToSysex::extractSysexFromData(std::vector<std::vector<uint8_t>>& _messages, const std::vector<uint8_t>& _data) { - splitMultipleSysex(_messages, _data); + constexpr uint8_t midiHeader[] = "MThd"; + const auto isMidiFile = _data.size() >= 4 && memcmp(_data.data(), midiHeader, 4) == 0; + splitMultipleSysex(_messages, _data, isMidiFile); return !_messages.empty(); } @@ -247,4 +290,26 @@ namespace synthLib } return(value); } + + void MidiToSysex::readVarLen(uint32_t& _numBytesRead, uint32_t& _result, const uint8_t* _data, const size_t _numBytes) + { + _numBytesRead = 0; + _result = 0; + + for(size_t i=0; i<_numBytes; ++i) + { + const auto b = _data[i]; + + const uint32_t v = b & 0x7f; + + _result += v; + + ++_numBytesRead; + + if (b & 0x80) + _result <<= 7; + else + break; + } + } } diff --git a/source/synthLib/midiToSysex.h b/source/synthLib/midiToSysex.h @@ -10,13 +10,14 @@ namespace synthLib { public: static bool readFile(std::vector<uint8_t>& _sysexMessages, const char* _filename); - static void splitMultipleSysex(std::vector<std::vector<uint8_t>>& _dst, const std::vector<uint8_t>& _src); + static void splitMultipleSysex(std::vector<std::vector<uint8_t>>& _dst, const std::vector<uint8_t>& _src, bool _isMidiFileData = false); static bool extractSysexFromFile(std::vector<std::vector<uint8_t>>& _messages, const std::string& _filename); static bool extractSysexFromData(std::vector<std::vector<uint8_t>>& _messages, const std::vector<uint8_t>& _data); private: static bool checkChunk(FILE* hFile, const char* _pCompareChunk); static uint32_t getChunkLength(FILE* hFile); static int32_t readVarLen(FILE* hFile, int* _pNumBytesRead); + static void readVarLen(uint32_t& _numBytesRead, uint32_t& _result, const uint8_t* _data, size_t _numBytes); static bool ignoreChunk(FILE* hFile); }; } diff --git a/source/synthLib/os.cpp b/source/synthLib/os.cpp @@ -136,12 +136,17 @@ namespace synthLib { while ((ent = readdir(dir))) { + std::string f = ent->d_name; + + if(f == "." || f == "..") + continue; + std::string file = _folder; if(file.back() != '/' && file.back() != '\\') file += '/'; - file += ent->d_name; + file += f; _files.push_back(file); } @@ -177,7 +182,7 @@ namespace synthLib return str; } - static std::string getExtension(const std::string &_name) + std::string getExtension(const std::string &_name) { const auto pos = _name.find_last_of('.'); if (pos != std::string::npos) @@ -185,6 +190,31 @@ namespace synthLib return {}; } + size_t getFileSize(const std::string& _file) + { + FILE* hFile = fopen(_file.c_str(), "rb"); + if (!hFile) + return 0; + + fseek(hFile, 0, SEEK_END); + const auto size = static_cast<size_t>(ftell(hFile)); + fclose(hFile); + return size; + } + + bool isDirectory(const std::string& _path) + { +#ifdef USE_DIRENT + struct stat statbuf; + stat(_path.c_str(), &statbuf); + if (S_ISDIR(statbuf.st_mode)) + return true; + return false; +#else + return std::filesystem::is_directory(_path); +#endif + } + std::string findFile(const std::string& _extension, const size_t _minSize, const size_t _maxSize, const bool _stripPluginComponentFolders) { std::string path = getModulePath(_stripPluginComponentFolders); @@ -228,13 +258,7 @@ namespace synthLib continue; } - FILE *hFile = fopen(file.c_str(), "rb"); - if (!hFile) - continue; - - fseek(hFile, 0, SEEK_END); - const auto size = static_cast<size_t>(ftell(hFile)); - fclose(hFile); + const auto size = getFileSize(file); if (_minSize && size < _minSize) continue; @@ -269,6 +293,9 @@ namespace synthLib bool hasExtension(const std::string& _filename, const std::string& _extension) { + if (_extension.empty()) + return true; + return lowercase(getExtension(_filename)) == lowercase(_extension); } @@ -313,7 +340,7 @@ namespace synthLib return true; } - if(_data.size() < static_cast<size_t>(size)) + if(_data.size() != static_cast<size_t>(size)) _data.resize(size); const auto read = fread(&_data[0], 1, _data.size(), hFile); diff --git a/source/synthLib/os.h b/source/synthLib/os.h @@ -20,7 +20,10 @@ namespace synthLib std::string findROM(size_t _minSize, size_t _maxSize); std::string findROM(size_t _expectedSize = 524288); + std::string getExtension(const std::string& _name); bool hasExtension(const std::string& _filename, const std::string& _extension); + size_t getFileSize(const std::string& _file); + bool isDirectory(const std::string& _path); void setFlushDenormalsToZero(); diff --git a/source/synthLib/plugin.cpp b/source/synthLib/plugin.cpp @@ -84,7 +84,7 @@ namespace synthLib { return m_device->isValid(); } - +#if !SYNTHLIB_DEMO_MODE bool Plugin::getState(std::vector<uint8_t>& _state, StateType _type) const { if(!m_device) @@ -119,7 +119,7 @@ namespace synthLib return m_device->setState(state, stateType); } - +#endif void Plugin::insertMidiEvent(const SMidiEvent& _ev) { if(m_midiIn.empty() || m_midiIn.back().offset <= _ev.offset) diff --git a/source/synthLib/plugin.h b/source/synthLib/plugin.h @@ -2,8 +2,9 @@ #include <mutex> -#include "../synthLib/midiTypes.h" -#include "../synthLib/resamplerInOut.h" +#include "midiTypes.h" +#include "resamplerInOut.h" +#include "buildconfig.h" #include "../dsp56300/source/dsp56kEmu/ringbuffer.h" @@ -31,9 +32,10 @@ namespace synthLib bool isValid() const; +#if !SYNTHLIB_DEMO_MODE bool getState(std::vector<uint8_t>& _state, StateType _type) const; bool setState(const std::vector<uint8_t>& _state); - +#endif void insertMidiEvent(const SMidiEvent& _ev); bool setLatencyBlocks(uint32_t _latencyBlocks); diff --git a/source/virusConsoleLib/audioProcessor.cpp b/source/virusConsoleLib/audioProcessor.cpp @@ -33,8 +33,8 @@ void AudioProcessor::processBlock(const uint32_t _blockSize) m_outputBuffers[i].resize(_blockSize); m_inputBuffers[i].resize(_blockSize); - m_inputs[i] = &m_inputBuffers[i][0]; - m_outputs[i] = &m_outputBuffers[i][0]; + m_inputs[i] = m_inputBuffers[i].data(); + m_outputs[i] = m_outputBuffers[i].data(); } m_stereoOutput.reserve(m_outputBuffers.size() * 2); diff --git a/source/virusConsoleLib/consoleApp.cpp b/source/virusConsoleLib/consoleApp.cpp @@ -173,7 +173,7 @@ std::string ConsoleApp::getSingleNameAsFilename() const void ConsoleApp::audioCallback(uint32_t audioCallbackCount) { - uc->process(1); // FIXME wrong value + uc->process(); constexpr uint8_t baseChannel = 0; @@ -306,7 +306,7 @@ void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampl constexpr uint32_t blockSize = 64; - constexpr uint32_t notifyThreshold = ((blockSize<<1) - 4); + constexpr uint32_t notifyThreshold = blockSize - 4; uint32_t callbackCount = 0; dsp56k::Semaphore sem(1); @@ -320,7 +320,8 @@ void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampl // The DSP thread needs to lock & unlock a mutex to inform the waiting thread (us) that data is // available if the output ring buffer was completely drained. We can omit this by ensuring that // the output buffer never becomes completely empty. - const auto sizeReached = esai.getAudioOutputs().size() >= notifyThreshold; + const auto availableSize = esai.getAudioOutputs().size(); + const auto sizeReached = availableSize >= notifyThreshold; --notifyTimeout; @@ -332,8 +333,8 @@ void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampl } callbackCount++; - if((callbackCount & 0x07) == 0) - audioCallback(callbackCount>>3); + if((callbackCount & 0x3) == 0) + audioCallback(callbackCount>>2); }, 0); bootDSP(_createDebugger).join(); diff --git a/source/virusIntegrationTest/integrationTest.cpp b/source/virusIntegrationTest/integrationTest.cpp @@ -38,120 +38,127 @@ int main(int _argc, char* _argv[]) try { - const CommandLine cmd(_argc, _argv); + bool forever = true; - if(cmd.contains("rom") && cmd.contains("preset")) + while(forever) { - const auto romFile = cmd.get("rom"); - const auto preset = cmd.get("preset"); + const CommandLine cmd(_argc, _argv); - IntegrationTest test(cmd, romFile, preset, std::string()); - return test.run(); - } - if(cmd.contains("folder")) - { - std::vector<std::string> subfolders; - synthLib::getDirectoryEntries(subfolders, cmd.get("folder")); + forever = cmd.contains("forever"); - if(subfolders.empty()) + if(cmd.contains("rom") && cmd.contains("preset")) { - std::cout << "Nothing found for testing in folder " << cmd.get("folder") << std::endl; - return -1; - } + const auto romFile = cmd.get("rom"); + const auto preset = cmd.get("preset"); - for (auto& subfolder : subfolders) + IntegrationTest test(cmd, romFile, preset, std::string()); + return test.run(); + } + if(cmd.contains("folder")) { - if(subfolder.find("/.") != std::string::npos) - continue; + std::vector<std::string> subfolders; + synthLib::getDirectoryEntries(subfolders, cmd.get("folder")); - std::vector<std::string> files; - synthLib::getDirectoryEntries(files, subfolder); - - std::string romFile; - std::string presetsFile; - - if(files.empty()) + if(subfolders.empty()) { - std::cout << "Directory " << subfolder << " doesn't contain any files" << std::endl; + std::cout << "Nothing found for testing in folder " << cmd.get("folder") << std::endl; return -1; } - for (auto& file : files) + for (auto& subfolder : subfolders) { - if(synthLib::hasExtension(file, ".txt")) - presetsFile = file; - if(synthLib::hasExtension(file, ".bin")) - romFile = file; - } + if(subfolder.find("/.") != std::string::npos) + continue; - if(romFile.empty()) - { - std::cout << "Failed to find ROM in folder " << subfolder << std::endl; - return -1; - } - if(presetsFile.empty()) - { - std::cout << "Failed to find presets file in folder " << subfolder << std::endl; - return -1; - } + std::vector<std::string> files; + synthLib::getDirectoryEntries(files, subfolder); - if(romFile.find("firmware") != std::string::npos) - { - auto* hFile = fopen(romFile.c_str(), "rb"); - size_t size = 0; - if(hFile) + std::string romFile; + std::string presetsFile; + + if(files.empty()) { - fseek(hFile, 0, SEEK_END); - size = ftell(hFile); - fclose(hFile); + std::cout << "Directory " << subfolder << " doesn't contain any files" << std::endl; + return -1; } - if(size > virusLib::ROMFile::getRomSizeModelABC()) + + for (auto& file : files) { - std::cout << "Ignoring TI verification tests, TI is not supported" << std::endl; - continue; + if(synthLib::hasExtension(file, ".txt")) + presetsFile = file; + if(synthLib::hasExtension(file, ".bin")) + romFile = file; } - } - std::vector<std::string> presets; + if(romFile.empty()) + { + std::cout << "Failed to find ROM in folder " << subfolder << std::endl; + return -1; + } + if(presetsFile.empty()) + { + std::cout << "Failed to find presets file in folder " << subfolder << std::endl; + return -1; + } - std::ifstream ss; - ss.open(presetsFile.c_str(), std::ios::in); + if(romFile.find("firmware") != std::string::npos) + { + auto* hFile = fopen(romFile.c_str(), "rb"); + size_t size = 0; + if(hFile) + { + fseek(hFile, 0, SEEK_END); + size = ftell(hFile); + fclose(hFile); + } + if(size > virusLib::ROMFile::getRomSizeModelABC()) + { + std::cout << "Ignoring TI verification tests, TI is not supported" << std::endl; + continue; + } + } - if(!ss.is_open()) - { - std::cout << "Failed to open presets file " << presetsFile << std::endl; - return -1; - } + std::vector<std::string> presets; - std::string line; + std::ifstream ss; + ss.open(presetsFile.c_str(), std::ios::in); - while(std::getline(ss, line)) - { - while(!line.empty() && line.find_last_of("\r\n") != std::string::npos) - line = line.substr(0, line.size()-1); - if(!line.empty()) - presets.push_back(line); - } + if(!ss.is_open()) + { + std::cout << "Failed to open presets file " << presetsFile << std::endl; + return -1; + } - ss.close(); + std::string line; - if(presets.empty()) - { - std::cout << "Presets file " << presetsFile << " is empty" << std::endl; - return -1; - } + while(std::getline(ss, line)) + { + while(!line.empty() && line.find_last_of("\r\n") != std::string::npos) + line = line.substr(0, line.size()-1); + if(!line.empty()) + presets.push_back(line); + } - for (auto& preset : presets) - { - IntegrationTest test(cmd, romFile, preset, subfolder + '/'); - if(test.run() != 0) + ss.close(); + + if(presets.empty()) + { + std::cout << "Presets file " << presetsFile << " is empty" << std::endl; return -1; + } + + for (auto& preset : presets) + { + IntegrationTest test(cmd, romFile, preset, subfolder + '/'); + if(test.run() != 0) + return -1; + } } - } - return 0; + if(!forever) + return 0; + } } - std::cout << "invalid command line arguments" << std::endl; return -1; } diff --git a/source/virusLib/device.cpp b/source/virusLib/device.cpp @@ -7,6 +7,8 @@ #include "../synthLib/deviceException.h" +#include <cstring> + namespace virusLib { Device::Device(const ROMFile& _rom, const bool _createDebugger/* = false*/) @@ -76,6 +78,7 @@ namespace virusLib m_numSamplesProcessed += static_cast<uint32_t>(_size); } +#if !SYNTHLIB_DEMO_MODE bool Device::getState(std::vector<uint8_t>& _state, const synthLib::StateType _type) { return m_mc->getState(_state, _type); @@ -93,8 +96,9 @@ namespace virusLib return false; return m_mc->setState(messages); } +#endif - bool Device::find4CC(uint32_t& _offset, const std::vector<uint8_t>& _data, const std::string& _4cc) + bool Device::find4CC(uint32_t& _offset, const std::vector<uint8_t>& _data, const std::string_view& _4cc) { for(uint32_t i=0; i<_data.size() - _4cc.size(); ++i) { @@ -333,7 +337,7 @@ namespace virusLib void Device::onAudioWritten() { m_mc->getMidiQueue(0).onAudioWritten(); - m_mc->process(1); + m_mc->process(); } void Device::configureDSP(DspSingle& _dsp, const ROMFile& _rom) @@ -354,4 +358,27 @@ namespace virusLib _dsp.startDSPThread(_createDebugger); return res; } + + bool Device::setDspClockPercent(const uint32_t _percent) + { + if(!m_dsp) + return false; + + bool res = m_dsp->getPeriphX().getEsaiClock().setSpeedPercent(_percent); + + if(m_dsp2) + res &= m_dsp2->getPeriphX().getEsaiClock().setSpeedPercent(_percent); + + return res; + } + + uint32_t Device::getDspClockPercent() const + { + return !m_dsp ? 0 : m_dsp->getPeriphX().getEsaiClock().getSpeedPercent(); + } + + uint64_t Device::getDspClockHz() const + { + return !m_dsp ? 0 : m_dsp->getPeriphX().getEsaiClock().getSpeedInHz(); + } } diff --git a/source/virusLib/device.h b/source/virusLib/device.h @@ -20,11 +20,12 @@ namespace virusLib void process(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _size, const std::vector<synthLib::SMidiEvent>& _midiIn, std::vector<synthLib::SMidiEvent>& _midiOut) override; +#if !SYNTHLIB_DEMO_MODE bool getState(std::vector<uint8_t>& _state, synthLib::StateType _type) override; bool setState(const std::vector<uint8_t>& _state, synthLib::StateType _type) override; bool setStateFromUnknownCustomData(const std::vector<uint8_t>& _state) override; - - static bool find4CC(uint32_t& _offset, const std::vector<uint8_t>& _data, const std::string& _4cc); +#endif + static bool find4CC(uint32_t& _offset, const std::vector<uint8_t>& _data, const std::string_view& _4cc); static bool parseTIcontrolPreset(std::vector<synthLib::SMidiEvent>& _events, const std::vector<uint8_t>& _state); static bool parsePowercorePreset(std::vector<std::vector<uint8_t>>& _sysexPresets, const std::vector<uint8_t>& _data); @@ -36,6 +37,10 @@ namespace virusLib static void createDspInstances(DspSingle*& _dspA, DspSingle*& _dspB, const ROMFile& _rom); static std::thread bootDSP(DspSingle& _dsp, const ROMFile& _rom, bool _createDebugger); + + bool setDspClockPercent(uint32_t _percent) override; + uint32_t getDspClockPercent() const override; + uint64_t getDspClockHz() const override; private: bool sendMidi(const synthLib::SMidiEvent& _ev, std::vector<synthLib::SMidiEvent>& _response) override; diff --git a/source/virusLib/dspSingle.h b/source/virusLib/dspSingle.h @@ -2,6 +2,7 @@ #include "dsp56kEmu/dspthread.h" #include "dsp56kEmu/memory.h" +#include "dsp56kEmu/peripherals.h" #include "../synthLib/audioTypes.h" diff --git a/source/virusLib/hdi08MidiQueue.cpp b/source/virusLib/hdi08MidiQueue.cpp @@ -71,6 +71,6 @@ namespace virusLib void Hdi08MidiQueue::onAudioWritten() { ++m_numSamplesWritten; - sendPendingMidiEvents(m_numSamplesWritten >> 1); + sendPendingMidiEvents(m_numSamplesWritten); } } diff --git a/source/virusLib/hdi08TxParser.h b/source/virusLib/hdi08TxParser.h @@ -1,10 +1,11 @@ #pragma once -#include <array> - #include "../synthLib/midiTypes.h" #include "../dsp56300/source/dsp56kEmu/types.h" +#include <array> +#include <cstddef> + namespace virusLib { class Microcontroller; diff --git a/source/virusLib/microcontroller.cpp b/source/virusLib/microcontroller.cpp @@ -75,7 +75,7 @@ Microcontroller::Microcontroller(DspSingle& _dsp, const ROMFile& _romFile, bool if(ROMFile::getSingleName(single).size() != 10) { failed = true; - break; + break; } singles.emplace_back(single); @@ -868,7 +868,7 @@ bool Microcontroller::loadMultiSingle(uint8_t _part, const TPreset& _multi) return partProgramChange(_part, partSingle); } -void Microcontroller::process(size_t _size) +void Microcontroller::process() { m_hdi08.exec(); @@ -883,6 +883,7 @@ void Microcontroller::process(size_t _size) sendPreset(preset.program, preset.data, preset.isMulti); } +#if !SYNTHLIB_DEMO_MODE bool Microcontroller::getState(std::vector<unsigned char>& _state, const StateType _type) { const auto deviceId = static_cast<uint8_t>(m_globalSettings[DEVICE_ID]); @@ -960,6 +961,7 @@ bool Microcontroller::setState(const std::vector<synthLib::SMidiEvent>& _events) return true; } +#endif void Microcontroller::addDSP(DspSingle& _dsp, bool _useEsaiBasedMidiTiming) { @@ -1158,6 +1160,6 @@ void Microcontroller::receiveUpgradedPreset() bool Microcontroller::isValid(const TPreset& _preset) { - return _preset.front() > 0; + return _preset[240] >= 32 && _preset[240] <= 127; } } diff --git a/source/virusLib/microcontroller.h b/source/virusLib/microcontroller.h @@ -4,6 +4,7 @@ #include "../synthLib/deviceTypes.h" #include "../synthLib/midiTypes.h" +#include "../synthLib/buildconfig.h" #include <list> #include <mutex> @@ -37,11 +38,13 @@ public: void sendInitControlCommands(); void createDefaultState(); - void process(size_t _size); + void process(); +#if !SYNTHLIB_DEMO_MODE bool getState(std::vector<unsigned char>& _state, synthLib::StateType _type); bool setState(const std::vector<unsigned char>& _state, synthLib::StateType _type); bool setState(const std::vector<synthLib::SMidiEvent>& _events); +#endif void addDSP(DspSingle& _dsp, bool _useEsaiBasedMidiTiming); diff --git a/source/virusLib/midiFileToRomData.cpp b/source/virusLib/midiFileToRomData.cpp @@ -10,12 +10,18 @@ namespace virusLib bool MidiFileToRomData::load(const std::string& _filename) { std::vector<uint8_t> sysex; - std::vector<std::vector<uint8_t>> packets; if(!synthLib::MidiToSysex::readFile(sysex, _filename.c_str())) return false; - synthLib::MidiToSysex::splitMultipleSysex(packets, sysex); + return load(sysex); + } + + bool MidiFileToRomData::load(const std::vector<uint8_t>& _fileData) + { + std::vector<std::vector<uint8_t>> packets; + + synthLib::MidiToSysex::splitMultipleSysex(packets, _fileData); return add(packets); } diff --git a/source/virusLib/midiFileToRomData.h b/source/virusLib/midiFileToRomData.h @@ -16,6 +16,7 @@ namespace virusLib } bool load(const std::string& _filename); + bool load(const std::vector<uint8_t>& _fileData); bool add(const std::vector<Packet>& _packets); bool add(const Packet& _packet); diff --git a/source/virusLib/romfile.cpp b/source/virusLib/romfile.cpp @@ -14,6 +14,8 @@ #include <cstring> // memcpy +#include "dsp56kEmu/memory.h" + #ifdef _WIN32 #define NOMINMAX #include <Windows.h> @@ -121,7 +123,7 @@ bool ROMFile::loadROMData(std::string& _loadedFile, std::vector<uint8_t>& _loade bool ROMFile::initialize() { - std::istream *dsp = new imemstream(reinterpret_cast<std::vector<char>&>(m_romFileData)); + const std::unique_ptr<std::istream> dsp(new imemstream(reinterpret_cast<std::vector<char>&>(m_romFileData))); const auto chunks = readChunks(*dsp); @@ -169,7 +171,7 @@ std::vector<ROMFile::Chunk> ROMFile::readChunks(std::istream& _file) } else { - LOG("Invalid ROM, unexpected filesize") + LOG("Invalid ROM, unexpected filesize"); return {}; } @@ -215,7 +217,11 @@ std::thread ROMFile::bootDSP(dsp56k::DSP& dsp, dsp56k::Peripherals56362& periph) { // Load BootROM in DSP memory for (uint32_t i=0; i<bootRom.data.size(); i++) - dsp.memory().set(dsp56k::MemArea_P, bootRom.offset + i, bootRom.data[i]); + { + const auto p = bootRom.offset + i; + dsp.memory().set(dsp56k::MemArea_P, p, bootRom.data[i]); + dsp.getJit().notifyProgramMemWrite(p); + } // dsp.memory().saveAssembly((m_file + "_BootROM.asm").c_str(), bootRom.offset, bootRom.size, false, false, &periph); diff --git a/temp/cmake_win64/gearmulator.sln.DotSettings b/temp/cmake_win64/gearmulator.sln.DotSettings @@ -5,7 +5,10 @@ <s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableClangFormatSupport/@EntryValue">False</s:Boolean> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=AAR/@EntryIndexedValue">AAR</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=AB/@EntryIndexedValue">AB</s:String> + <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=BMI/@EntryIndexedValue">BMI</s:String> + <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=CC/@EntryIndexedValue">CC</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=CCR/@EntryIndexedValue">CCR</s:String> + <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=DSP/@EntryIndexedValue">DSP</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=FX/@EntryIndexedValue">FX</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=LA/@EntryIndexedValue">LA</s:String> @@ -19,6 +22,7 @@ <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=SSH/@EntryIndexedValue">SSH</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=SSL/@EntryIndexedValue">SSL</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=TI/@EntryIndexedValue">TI</s:String> + <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Abbreviations/=XY/@EntryIndexedValue">XY</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Classes_0020and_0020structs/@EntryIndexedValue">&lt;NamingElement Priority="1"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="__interface" /&gt;&lt;type Name="class" /&gt;&lt;type Name="struct" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/NamingElement&gt;</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Class_0020and_0020struct_0020fields/@EntryIndexedValue">&lt;NamingElement Priority="11"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="class field" /&gt;&lt;type Name="struct field" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="m_" Suffix="" Style="aaBb" /&gt;&lt;/NamingElement&gt;</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Class_0020and_0020struct_0020methods/@EntryIndexedValue">&lt;NamingElement Priority="10"&gt;&lt;Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"&gt;&lt;type Name="member function" /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;&lt;/NamingElement&gt;</s:String> diff --git a/xcodeversion.cmake b/xcodeversion.cmake @@ -0,0 +1,20 @@ +# XCODE_VERSION is set by CMake when using the Xcode generator, otherwise we need +# to detect it manually here. +if(NOT XCODE_VERSION) + execute_process( + COMMAND xcodebuild -version + OUTPUT_VARIABLE xcodebuild_version + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_FILE /dev/null + ) + string(REGEX MATCH "Xcode ([0-9][0-9]?([.][0-9])+)" version_match ${xcodebuild_version}) + if(version_match) + message(STATUS "Identified Xcode Version: ${CMAKE_MATCH_1}") + set(XCODE_VERSION ${CMAKE_MATCH_1}) + else() + # If detecting Xcode version failed, set a crazy high version so we default + # to the newest. + set(XCODE_VERSION 99) + message(WARNING "Failed to detect the version of an installed copy of Xcode, falling back to highest supported version. Set XCODE_VERSION to override.") + endif() +endif()