commit 44608e6611d94896164175eb6a02ff083f248d11
parent 5d10c754fc61c9cb7909a3fbd18dacad56b9bcb7
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date: Wed, 17 Apr 2024 01:12:29 +0200
update Vavra open source version
Diffstat:
371 files changed, 32296 insertions(+), 3324 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.10)
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})
@@ -33,7 +34,7 @@ if(MSVC)
set(CMAKE_STATIC_LINKER_FLAGS_RELEASE "${CMAKE_STATIC_LINKER_FLAGS_RELEASE} /LTCG")
set(CMAKE_MODULE_LINKER_FLAGS_RELEASE "${CMAKE_MODULE_LINKER_FLAGS_RELEASE} /LTCG /DEBUG")
- set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG")
+ set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /LTCG /DEBUG")
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS /SAFESEH:NO")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SAFESEH:NO")
@@ -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.sh b/build_linux.sh
@@ -1,4 +1,4 @@
-cmake . -B ./temp/cmake_linux
+cmake . -B ./temp/cmake_linux -Dgearmulator_BUILD_JUCEPLUGIN=ON -Dgearmulator_BUILD_JUCEPLUGIN_CLAP=ON -Dgearmulator_BUILD_JUCEPLUGIN_LV2=ON -Dgearmulator_SYNTH_OSIRUS=ON
cd ./temp/cmake_linux
cmake --build . --config Release
cpack -G DEB
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,211 @@
Release Notes
+1.3.11
+
+Framework:
+
+- [Imp] Added disclaimer about ROM usage
+- [Imp] Add warning message if a Midi port cannot be opened because it is already in use
+- [Imp] Third-party skins are now expected to be in a folder that is named like the plugin:
+ skins_PlugName, for example skins_Osirus.
+ Currently selected skins are still loaded and you can export the current skin to
+ disk to create the new folder automatically. Move other skins to the new
+ folder manually.
+
+Patch Manager:
+
+- [Imp] Database is now cached so subsequent loads are faster. If a datasource needs to be
+ updated, right click and select "Refresh" to force a rescan
+- [Imp] Added a default text "Search..." to search boxes independent from the used skin
+- [Imp] Allow to add multiple files/folders at once when adding datasources
+
+- [Fix] Info about selected patch not updated when patch renamed or tags modified
+
+Osirus/OsTIrus:
+
+- [Fix] Plugin may crash if a patch data source folder contains files not belonging to presets
+
+1.3.10
+
+Vavra:
+
+- [Fix] AU validation got stuck
+- [Fix] LCD color being fully transparent if not specified in skin
+
+Osirus/OsTIrus:
+
+- [Fix] Part volume knob for part 1 was bound to selected part instead of part 1
+- [Fix] Selecting a ROM preset via part drop down didn't work after switching to
+ another ROM
+
+1.3.9
+
+DSP:
+
+- [Imp] Small performance optimizations
+
+- [Fix] DSP thread not set to high priority on Mac/Linux
+
+Framework:
+
+- [Imp] Upgraded Juce to version 7.0.10
+- [Imp] Plugin window can now have an arbitrary size by dragging the bottom right corner
+- [Imp] Added support for the LV2 plugin format
+- [Imp] Added options to Save button to allow storing the current preset in a user bank
+
+- [Fix] VST3 packaging corrected for Windows & Linux to match specification as stated at
+ https://steinbergmedia.github.io/vst3_dev_portal/pages/Technical+Documentation/Locations+Format/Plugin+Format.html
+- [Fix] Patch Manager: Failed to parse folder that contains UTF-8 characters
+- [Fix] Patch Manager: Selection was not updated when a part is saved to a user bank
+- [Fix] Knob movement via mouse wheel didn't update automation & parameter display
+
+Osirus/OsTIrus:
+
+- [Imp] Multiple ROMs are now supported. The ROM can be chosen from the dropdown, the
+ selected ROM is saved in the DAW plugin state. Note that the ROM content
+ is not saved, only the filename. If the ROM cannot be found when reopening
+ a project, the first one that is found is used instead.
+- [Imp] Added support for Virus A
+
+- [Fix] Crash if preset is selected in Single mode while parameters are locked
+- [Fix] Part Volume knob did not work in Single mode
+
+microQ:
+
+- [Fix] Added missing Attack Level parameter that was missing for all envelopes
+
+1.3.8 (2024.03.11)
+
+DSP:
+
+- [Imp] Small performance improvements
+
+Framework:
+
+- [Imp] Second click on root tree items of category tree deselects the item
+- [Imp] Patch Manager: "Uncategorized" has been moved into the Categories group
+
+- [Fix] Patch Manager: Missing patches if a directory contains files with unicode characters
+
+1.3.7
+
+Framework:
+
+- [Imp] Patch Manager: A category can now be deselected via second mouse click to remove
+ any category filter
+- [Imp] Patch Manager: Root tree items (Factory Patches, Datasources) are no longer
+ auto-expand upon load
+- [Imp] Patch Manager: data source names are now shortened
+
+- [Imp] Inform that changing plugin latency blocks while being in use might cause synchronization
+ problems
+
+- [Fix] Patch Manager: No error message was displayed if a user bank cannot be written to disk
+- [Fix] Patch Manager: It was not possible to add presets to a newly created tag until another
+ data source was selected
+
+Osirus:
+
+- [Imp] Disabled poly pressure to control Page B parameters to prevent accidental patch changes
+ by keyboards that emit MPE messages
+- [Fix] Locking parameter regions didn't work properly on older version presets and may
+ have caused silence or invalid presets
+
+1.3.6 (2024.03.03)
+
+DSP:
+
+- [Imp] Performance improvements
+
+- [Fix] Crash on x86 processors in various circumstances
+
+Framework:
+
+- [Fix] PatchManager: Edit field not refreshing text while typing in some hosts when adding
+ a user bank or favourite
+
+Osirus:
+
+- [Fix] Crash when loading Virus B OS 4.58
+
+1.3.5 (2024.03.01)
+
+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
+
+Vavra:
+
+- [Imp] LCD text color can now be adjusted via skin (export included skin and search
+ for "lcdTextColor" in json)
+
+- [Fix] Phase of input was not correct
+
+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
+
+Vavra:
+
+- [Fix] Possible crash in midi processing code
+
+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/CMakeLists.txt b/source/CMakeLists.txt
@@ -1,10 +1,8 @@
cmake_minimum_required(VERSION 3.15)
-option(${CMAKE_PROJECT_NAME}_BUILD_JUCEPLUGIN "Build Juce plugin" on)
-option(${CMAKE_PROJECT_NAME}_BUILD_JUCEPLUGIN_CLAP "Build CLAP version of Juce plugin" on)
-
option(${CMAKE_PROJECT_NAME}_SYNTH_OSIRUS "Build Osirus" off)
option(${CMAKE_PROJECT_NAME}_SYNTH_VAVRA "Build Vavra" on)
+option(${CMAKE_PROJECT_NAME}_SYNTH_XT "Build XT" on)
# ----------------- DSP56300 emulator
@@ -51,10 +49,15 @@ if(${CMAKE_PROJECT_NAME}_SYNTH_OSIRUS)
endif()
endif()
+# ----------------- Waldorf synths
+if(${CMAKE_PROJECT_NAME}_SYNTH_VAVRA OR ${CMAKE_PROJECT_NAME}_SYNTH_XT)
+ add_subdirectory(mc68k)
+ add_subdirectory(wLib)
+endif()
+
# ----------------- Synth Vavra
if(${CMAKE_PROJECT_NAME}_SYNTH_VAVRA)
- add_subdirectory(mc68k)
add_subdirectory(mqLib)
# needed for test console
@@ -78,3 +81,12 @@ if(${CMAKE_PROJECT_NAME}_SYNTH_VAVRA)
add_subdirectory(mqJucePlugin)
endif()
endif()
+
+if(${CMAKE_PROJECT_NAME}_SYNTH_XT)
+ add_subdirectory(xtLib)
+ add_subdirectory(xtTestConsole)
+
+ if(${CMAKE_PROJECT_NAME}_BUILD_JUCEPLUGIN)
+ add_subdirectory(xtJucePlugin)
+ endif()
+endif()
diff --git a/source/juce.cmake b/source/juce.cmake
@@ -1,31 +1,44 @@
+option(${CMAKE_PROJECT_NAME}_BUILD_JUCEPLUGIN "Build Juce plugins" on)
+option(${CMAKE_PROJECT_NAME}_BUILD_JUCEPLUGIN_CLAP "Build CLAP version of Juce plugins" on)
+option(${CMAKE_PROJECT_NAME}_BUILD_JUCEPLUGIN_LV2 "Build LV2 version of Juce plugins" off)
option(${CMAKE_PROJECT_NAME}_BUILD_FX_PLUGIN "Build FX plugin variants" off)
set(USE_CLAP ${CMAKE_PROJECT_NAME}_BUILD_JUCEPLUGIN_CLAP)
+set(USE_LV2 ${CMAKE_PROJECT_NAME}_BUILD_JUCEPLUGIN_LV2)
+
set(JUCE_CMAKE_DIR ${CMAKE_CURRENT_LIST_DIR})
+set(juce_formats AU VST3)
+
if(JUCE_GLOBAL_VST2_SDK_PATH)
- set(VST "VST")
-else()
- set(VST "")
-endif()
+ list(APPEND juce_formats VST)
+endif()
+
+if(USE_LV2)
+ list(APPEND juce_formats LV2)
+endif()
macro(createJucePlugin targetName productName isSynth plugin4CC binaryDataProject synthLibProject)
juce_add_plugin(${targetName}
- # VERSION ... # Set this if the plugin version is different to the project version
- # ICON_BIG ... # ICON_* arguments specify a path to an image file to use as an icon for the Standalone
+ # VERSION ... # Set this if the plugin version is different to the project version
+ # ICON_BIG ... # ICON_* arguments specify a path to an image file to use as an icon for the Standalone
# ICON_SMALL ...
- COMPANY_NAME "The Usual Suspects" # Specify the name of the plugin's author
- IS_SYNTH ${isSynth} # Is this a synth or an effect?
- NEEDS_MIDI_INPUT TRUE # Does the plugin need midi input?
- NEEDS_MIDI_OUTPUT TRUE # Does the plugin need midi output?
- IS_MIDI_EFFECT FALSE # Is this plugin a MIDI effect?
- EDITOR_WANTS_KEYBOARD_FOCUS TRUE # Does the editor need keyboard focus?
- COPY_PLUGIN_AFTER_BUILD FALSE # Should the plugin be installed to a default location after building?
- PLUGIN_MANUFACTURER_CODE TusP # A four-character manufacturer id with at least one upper-case character
- PLUGIN_CODE ${plugin4CC} # A unique four-character plugin id with exactly one upper-case character
- # GarageBand 10.3 requires the first letter to be upper-case, and the remaining letters to be lower-case
- FORMATS AU VST3 ${VST} Standalone # The formats to build. Other valid formats are: AAX Unity VST AU AUv3
- PRODUCT_NAME ${productName} # The name of the final executable, which can differ from the target name
+ COMPANY_NAME "The Usual Suspects" # Specify the name of the plugin's author
+ IS_SYNTH ${isSynth} # Is this a synth or an effect?
+ NEEDS_MIDI_INPUT TRUE # Does the plugin need midi input?
+ NEEDS_MIDI_OUTPUT TRUE # Does the plugin need midi output?
+ IS_MIDI_EFFECT FALSE # Is this plugin a MIDI effect?
+ EDITOR_WANTS_KEYBOARD_FOCUS TRUE # Does the editor need keyboard focus?
+ COPY_PLUGIN_AFTER_BUILD FALSE # Should the plugin be installed to a default location after building?
+ PLUGIN_MANUFACTURER_CODE TusP # A four-character manufacturer id with at least one upper-case character
+ PLUGIN_CODE ${plugin4CC} # A unique four-character plugin id with exactly one upper-case character
+ # GarageBand 10.3 requires the first letter to be upper-case, and the remaining letters to be lower-case
+ FORMATS ${juce_formats} # The formats to build. Other valid formats are: AAX Unity VST AU AUv3 LV2
+ PRODUCT_NAME ${productName} # The name of the final executable, which can differ from the target name
+ VST3_AUTO_MANIFEST TRUE # While generating a moduleinfo.json is nice, Juce does not properly package using cpack on Win/Linux
+ # and completely fails on Linux if we change the suffix to .vst3, so we skip that completely for now
+ BUNDLE_ID "com.theusualsuspects.${productName}"
+ LV2URI "http://theusualsuspects.lv2.${productName}"
)
target_sources(${targetName} PRIVATE ${SOURCES})
@@ -39,6 +52,7 @@ macro(createJucePlugin targetName productName isSynth plugin4CC binaryDataProjec
JUCE_USE_CURL=0 # If you remove this, add `NEEDS_CURL TRUE` to the `juce_add_plugin` call
JUCE_VST3_CAN_REPLACE_VST2=0
JUCE_WIN_PER_MONITOR_DPI_AWARE=0
+ JUCE_MODAL_LOOPS_PERMITTED=1
)
target_link_libraries(${targetName}
@@ -61,6 +75,14 @@ macro(createJucePlugin targetName productName isSynth plugin4CC binaryDataProjec
list(APPEND clapFeatures audio-effect synthesizer multi-effects)
endif()
+ if(TARGET ${targetName}_rc_lib)
+ set_property(TARGET ${targetName}_rc_lib PROPERTY FOLDER ${targetName})
+ endif()
+
+ if(TARGET ${binaryDataProject} AND ${isSynth})
+ set_property(TARGET ${binaryDataProject} PROPERTY FOLDER ${targetName})
+ endif()
+
if(USE_CLAP)
clap_juce_extensions_plugin(TARGET ${targetName}
CLAP_ID "com.theusualsuspects.${plugin4CC}"
@@ -68,31 +90,50 @@ macro(createJucePlugin targetName productName isSynth plugin4CC binaryDataProjec
CLAP_SUPPORT_URL "https://dsp56300.wordpress.com"
CLAP_MANUAL_URL "https://dsp56300.wordpress.com"
)
+ set_property(TARGET ${targetName}_CLAP PROPERTY FOLDER ${targetName})
endif()
if(UNIX AND NOT APPLE)
target_link_libraries(${targetName} PUBLIC -static-libgcc -static-libstdc++)
endif()
+
+ if(APPLE)
+ install(TARGETS ${targetName}_VST3 DESTINATION . COMPONENT ${productName}-VST3)
+ else()
+ get_target_property(vst3OutputFolder ${targetName}_VST3 ARCHIVE_OUTPUT_DIRECTORY)
+ if(UNIX)
+ set(dest /usr/local/lib/vst3)
+ set(pattern "*.so")
+ else()
+ set(dest .)
+ set(pattern "*.vst3")
+ endif()
+ install(DIRECTORY ${vst3OutputFolder}/${productName}.vst3 DESTINATION ${dest} COMPONENT ${productName}-VST3 FILES_MATCHING PATTERN ${pattern} PATTERN "*.json")
+ endif()
if(MSVC OR APPLE)
if(JUCE_GLOBAL_VST2_SDK_PATH)
install(TARGETS ${targetName}_VST DESTINATION . COMPONENT ${productName}-VST2)
endif()
- install(TARGETS ${targetName}_VST3 DESTINATION . COMPONENT ${productName}-VST3)
if(APPLE)
install(TARGETS ${targetName}_AU DESTINATION . COMPONENT ${productName}-AU)
endif()
if(USE_CLAP)
install(TARGETS ${targetName}_CLAP DESTINATION . COMPONENT ${productName}-CLAP)
endif()
+ if(USE_LV2)
+ install(TARGETS ${targetName}_LV2 DESTINATION . COMPONENT ${productName}-LV2)
+ endif()
elseif(UNIX)
if(JUCE_GLOBAL_VST2_SDK_PATH)
install(TARGETS ${targetName}_VST LIBRARY DESTINATION /usr/local/lib/vst/ COMPONENT ${productName}-VST2)
endif()
- install(TARGETS ${targetName}_VST3 LIBRARY DESTINATION /usr/local/lib/vst3/ COMPONENT ${productName}-VST3)
if(USE_CLAP)
install(TARGETS ${targetName}_CLAP LIBRARY DESTINATION /usr/local/lib/clap/ COMPONENT ${productName}-CLAP)
endif()
+ if(USE_LV2)
+ install(TARGETS ${targetName}_LV2 LIBRARY DESTINATION /usr/local/lib/lv2/ COMPONENT ${productName}-LV2)
+ endif()
endif()
if(APPLE AND ${isSynth})
diff --git a/source/jucePlugin/CMakeLists.txt b/source/jucePlugin/CMakeLists.txt
@@ -18,10 +18,14 @@ set(SOURCES
ui3/ControllerLinks.h
ui3/FxPage.cpp
ui3/FxPage.h
+ ui3/Leds.cpp
+ ui3/Leds.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/ParameterNames.h b/source/jucePlugin/ParameterNames.h
@@ -4,6 +4,7 @@ namespace Virus
{
static constexpr char g_paramDelayReverbMode[] = "Delay/Reverb Mode";
static constexpr char g_paramPartVolume[] = "Part Volume";
+ static constexpr char g_paramPatchVolume[] = "Patch Volume";
static constexpr char g_paramPartPanorama[] = "Panorama";
static constexpr char g_paramPlayMode[] = "Play Mode";
static constexpr char g_paramClockTempo[] = "Clock Tempo";
diff --git a/source/jucePlugin/PluginEditorState.cpp b/source/jucePlugin/PluginEditorState.cpp
@@ -22,3 +22,91 @@ 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);
+
+ const auto samplerates = m_processor.getDeviceSupportedSamplerates();
+
+ if(samplerates.size() > 1)
+ {
+ juce::PopupMenu srMenu;
+
+ const auto current = m_processor.getPreferredDeviceSamplerate();
+
+ const auto preferred = m_processor.getDevicePreferredSamplerates();
+
+ srMenu.addItem("Automatic (Match with host)", true, current == 0.0f, [this] { m_processor.setPreferredDeviceSamplerate(0.0f); });
+ srMenu.addSeparator();
+ srMenu.addSectionHeader("Official, used automatically");
+
+ auto addSRs = [&](bool _usePreferred)
+ {
+ for (const float samplerate : samplerates)
+ {
+ const auto isPreferred = std::find(preferred.begin(), preferred.end(), samplerate) != preferred.end();
+
+ if(isPreferred != _usePreferred)
+ continue;
+
+ const auto title = std::to_string(static_cast<int>(std::floor(samplerate + 0.5f))) + " Hz";
+
+ srMenu.addItem(title, _enabled, std::fabs(samplerate - current) < 1.0f, [this, samplerate] { m_processor.setPreferredDeviceSamplerate(samplerate); });
+ }
+ };
+
+ addSRs(true);
+ srMenu.addSeparator();
+ srMenu.addSectionHeader("Undocumented, use with care");
+ addSRs(false);
+
+ _menu.addSubMenu("Device Samplerate", srMenu);
+ }
+
+ 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
@@ -2,19 +2,23 @@
#include "PluginEditorState.h"
#include "ParameterNames.h"
-#include "../jucePluginEditorLib/pluginEditorWindow.h"
+#include "../virusLib/romloader.h"
-#include <juce_audio_processors/juce_audio_processors.h>
-#include <juce_audio_devices/juce_audio_devices.h>
+#include "../synthLib/deviceException.h"
+#include "../synthLib/binarystream.h"
+#include "../synthLib/os.h"
-static juce::PropertiesFile::Options getConfigOptions()
+namespace
{
- juce::PropertiesFile::Options opts;
- opts.applicationName = "DSP56300 Emulator";
- opts.filenameSuffix = ".settings";
- opts.folderName = "DSP56300 Emulator";
- opts.osxLibrarySubFolder = "Application Support/DSP56300 Emulator";
- return opts;
+ juce::PropertiesFile::Options getConfigOptions()
+ {
+ juce::PropertiesFile::Options opts;
+ opts.applicationName = "DSP56300 Emulator";
+ opts.filenameSuffix = ".settings";
+ opts.folderName = "DSP56300 Emulator";
+ opts.osxLibrarySubFolder = "Application Support/DSP56300 Emulator";
+ return opts;
+ }
}
//==============================================================================
@@ -26,226 +30,119 @@ AudioPluginAudioProcessor::AudioPluginAudioProcessor() :
.withOutput("Out 2", juce::AudioChannelSet::stereo(), true)
.withOutput("Out 3", juce::AudioChannelSet::stereo(), true)
#endif
- , getConfigOptions())
+ , ::getConfigOptions(), pluginLib::Processor::Properties{JucePlugin_Name, JucePlugin_IsSynth, JucePlugin_WantsMidiInput, JucePlugin_ProducesMidiOutput, JucePlugin_IsMidiEffect})
+ , m_roms(virusLib::ROMLoader::findROMs())
{
+ evRomChanged.retain(getSelectedRom());
+
m_clockTempoParam = getController().getParameterIndexByName(Virus::g_paramClockTempo);
const auto latencyBlocks = getConfig().getIntValue("latencyBlocks", static_cast<int>(getPlugin().getLatencyBlocks()));
- setLatencyBlocks(latencyBlocks);
+ Processor::setLatencyBlocks(latencyBlocks);
}
-AudioPluginAudioProcessor::~AudioPluginAudioProcessor() = default;
-
-//==============================================================================
-const juce::String AudioPluginAudioProcessor::getName() const
+AudioPluginAudioProcessor::~AudioPluginAudioProcessor()
{
- return JucePlugin_Name;
+ destroyEditorState();
}
-bool AudioPluginAudioProcessor::acceptsMidi() const
-{
- #if JucePlugin_WantsMidiInput
- return true;
- #else
- return false;
- #endif
-}
-
-bool AudioPluginAudioProcessor::producesMidi() const
-{
- #if JucePlugin_ProducesMidiOutput
- return true;
- #else
- return false;
- #endif
-}
+//==============================================================================
-bool AudioPluginAudioProcessor::isMidiEffect() const
+jucePluginEditorLib::PluginEditorState* AudioPluginAudioProcessor::createEditorState()
{
- #if JucePlugin_IsMidiEffect
- return true;
- #else
- return false;
- #endif
+ return new PluginEditorState(*this, getController());
}
-bool AudioPluginAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
+void AudioPluginAudioProcessor::processBpm(const float _bpm)
{
- // This is the place where you check if the layout is supported.
- // In this template code we only support mono or stereo.
- // Some plugin hosts, such as certain GarageBand versions, will only
- // load plugins that support stereo bus layouts.
- if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
- return false;
+ // clamp to virus range, 63-190
+ const auto bpmValue = juce::jmin(127, juce::jmax(0, static_cast<int>(_bpm)-63));
+ const auto clockParam = getController().getParameter(m_clockTempoParam, 0);
- // This checks if the input is stereo
- if (layouts.getMainInputChannelSet() != juce::AudioChannelSet::stereo())
- return false;
+ if (clockParam == nullptr || static_cast<int>(clockParam->getValueObject().getValue()) == bpmValue)
+ return;
- return true;
+ clockParam->getValueObject().setValue(bpmValue);
}
-void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
- juce::MidiBuffer& midiMessages)
+bool AudioPluginAudioProcessor::setSelectedRom(const uint32_t _index)
{
- juce::ignoreUnused (midiMessages);
-
- juce::ScopedNoDenormals noDenormals;
- const auto totalNumInputChannels = getTotalNumInputChannels();
- const auto totalNumOutputChannels = getTotalNumOutputChannels();
-
- // In case we have more outputs than inputs, this code clears any output
- // channels that didn't contain input data, (because these aren't
- // guaranteed to be empty - they may contain garbage).
- // This is here to avoid people getting screaming feedback
- // when they first compile a plugin, but obviously you don't need to keep
- // this code if your algorithm always overwrites all the output channels.
- for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
- buffer.clear (i, 0, buffer.getNumSamples());
-
- // This is the place where you'd normally do the guts of your plugin's
- // audio processing...
- // Make sure to reset the state if your inner loop is processing
- // the samples and the outer loop is handling the channels.
- // Alternatively, you can process the samples with the channels
- // interleaved by keeping the same state.
-
- synthLib::TAudioInputs inputs{};
- synthLib::TAudioOutputs outputs{};
-
- for (int channel = 0; channel < totalNumInputChannels; ++channel)
- inputs[channel] = buffer.getReadPointer(channel);
-
- for (int channel = 0; channel < totalNumOutputChannels; ++channel)
- outputs[channel] = buffer.getWritePointer(channel);
+ if(_index >= m_roms.size())
+ return false;
+ if(_index == m_selectedRom)
+ return true;
+ m_selectedRom = _index;
- for(const auto metadata : midiMessages)
+ try
{
- const auto message = metadata.getMessage();
+ synthLib::Device* device = createDevice();
+ getPlugin().setDevice(device);
+ (void)m_device.release();
+ m_device.reset(device);
- synthLib::SMidiEvent ev{};
+ evRomChanged.retain(getSelectedRom());
- if(message.isSysEx() || message.getRawDataSize() > 3)
- {
- ev.sysex.resize(message.getRawDataSize());
- memcpy( &ev.sysex[0], 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
- if(ev.sysex.size() > 1)
- {
- if(ev.sysex.front() == 0xf0 && ev.sysex[1] == 0xf0)
- ev.sysex.erase(ev.sysex.begin());
-
- if(ev.sysex.size() > 1)
- {
- if(ev.sysex[ev.sysex.size()-1] == 0xf7 && ev.sysex[ev.sysex.size()-2] == 0xf7)
- ev.sysex.erase(ev.sysex.begin());
- }
- }
- }
- else
- {
- ev.a = message.getRawData()[0];
- ev.b = message.getRawDataSize() > 0 ? message.getRawData()[1] : 0;
- ev.c = message.getRawDataSize() > 1 ? message.getRawData()[2] : 0;
-
- const auto status = ev.a & 0xf0;
-
- if(status == synthLib::M_CONTROLCHANGE || status == synthLib::M_POLYPRESSURE)
- {
- // forward to UI to react to control input changes that should move knobs
- static_cast<Virus::Controller&>(getController()).addPluginMidiOut(std::vector{ev});
- }
- }
-
- ev.offset = metadata.samplePosition;
-
- getPlugin().addMidiEvent(ev);
- }
-
- midiMessages.clear();
-
- juce::AudioPlayHead::CurrentPositionInfo pos{};
-
- auto* playHead = getPlayHead();
- if(playHead) {
- playHead->getCurrentPosition(pos);
-
- if(pos.bpm > 0) { // sync virus interal clock to host
- const uint8_t bpmValue = juce::jmin(127, juce::jmax(0, (int)pos.bpm-63)); // clamp to virus range, 63-190
- const auto clockParam = getController().getParameter(m_clockTempoParam, 0);
- if (clockParam != nullptr && (int)clockParam->getValueObject().getValue() != bpmValue) {
- clockParam->getValueObject().setValue(bpmValue);
- }
- }
+ return true;
}
-
- getPlugin().process(inputs, outputs, buffer.getNumSamples(), static_cast<float>(pos.bpm),
- static_cast<float>(pos.ppqPosition), pos.isPlaying);
-
- m_midiOut.clear();
- getPlugin().getMidiOut(m_midiOut);
-
- if (!m_midiOut.empty())
+ catch(const synthLib::DeviceException& e)
{
- getController().addPluginMidiOut(m_midiOut);
+ juce::NativeMessageBox::showMessageBox(juce::MessageBoxIconType::WarningIcon,
+ "Device creation failed:",
+ std::string("Failed to create device:\n\n") +
+ e.what() + "\n\n"
+ "Will continue using old ROM");
+ return false;
}
-
- for (auto& e : m_midiOut)
- {
- if (e.source == synthLib::MidiEventSourceEditor)
- continue;
-
- if (e.sysex.empty())
- {
- const juce::MidiMessage message(e.a, e.b, e.c, 0.0);
- midiMessages.addEvent(message, 0);
-
- // additionally send to the midi output we've selected in the editor
- if (m_midiOutput)
- m_midiOutput->sendMessageNow(message);
- }
- else
- {
- const juce::MidiMessage message(&e.sysex[0], static_cast<int>(e.sysex.size()), 0.0);
- midiMessages.addEvent(message, 0);
-
- // additionally send to the midi output we've selected in the editor
- if (m_midiOutput)
- m_midiOutput->sendMessageNow(message);
- }
- }
}
-//==============================================================================
-
-juce::AudioProcessorEditor* AudioPluginAudioProcessor::createEditor()
+synthLib::Device* AudioPluginAudioProcessor::createDevice()
{
- if(!m_editorState)
- m_editorState.reset(new PluginEditorState(*this, getController()));
- return new jucePluginEditorLib::EditorWindow(*this, *m_editorState, getConfig());
+ const auto* rom = getSelectedRom();
+ return new virusLib::Device(rom ? *rom : virusLib::ROMFile::invalid(), getPreferredDeviceSamplerate(), getHostSamplerate());
}
-void AudioPluginAudioProcessor::updateLatencySamples()
+pluginLib::Controller* AudioPluginAudioProcessor::createController()
{
- if constexpr(JucePlugin_IsSynth)
- setLatencySamples(getPlugin().getLatencyMidiToOutput());
- else
- setLatencySamples(getPlugin().getLatencyInputToOutput());
+ // force creation of device as the controller decides how to initialize based on the used ROM
+ getPlugin();
+
+ return new Virus::Controller(*this);
}
-synthLib::Device* AudioPluginAudioProcessor::createDevice()
+void AudioPluginAudioProcessor::saveChunkData(synthLib::BinaryStream& s)
{
- m_rom.reset(new virusLib::ROMFile(std::string()));
- return new virusLib::Device(*m_rom);
+ auto* rom = getSelectedRom();
+ if(rom)
+ {
+ synthLib::ChunkWriter cw(s, "ROM ", 2);
+ const auto romName = synthLib::getFilenameWithoutPath(rom->getFilename());
+ s.write<uint8_t>(static_cast<uint8_t>(rom->getModel()));
+ s.write(romName);
+ }
+ Processor::saveChunkData(s);
}
-pluginLib::Controller* AudioPluginAudioProcessor::createController()
+void AudioPluginAudioProcessor::loadChunkData(synthLib::ChunkReader& _cr)
{
- // force creation of device as the controller decides how to initialize based on the used ROM
- getPlugin();
+ _cr.add("ROM ", 2, [this](synthLib::BinaryStream& _binaryStream, unsigned _version)
+ {
+ auto model = virusLib::DeviceModel::ABC;
- return new Virus::Controller(*this);
+ if(_version > 1)
+ model = static_cast<virusLib::DeviceModel>(_binaryStream.read<uint8_t>());
+
+ const auto romName = _binaryStream.readString();
+
+ const auto& roms = getRoms();
+ for(uint32_t i=0; i<static_cast<uint32_t>(roms.size()); ++i)
+ {
+ const auto& rom = roms[i];
+ if(rom.getModel() == model && synthLib::getFilenameWithoutPath(rom.getFilename()) == romName)
+ setSelectedRom(i);
+ }
+ });
+
+ Processor::loadChunkData(_cr);
}
//==============================================================================
diff --git a/source/jucePlugin/PluginProcessor.h b/source/jucePlugin/PluginProcessor.h
@@ -3,12 +3,12 @@
#include "../synthLib/plugin.h"
#include "../virusLib/device.h"
+#include "../jucePluginLib/event.h"
+
#include "VirusController.h"
#include "../jucePluginEditorLib/pluginProcessor.h"
-class PluginEditorState;
-
//==============================================================================
class AudioPluginAudioProcessor : public jucePluginEditorLib::Processor
{
@@ -16,44 +16,62 @@ public:
AudioPluginAudioProcessor();
~AudioPluginAudioProcessor() override;
- bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
- void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
- using AudioProcessor::processBlock;
- juce::AudioProcessorEditor* createEditor() override;
- const juce::String getName() const override;
+ jucePluginEditorLib::PluginEditorState* createEditorState() override;
- bool acceptsMidi() const override;
- bool producesMidi() const override;
- bool isMidiEffect() const override;
+ void processBpm(float _bpm) override;
// _____________
//
std::string getRomName() const
{
- if(!m_rom)
+ const auto* rom = getSelectedRom();
+ if(!rom)
return "<invalid>";
- return juce::File(juce::String(m_rom->getFilename())).getFileNameWithoutExtension().toStdString();
+ return juce::File(juce::String(rom->getFilename())).getFileNameWithoutExtension().toStdString();
}
- virusLib::ROMFile::Model getModel() const
+
+ const virusLib::ROMFile* getSelectedRom() const
+ {
+ if(m_selectedRom >= m_roms.size())
+ return {};
+ return &m_roms[m_selectedRom];
+ }
+
+ virusLib::DeviceModel getModel() const
{
- return m_rom ? m_rom->getModel() : virusLib::ROMFile::Model::Invalid;
+ auto* rom = getSelectedRom();
+ return rom ? rom->getModel() : virusLib::DeviceModel::Invalid;
+ }
+
+ const auto& getRoms() const { return m_roms; }
+
+ bool setSelectedRom(uint32_t _index);
+ uint32_t getSelectedRomIndex() const { return m_selectedRom; }
+
+ uint32_t getPartCount() const
+ {
+ return getModel() == virusLib::DeviceModel::Snow ? 4 : 16;
}
// _____________
//
private:
- void updateLatencySamples() override;
-
synthLib::Device* createDevice() override;
pluginLib::Controller* createController() override;
+ void saveChunkData(synthLib::BinaryStream& s) override;
+ void loadChunkData(synthLib::ChunkReader& _cr) override;
+
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessor)
- std::unique_ptr<virusLib::ROMFile> m_rom;
+ std::vector<virusLib::ROMFile> m_roms;
+ uint32_t m_selectedRom = 0;
uint32_t m_clockTempoParam = 0xffffffff;
- std::unique_ptr<PluginEditorState> m_editorState;
+
+public:
+ pluginLib::Event<const virusLib::ROMFile*> evRomChanged;
};
diff --git a/source/jucePlugin/VirusController.cpp b/source/jucePlugin/VirusController.cpp
@@ -41,9 +41,12 @@ namespace Virus
switch(p.getModel())
{
default:
- case virusLib::ROMFile::Model::ABC: m_singles.resize(8); break;
- case virusLib::ROMFile::Model::Snow: m_singles.resize(10); break;
- case virusLib::ROMFile::Model::TI: m_singles.resize(26); break;
+ case virusLib::DeviceModel::A:
+ case virusLib::DeviceModel::B:
+ case virusLib::DeviceModel::C: m_singles.resize(8); break;
+ case virusLib::DeviceModel::Snow: m_singles.resize(10); break;
+ case virusLib::DeviceModel::TI:
+ case virusLib::DeviceModel::TI2: m_singles.resize(26); break;
}
registerParams(p);
@@ -190,6 +193,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 +222,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 +261,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 +348,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 +388,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;
@@ -387,15 +416,18 @@ namespace Virus
const uint8_t ch = patch.progNumber == virusLib::SINGLE ? 0 : patch.progNumber;
+ const auto locked = getLockedParameterNames();
+
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);
+ if(locked.find(p->getDescription().name) == locked.end())
+ 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 +464,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 +504,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 +654,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 +669,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 +678,26 @@ 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) const
{
- pluginLib::MidiPacket::Data data;
- pluginLib::MidiPacket::ParamValues parameterValues;
+ auto* m = getMidiPacket(midiPacketName(MidiPacketType::SingleDump));
+ assert(m);
- if(!parseSingle(data, parameterValues, _sysex))
- return {};
+ const auto idxBank = m->getByteIndexForType(pluginLib::MidiDataType::Bank);
+ const auto idxProgram = m->getByteIndexForType(pluginLib::MidiDataType::Program);
- return createSingleDump(_modifyBank ? toMidiByte(_newBank) : data[pluginLib::MidiDataType::Bank], _modifyProgram ? _newProgram : data[pluginLib::MidiDataType::Program], parameterValues);
+ assert(idxBank != pluginLib::MidiPacket::InvalidIndex);
+ assert(idxProgram != pluginLib::MidiPacket::InvalidIndex);
+
+ auto data = _sysex;
+
+ data[idxBank] = toMidiByte(_newBank);
+ data[idxProgram] = _newProgram;
+
+ return data;
}
- void Controller::selectPrevPreset(uint8_t _part)
+ void Controller::selectPrevPreset(const uint8_t _part)
{
if(getCurrentPartProgram(_part) > 0)
{
@@ -678,4 +719,52 @@ namespace Virus
sprintf(temp, "Bank %c", 'A' + _index);
return temp;
}
+
+ bool Controller::activatePatch(const std::vector<unsigned char>& _sysex)
+ {
+ 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);
+
+ // re-pack, force to edit buffer
+ const auto msg = modifySingleDump(_sysex, virusLib::BankNumber::EditBuffer, program);
+
+ if(msg.empty())
+ return false;
+
+ // if we have locked parameters, get them, send the preset and then send each locked parameter value afterward.
+ // Modifying the preset directly does not work because a preset might be an old version that we do not know
+ const auto lockedParameters = getLockedParameters(static_cast<uint8_t>(_part == virusLib::SINGLE ? 0 : _part));
+
+ sendSysEx(msg);
+
+ for (const auto& lockedParameter : lockedParameters)
+ {
+ const auto v = lockedParameter->getUnnormalizedValue();
+ sendParameterChange(*lockedParameter, static_cast<uint8_t>(v));
+ }
+
+ 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) const;
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/FxPage.cpp b/source/jucePlugin/ui3/FxPage.cpp
@@ -18,8 +18,8 @@ namespace genericVirusUI
const auto containerReverb = _editor.findComponent("ContainerReverb");
const auto containerDelay = _editor.findComponent("ContainerDelay");
- m_conditionReverb.reset(new genericUI::Condition(*containerReverb, p, 0, {2,3,4}));
- m_conditionDelay.reset(new genericUI::Condition(*containerDelay, p, 0, {0,1,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26}));
+ m_conditionReverb.reset(new genericUI::ConditionByParameterValues(*containerReverb, p, 0, {2,3,4}));
+ m_conditionDelay.reset(new genericUI::ConditionByParameterValues(*containerDelay, p, 0, {0,1,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26}));
}
FxPage::~FxPage()
diff --git a/source/jucePlugin/ui3/FxPage.h b/source/jucePlugin/ui3/FxPage.h
@@ -12,7 +12,7 @@ namespace genericVirusUI
explicit FxPage(const VirusEditor& _editor);
~FxPage();
private:
- std::unique_ptr<genericUI::Condition> m_conditionReverb;
- std::unique_ptr<genericUI::Condition> m_conditionDelay;
+ std::unique_ptr<genericUI::ConditionByParameterValues> m_conditionReverb;
+ std::unique_ptr<genericUI::ConditionByParameterValues> m_conditionDelay;
};
}
diff --git a/source/jucePlugin/ui3/Leds.cpp b/source/jucePlugin/ui3/Leds.cpp
@@ -0,0 +1,50 @@
+#include "Leds.h"
+
+#include "VirusEditor.h"
+
+#include "../PluginProcessor.h"
+
+namespace genericVirusUI
+{
+ constexpr const char* g_lfoNames[3] = {"Lfo1LedOn", "Lfo2LedOn", "Lfo3LedOn"};
+
+ Leds::Leds(const genericUI::Editor& _editor, AudioPluginAudioProcessor& _processor)
+ {
+ for(size_t i=0; i<m_lfos.size(); ++i)
+ {
+ if(auto* comp = _editor.findComponentT<juce::Component>(g_lfoNames[i], false))
+ {
+ m_lfos[i].reset(new jucePluginEditorLib::Led(comp));
+ m_lfos[i]->setSourceCallback([i, &_processor]
+ {
+ auto* d = dynamic_cast<virusLib::Device*>(_processor.getPlugin().getDevice());
+
+ const auto v = std::clamp(d->getFrontpanelState().m_lfoPhases[i], 0.0f, 1.0f);
+ return std::pow(1.0f - v, 0.2f);
+ });
+ }
+ }
+
+ if(auto* comp = _editor.findComponentT<juce::Component>("logolight", false))
+ {
+ m_logo.reset(new jucePluginEditorLib::Led(comp));
+
+ m_logo->setSourceCallback([&_processor]
+ {
+ auto* d = dynamic_cast<virusLib::Device*>(_processor.getPlugin().getDevice());
+
+ const auto& s = d->getFrontpanelState();
+
+ const auto v = std::clamp(_processor.getModel() == virusLib::DeviceModel::Snow ? s.m_bpm : s.m_logo, 0.0f, 1.0f);
+
+ return std::pow(1.0f - v, 0.2f);
+ });
+ }
+ }
+
+ Leds::~Leds()
+ {
+ for (auto& led : m_lfos)
+ led.reset();
+ }
+}
diff --git a/source/jucePlugin/ui3/Leds.h b/source/jucePlugin/ui3/Leds.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <array>
+#include <memory>
+
+#include "../../jucePluginEditorLib/led.h"
+
+class AudioPluginAudioProcessor;
+
+namespace genericUI
+{
+ class Editor;
+}
+
+namespace genericVirusUI
+{
+ class Leds
+ {
+ public:
+ Leds(const genericUI::Editor& _editor, AudioPluginAudioProcessor& _processor);
+ ~Leds();
+
+ private:
+ std::array<std::unique_ptr<jucePluginEditorLib::Led>, 3> m_lfos;
+ std::unique_ptr<jucePluginEditorLib::Led> m_logo;
+ };
+}
diff --git a/source/jucePlugin/ui3/PartButton.cpp b/source/jucePlugin/ui3/PartButton.cpp
@@ -0,0 +1,83 @@
+#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
+ {
+ pluginLib::patchDB::SearchRequest req;
+ req.sourceType = pluginLib::patchDB::SourceType::Rom;
+
+ m_editor.getPatchManager()->search(std::move(req), [this](const pluginLib::patchDB::Search& _search)
+ {
+ std::map<std::string, std::vector<pluginLib::patchDB::PatchPtr>> patches;
+
+ {
+ std::shared_lock lock(_search.resultsMutex);
+ for (const auto& patch : _search.results)
+ {
+ const auto s = patch->source.lock();
+ if(!s)
+ continue;
+ patches[s->name].push_back(patch);
+ }
+ }
+
+ for (auto& it : patches)
+ {
+ std::sort(it.second.begin(), it.second.end(), [](const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b)
+ {
+ return _a->program < _b->program;
+ });
+ }
+
+ juce::MessageManager::callAsync([this, patches = std::move(patches)]
+ {
+ juce::PopupMenu selector;
+
+ for (const auto& it : patches)
+ {
+ juce::PopupMenu p;
+ for (const auto& patch : it.second)
+ {
+ const auto& presetName = patch->getName();
+ p.addItem(presetName, [this, patch]
+ {
+ if(m_editor.getPatchManager()->activatePatch(patch, getPart()))
+ m_editor.getPatchManager()->setSelectedPatch(getPart(), patch);
+ });
+ }
+ selector.addSubMenu(it.first, p);
+ }
+ selector.showMenuAsync(juce::PopupMenu::Options());
+ });
+ });
+ }
+}
diff --git a/source/jucePlugin/ui3/PartButton.h b/source/jucePlugin/ui3/PartButton.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#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,27 +1,42 @@
#include "Parts.h"
+#include "PartButton.h"
#include "VirusEditor.h"
#include "../VirusController.h"
+#include "../PluginProcessor.h"
#include "../ParameterNames.h"
+#include "../../jucePluginEditorLib/pluginProcessor.h"
+#include "../../jucePluginEditorLib/patchmanager/savepatchdesc.h"
+
#include "../../jucePluginLib/parameterbinding.h"
+#include "../../virusLib/device.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");
+ _editor.findComponents<juce::Component>(m_partActive, "PartActive");
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 +44,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);
@@ -45,6 +60,17 @@ namespace genericVirusUI
}
updateAll();
+
+ if(!m_partActive.empty())
+ {
+ for (const auto & partActive : m_partActive)
+ {
+ partActive->setInterceptsMouseClicks(false, false);
+ partActive->setVisible(false);
+ }
+
+ startTimer(1000/20);
+ }
}
Parts::~Parts() = default;
@@ -69,14 +95,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 +135,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
@@ -138,9 +166,11 @@ namespace genericVirusUI
{
const auto multiMode = m_editor.getController().isMultiMode();
+ const auto partCount = multiMode ? static_cast<AudioPluginAudioProcessor&>(m_editor.getProcessor()).getPartCount() : 1;
+
for(size_t i=0; i<m_partSelect.size(); ++i)
{
- const bool visible = multiMode || !i;
+ const bool visible = i < partCount;
VirusEditor::setEnabled(*m_partSelect[i], visible);
VirusEditor::setEnabled(*m_partPan[i], visible);
@@ -154,6 +184,28 @@ namespace genericVirusUI
m_presetName[i]->setVisible(visible);
}
+
+ const auto volumeParam = m_editor.getController().getParameterIndexByName(multiMode ? Virus::g_paramPartVolume : Virus::g_paramPatchVolume);
+ m_editor.getParameterBinding().bind(*m_partVolume[0], volumeParam, 0);
+ m_partVolume[0]->getProperties().set("parameter", static_cast<int>(volumeParam));
+ }
+
+ void Parts::timerCallback()
+ {
+ auto* device = dynamic_cast<virusLib::Device*>(m_editor.getProcessor().getPlugin().getDevice());
+
+ if(!device)
+ return;
+
+ auto& fpState = device->getFrontpanelState();
+
+ const uint32_t maxPart = m_editor.getController().isMultiMode() ? 16 : 1;
+
+ for(uint32_t i=0; i<m_partActive.size(); ++i)
+ {
+ m_partActive[i]->setVisible(i < maxPart && fpState.m_midiEventReceived[i]);
+ fpState.m_midiEventReceived[i] = false;
+ }
}
void Parts::updateAll() const
diff --git a/source/jucePlugin/ui3/Parts.h b/source/jucePlugin/ui3/Parts.h
@@ -2,17 +2,35 @@
#include <vector>
-#include <juce_audio_processors/juce_audio_processors.h>
+#include "../../juceUiLib/button.h"
namespace genericVirusUI
{
+ class PartButton;
class VirusEditor;
- class Parts
+ 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 : juce::Timer
{
public:
explicit Parts(VirusEditor& _editor);
- virtual ~Parts();
+ ~Parts() override;
void onProgramChange() const;
void onPlayModeChanged() const;
@@ -20,24 +38,27 @@ 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;
void updateAll() const;
void updateSingleOrMultiMode() const;
+ void timerCallback() override;
+
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::Component*> m_partActive;
- 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,437 @@
+#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
+{
+ 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,42 @@
+#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;
+
+ // 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"
@@ -17,11 +19,13 @@ namespace genericVirusUI
Editor(_processorRef, _binding, std::move(_skinFolder)),
m_processor(_processorRef),
m_parameterBinding(_binding),
- m_openMenuCallback(std::move(_openMenuCallback))
+ m_openMenuCallback(std::move(_openMenuCallback)),
+ m_romChangedListener(_processorRef.evRomChanged)
{
create(_jsonFilename);
m_parts.reset(new Parts(*this));
+ m_leds.reset(new Leds(*this, _processorRef));
// be backwards compatible with old skins
if(getTabGroupCount() == 0)
@@ -37,7 +41,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");
@@ -61,15 +73,32 @@ namespace genericVirusUI
if(m_romSelector)
{
- if(!_processorRef.isPluginValid())
+ const auto roms = m_processor.getRoms();
+
+ if(roms.empty())
+ {
m_romSelector->addItem("<No ROM found>", 1);
+ }
else
- m_romSelector->addItem(_processorRef.getRomName(), 1);
+ {
+ int id = 1;
+
+ for (const auto& rom : roms)
+ m_romSelector->addItem(juce::File(rom.getFilename()).getFileNameWithoutExtension(), id++);
+ }
- m_romSelector->setSelectedId(1, juce::dontSendNotification);
+ m_romSelector->setSelectedId(static_cast<int>(m_processor.getSelectedRomIndex()) + 1, juce::dontSendNotification);
+
+ m_romSelector->onChange = [this, roms]
+ {
+ const auto oldIndex = m_processor.getSelectedRomIndex();
+ const auto newIndex = m_romSelector->getSelectedId() - 1;
+ if(!m_processor.setSelectedRom(newIndex))
+ m_romSelector->setSelectedId(static_cast<int>(oldIndex) + 1);
+ };
}
- getController().onProgramChange = [this] { onProgramChange(); };
+ getController().onProgramChange = [this](int _part) { onProgramChange(_part); };
addMouseListener(this, true);
@@ -86,8 +115,6 @@ namespace genericVirusUI
m_deviceModel = findComponentT<juce::Label>("DeviceModel", false);
- updateDeviceModel();
-
auto* presetSave = findComponentT<juce::Button>("PresetSave", false);
if(presetSave)
presetSave->onClick = [this] { savePreset(); };
@@ -103,9 +130,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);
@@ -114,10 +146,21 @@ namespace genericVirusUI
updatePresetName();
updatePlayModeButtons();
+
+ m_romChangedListener = [this](auto)
+ {
+ updateDeviceModel();
+ updateKeyValueConditions("deviceModel", virusLib::getModelName(m_processor.getModel()));
+ m_parts->onPlayModeChanged();
+ };
}
VirusEditor::~VirusEditor()
{
+ m_presetName->removeMouseListener(m_presetNameMouseListener);
+ delete m_presetNameMouseListener;
+ m_presetNameMouseListener = nullptr;
+
m_focusedParameter.reset();
m_parameterBinding.clearBindings();
@@ -150,17 +193,31 @@ namespace genericVirusUI
return findEmbeddedResource(_filename, _size);
}
- PatchBrowser* VirusEditor::getPatchBrowser()
+ std::pair<std::string, std::string> VirusEditor::getDemoRestrictionText() const
{
- return m_patchBrowser.get();
+ 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)
+ {
+ 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()
@@ -201,9 +258,12 @@ namespace genericVirusUI
if(!m_deviceModel)
return;
- const auto& presets = getController().getSinglePresets();
- const auto& data = presets.front().front().data;
- if(data.empty())
+ auto* rom = m_processor.getSelectedRom();
+ if(!rom)
+ return;
+
+ virusLib::ROMFile::TPreset data;
+ if(!rom->getSingle(0, 0, data))
return;
std::string m;
@@ -219,31 +279,35 @@ namespace genericVirusUI
}
m_deviceModel->setText(m, juce::dontSendNotification);
- m_deviceModel = nullptr; // only update once
}
void VirusEditor::savePreset()
{
juce::PopupMenu menu;
- auto addEntry = [&](juce::PopupMenu& _menu, const std::string& _name, const std::function<void(FileType)>& _callback)
+ const auto countAdded = getPatchManager()->createSaveMenuEntries(menu, getPatchManager()->getCurrentPart());
+
+ if(countAdded)
+ menu.addSeparator();
+
+ 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, "Export 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, "Export Arrangement (Multi + 16 Singles)", [this](jucePluginEditorLib::FileType _type)
{
savePresets(SaveType::Arrangement, _type);
});
@@ -252,13 +316,13 @@ 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);
});
}
- menu.addSubMenu("Bank", banksMenu);
+ menu.addSubMenu("Export Bank", banksMenu);
menu.showMenuAsync(juce::PopupMenu::Options());
}
@@ -267,33 +331,35 @@ 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.activatePatch(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();
});
}
@@ -302,7 +368,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 +381,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 +433,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
@@ -4,11 +4,19 @@
#include "../../jucePluginEditorLib/pluginEditor.h"
#include "../../jucePluginEditorLib/focusedParameter.h"
+#include "../../jucePluginLib/event.h"
+
#include "Parts.h"
#include "Tabs.h"
#include "FxPage.h"
-#include "PatchBrowser.h"
+#include "PatchManager.h"
#include "ControllerLinks.h"
+#include "Leds.h"
+
+namespace virusLib
+{
+ class ROMFile;
+}
namespace jucePluginEditorLib
{
@@ -41,7 +49,6 @@ namespace genericVirusUI
void setPart(size_t _part);
- AudioPluginAudioProcessor& getProcessor() const { return m_processor; }
pluginLib::ParameterBinding& getParameterBinding() const { return m_parameterBinding; }
Virus::Controller& getController() const;
@@ -49,10 +56,12 @@ namespace genericVirusUI
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,20 +77,21 @@ 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;
std::unique_ptr<Parts> m_parts;
+ std::unique_ptr<Leds> m_leds;
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 +103,8 @@ namespace genericVirusUI
juce::Label* m_deviceModel = nullptr;
- juce::TooltipWindow m_tooltipWindow;
-
std::function<void()> m_openMenuCallback;
+
+ pluginLib::EventListener<const virusLib::ROMFile*> m_romChangedListener;
};
}
diff --git a/source/jucePluginEditorLib/CMakeLists.txt b/source/jucePluginEditorLib/CMakeLists.txt
@@ -4,19 +4,50 @@ project(jucePluginEditorLib VERSION ${CMAKE_PROJECT_VERSION})
set(SOURCES
focusedParameter.cpp focusedParameter.h
focusedParameterTooltip.cpp focusedParameterTooltip.h
+ lcd.cpp lcd.h
+ led.cpp led.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)
target_compile_definitions(jucePluginEditorLib PRIVATE JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1)
+set_property(TARGET jucePluginEditorLib PROPERTY FOLDER "Gearmulator")
diff --git a/source/jucePluginEditorLib/focusedParameter.cpp b/source/jucePluginEditorLib/focusedParameter.cpp
@@ -91,8 +91,15 @@ namespace jucePluginEditorLib
const int part = props.contains("part") ? static_cast<int>(props["part"]) : static_cast<int>(m_controller.getCurrentPart());
- const auto* p = m_controller.getParameter(v, static_cast<uint8_t>(part));
+ auto* p = m_controller.getParameter(v, static_cast<uint8_t>(part));
+ // do not show soft knob parameter if the softknob is bound to another parameter
+ if(p && p->getDescription().isSoftKnob())
+ {
+ const auto* softKnob = m_controller.getSoftknob(p);
+ if(softKnob && softKnob->isBound())
+ p = softKnob->getTargetParameter();
+ }
if(!p)
{
if (m_focusedParameterName)
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,8 +1,10 @@
#include "focusedParameterTooltip.h"
+#include "juce_gui_basics/juce_gui_basics.h"
+
namespace jucePluginEditorLib
{
- FocusedParameterTooltip::FocusedParameterTooltip(juce::Label* _label) : m_label(_label)
+ FocusedParameterTooltip::FocusedParameterTooltip(juce::Label* _label) : m_label(_label), m_defaultWidth(_label ? _label->getWidth() : 0)
{
setVisible(false);
}
@@ -33,8 +35,14 @@ namespace jucePluginEditorLib
parent = parent->getParentComponent();
}
- x += (_component->getWidth()>>1) - (m_label->getWidth()>>1);
+ int labelWidth = m_defaultWidth;
+
+ if(_value.length() > 3)
+ labelWidth += (m_label->getHeight()>>1) * (_value.length() - 3);
+
+ x += (_component->getWidth()>>1) - (labelWidth>>1);
y += _component->getHeight() + (m_label->getHeight()>>1);
+
/*
// global to local of tooltip parent
parent = m_label->getParentComponent();
@@ -50,6 +58,7 @@ namespace jucePluginEditorLib
y += static_cast<int>(m_label->getProperties()["offsetY"]);
m_label->setTopLeftPosition(x,y);
+ m_label->setSize(labelWidth, m_label->getHeight());
m_label->setText(_value, juce::dontSendNotification);
m_label->setVisible(true);
m_label->toFront(false);
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
{
@@ -15,5 +20,6 @@ namespace jucePluginEditorLib
private:
juce::Label* m_label = nullptr;
+ int m_defaultWidth;
};
}
diff --git a/source/jucePluginEditorLib/lcd.cpp b/source/jucePluginEditorLib/lcd.cpp
@@ -0,0 +1,189 @@
+#include "lcd.h"
+
+// LCD simulation
+
+namespace
+{
+ constexpr int g_pixelsPerCharW = 5;
+ constexpr int g_pixelsPerCharH = 8;
+
+ constexpr float g_pixelSpacingAdjust = 3.0f; // 1.0f = 100% as on the hardware, but it looks better on screen if it's a bit more
+
+ constexpr float g_pixelSpacingW = 0.05f * g_pixelSpacingAdjust;
+ constexpr float g_pixelSizeW = 0.6f;
+ constexpr float g_charSpacingW = 0.4f;
+
+ constexpr float g_charSizeW = g_pixelsPerCharW * g_pixelSizeW + g_pixelSpacingW * (g_pixelsPerCharW - 1);
+ constexpr float g_pixelStrideW = g_pixelSizeW + g_pixelSpacingW;
+ constexpr float g_charStrideW = g_charSizeW + g_charSpacingW;
+
+ constexpr float g_pixelSpacingH = 0.05f * g_pixelSpacingAdjust;
+ constexpr float g_pixelSizeH = 0.65f;
+ constexpr float g_charSpacingH = 0.4f;
+
+ constexpr float g_charSizeH = g_pixelsPerCharH * g_pixelSizeH + g_pixelSpacingH * (g_pixelsPerCharH - 1);
+ constexpr float g_pixelStrideH = g_pixelSizeH + g_pixelSpacingH;
+ constexpr float g_charStrideH = g_charSizeH + g_charSpacingH;
+}
+
+namespace jucePluginEditorLib
+{
+Lcd::Lcd(juce::Component& _parent, const uint32_t _width, const uint32_t _height)
+ : Button("LCDButton"),
+ m_parent(_parent),
+ m_scaleW(static_cast<float>(_parent.getWidth()) / (static_cast<float>(_width) * g_charSizeW + g_charSpacingW * (static_cast<float>(_width) - 1))),
+ m_scaleH(static_cast<float>(_parent.getHeight()) / (static_cast<float>(_height) * g_charSizeH + g_charSpacingH * (static_cast<float>(_height) - 1))),
+ m_width(_width),
+ m_height(_height)
+{
+ setSize(_parent.getWidth(), _parent.getHeight());
+
+ {
+ const std::string bgColor = _parent.getProperties().getWithDefault("lcdBackgroundColor", juce::String()).toString().toStdString();
+ if (bgColor.size() == 6)
+ m_charBgColor = strtol(bgColor.c_str(), nullptr, 16) | 0xff000000;
+ }
+
+ {
+ const std::string color = _parent.getProperties().getWithDefault("lcdTextColor", juce::String()).toString().toStdString();
+ if (color.size() == 6)
+ m_charColor = strtol(color.c_str(), nullptr, 16) | 0xff000000;
+ }
+
+ m_text.resize(_width * _height, 255); // block character
+ m_overrideText.resize(_width * _height, 0);
+
+ m_cgData.fill({0});
+
+ onClick = [&]()
+ {
+ onClicked();
+ };
+
+ setEnabled(true);
+}
+
+Lcd::~Lcd() = default;
+
+void Lcd::setText(const std::vector<uint8_t>& _text)
+{
+ if (m_text == _text)
+ return;
+
+ m_text = _text;
+
+ repaint();
+}
+
+void Lcd::setCgRam(const std::array<uint8_t, 64>& _data)
+{
+ for (uint8_t i=0; i<static_cast<uint8_t>(m_cgData.size()); ++i)
+ {
+ std::array<uint8_t, 8> c{};
+ memcpy(c.data(), &_data[i*8], 8);
+
+ if (c != m_cgData[i])
+ {
+ m_cgData[i] = c;
+ m_characterPaths[i] = createPath(i);
+ }
+ }
+
+ repaint();
+}
+
+void Lcd::postConstruct()
+{
+ for (uint32_t i=16; i<256; ++i)
+ m_characterPaths[i] = createPath(static_cast<uint8_t>(i));
+
+ m_parent.addAndMakeVisible(this);
+}
+
+void Lcd::paint(juce::Graphics& _g)
+{
+ const auto& text = m_overrideText[0] ? m_overrideText : m_text;
+
+ uint32_t charIdx = 0;
+
+ for (uint32_t y=0; y < m_height; ++y)
+ {
+ const auto ty = static_cast<float>(y) * g_charStrideH * m_scaleH;
+
+ for (uint32_t x = 0; x < m_width; ++x, ++charIdx)
+ {
+ const auto tx = static_cast<float>(x) * g_charStrideW * m_scaleW;
+
+ const auto t = juce::AffineTransform::translation(tx, ty);
+
+ const auto c = text[charIdx];
+ const auto& p = m_characterPaths[c];
+
+ if (m_charBgColor)
+ {
+ _g.setColour(juce::Colour(m_charBgColor));
+ _g.fillPath(m_characterPaths[255], t);
+ }
+ _g.setColour(juce::Colour(m_charColor));
+ _g.fillPath(p, t);
+ }
+ }
+}
+
+juce::Path Lcd::createPath(const uint8_t _character) const
+{
+ const auto* data = _character < m_cgData.size() ? m_cgData[_character].data() : getCharacterData(_character);
+
+ juce::Path path;
+
+ const auto h = g_pixelSizeH * m_scaleH;
+ const auto w = g_pixelSizeW * m_scaleW;
+
+ for (auto y=0; y<8; ++y)
+ {
+ const auto y0 = static_cast<float>(y) * g_pixelStrideH * m_scaleH;
+
+ for (auto x=0; x<=4; ++x)
+ {
+ const auto bit = 4-x;
+
+ const auto set = data[y] & (1<<bit);
+
+ if(!set)
+ continue;
+
+ const auto x0 = static_cast<float>(x) * g_pixelStrideW * m_scaleW;
+
+ path.addRectangle(x0, y0, w, h);
+ }
+ }
+
+ return path;
+}
+
+void Lcd::onClicked()
+{
+ if(isTimerRunning())
+ return;
+
+ std::vector<std::vector<uint8_t>> lines;
+ getOverrideText(lines);
+
+ for (auto& c : m_overrideText)
+ c = ' ';
+
+ for(size_t y=0; y<std::min(lines.size(), static_cast<size_t>(m_height)); ++y)
+ {
+ memcpy(&m_overrideText[m_width*y], lines[y].data(), std::min(lines[y].size(), static_cast<size_t>(m_width)));
+ }
+
+ startTimer(3000);
+}
+
+void Lcd::timerCallback()
+{
+ stopTimer();
+ m_overrideText[0] = 0;
+ repaint();
+}
+}
+\ No newline at end of file
diff --git a/source/jucePluginEditorLib/lcd.h b/source/jucePluginEditorLib/lcd.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "juce_gui_basics/juce_gui_basics.h"
+
+namespace jucePluginEditorLib
+{
+ class Lcd : public juce::Button, juce::Timer
+ {
+ public:
+ explicit Lcd(Component& _parent, uint32_t _width, uint32_t _height);
+ ~Lcd() override;
+
+ void setText(const std::vector<uint8_t> &_text);
+ void setCgRam(const std::array<uint8_t, 64> &_data);
+
+ protected:
+ void postConstruct();
+
+ private:
+ void paintButton(juce::Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override {}
+ void paint(juce::Graphics& _g) override;
+ juce::Path createPath(uint8_t _character) const;
+ void onClicked();
+ void timerCallback() override;
+
+ virtual bool getOverrideText(std::vector<std::vector<uint8_t>>& _lines) = 0;
+ virtual const uint8_t* getCharacterData(uint8_t _character) const = 0;
+
+ private:
+ Component& m_parent;
+
+ std::array<juce::Path, 256> m_characterPaths;
+
+ const float m_scaleW;
+ const float m_scaleH;
+
+ const uint32_t m_width;
+ const uint32_t m_height;
+ std::vector<uint8_t> m_overrideText;
+ std::vector<uint8_t> m_text;
+ std::array<std::array<uint8_t, 8>, 8> m_cgData{{{0}}};
+ uint32_t m_charBgColor = 0xff000000;
+ uint32_t m_charColor = 0xff000000;
+ };
+}
diff --git a/source/jucePluginEditorLib/led.cpp b/source/jucePluginEditorLib/led.cpp
@@ -0,0 +1,49 @@
+#include "led.h"
+
+#include "juce_gui_basics/juce_gui_basics.h"
+
+namespace jucePluginEditorLib
+{
+ Led::Led(juce::Component* _targetAlpha, juce::Component* _targetInvAlpha)
+ : m_targetAlpha(_targetAlpha)
+ , m_targetInvAlpha(_targetInvAlpha)
+ {
+ }
+
+ void Led::setValue(const float _v)
+ {
+ if(m_value == _v) // NOLINT(clang-diagnostic-float-equal)
+ return;
+
+ m_value = _v;
+
+ repaint();
+ }
+
+ void Led::timerCallback()
+ {
+ setValue(m_sourceCallback());
+ }
+
+ void Led::setSourceCallback(SourceCallback&& _func, const int _timerMilliseconds/* = 1000/60*/)
+ {
+ m_sourceCallback = std::move(_func);
+
+ if(m_sourceCallback)
+ startTimer(_timerMilliseconds);
+ else
+ stopTimer();
+ }
+
+ void Led::repaint() const
+ {
+ m_targetAlpha->setOpaque(false);
+ m_targetAlpha->setAlpha(m_value);
+
+ if(!m_targetInvAlpha)
+ return;
+
+ m_targetInvAlpha->setOpaque(false);
+ m_targetInvAlpha->setAlpha(1.0f - m_value);
+ }
+}
diff --git a/source/jucePluginEditorLib/led.h b/source/jucePluginEditorLib/led.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include "juce_events/juce_events.h"
+
+namespace juce
+{
+ class Component;
+}
+
+namespace jucePluginEditorLib
+{
+ class Led : public juce::Timer
+ {
+ public:
+ using SourceCallback = std::function<float()>;
+
+ Led(juce::Component* _targetAlpha, juce::Component* _targetInvAlpha = nullptr);
+
+ void setValue(float _v);
+
+ void timerCallback() override;
+
+ void setSourceCallback(SourceCallback&& _func, int _timerMilliseconds = 1000/60);
+
+ private:
+ void repaint() const;
+
+ juce::Component* m_targetAlpha;
+ juce::Component* m_targetInvAlpha;
+
+ float m_value = -1.0f;
+
+ SourceCallback m_sourceCallback;
+ };
+}
diff --git a/source/jucePluginEditorLib/midiPorts.cpp b/source/jucePluginEditorLib/midiPorts.cpp
@@ -75,6 +75,13 @@ namespace jucePluginEditorLib
delete deviceManager;
}
+ void MidiPorts::showMidiPortFailedMessage(const char* _name) const
+ {
+ juce::NativeMessageBox::showMessageBoxAsync(juce::MessageBoxIconType::WarningIcon, m_processor.getProperties().name,
+ std::string("Failed to open Midi ") + _name + ".\n\n"
+ "Make sure that the device is not already in use by another program.", nullptr, juce::ModalCallbackFunction::create([](int){}));
+ }
+
void MidiPorts::updateMidiInput(int index)
{
const auto list = juce::MidiInput::getAvailableDevices();
@@ -99,6 +106,7 @@ namespace jucePluginEditorLib
if (!m_processor.setMidiInput(newInput.identifier))
{
+ showMidiPortFailedMessage("Input");
m_midiIn->setSelectedItemIndex(0, juce::dontSendNotification);
m_lastInputIndex = 0;
return;
@@ -130,6 +138,7 @@ namespace jucePluginEditorLib
const auto newOutput = list[index];
if (!m_processor.setMidiOutput(newOutput.identifier))
{
+ showMidiPortFailedMessage("Output");
m_midiOut->setSelectedItemIndex(0, juce::dontSendNotification);
m_lastOutputIndex = 0;
return;
diff --git a/source/jucePluginEditorLib/midiPorts.h b/source/jucePluginEditorLib/midiPorts.h
@@ -31,6 +31,7 @@ namespace jucePluginEditorLib
int m_lastInputIndex = 0;
int m_lastOutputIndex = 0;
+ void showMidiPortFailedMessage(const char* _name) const;
void updateMidiInput(int _index);
void updateMidiOutput(int _index);
};
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,215 @@
+#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::LocalStorage:
+ return _ds.name;
+ case pluginLib::patchDB::SourceType::File:
+ case pluginLib::patchDB::SourceType::Folder:
+ {
+ auto n = _ds.name;
+ const auto idxA = n.find_first_of("\\/");
+ const auto idxB = n.find_last_of("\\/");
+ if(idxA != std::string::npos && idxB != std::string::npos && (idxB - idxA) > 1)
+ {
+ return n.substr(0, idxA+1) + ".." + n.substr(idxB);
+ }
+ return n;
+ }
+ 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, const SavePatchDesc* _savePatchDesc/* = nullptr*/)
+ {
+ 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
+ const int part = _savePatchDesc ? _savePatchDesc->getPart() : -1;
+
+ getPatchManager().copyPatchesToLocalStorage(m_dataSource, _patches, part);
+#endif
+ }
+ }
+
+ void DatasourceTreeItem::itemClicked(const juce::MouseEvent& _mouseEvent)
+ {
+ if(!_mouseEvent.mods.isPopupMenu())
+ {
+ TreeItem::itemClicked(_mouseEvent);
+ 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, const SavePatchDesc* _savePatchDesc = nullptr) 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,74 @@
+#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_editorLabel->getCurrentTextEditor()->addListener(this);
+
+ 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();
+ }
+ juce::Label::Listener::editorHidden(_label, _textEditor);
+ }
+
+ void Editable::labelTextChanged(juce::Label* _label)
+ {
+ if(m_editorLabel)
+ m_editorLabel->repaint();
+ }
+
+ void Editable::textEditorTextChanged(juce::TextEditor& _textEditor)
+ {
+ if(m_editorLabel)
+ m_editorLabel->repaint();
+ _textEditor.repaint();
+ }
+
+ 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,32 @@
+#pragma once
+
+#include <functional>
+#include <string>
+
+#include "juce_gui_basics/juce_gui_basics.h"
+
+namespace jucePluginEditorLib::patchManager
+{
+ class Editable : juce::Label::Listener, juce::TextEditor::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;
+ void textEditorTextChanged (juce::TextEditor&) 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,353 @@
+#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);
+ }
+
+ setDeselectonSecondClick(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);
+ return;
+ }
+
+ juce::PopupMenu menu;
+
+ const auto tagType = toTagType(m_type);
+
+ if(m_type == GroupType::DataSources)
+ {
+ menu.addItem("Add Folders...", [this]
+ {
+ juce::FileChooser fc("Select Folders");
+
+ if(fc.showDialog(
+ juce::FileBrowserComponent::openMode |
+ juce::FileBrowserComponent::canSelectDirectories |
+ juce::FileBrowserComponent::canSelectMultipleItems
+ , nullptr))
+ {
+ for (const auto& r : fc.getResults())
+ {
+ const auto result = r.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 Files...", [this]
+ {
+ juce::FileChooser fc("Select Files");
+ if(fc.showDialog(
+ juce::FileBrowserComponent::openMode |
+ juce::FileBrowserComponent::canSelectFiles |
+ juce::FileBrowserComponent::canSelectMultipleItems,
+ nullptr))
+ {
+ for (const auto&r : fc.getResults())
+ {
+ const auto result = r.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 });
+
+ validateParent(_dataSource, item);
+
+ return item;
+ }
+
+ TagTreeItem* GroupTreeItem::createSubItem(const std::string& _tag)
+ {
+ auto item = new TagTreeItem(getPatchManager(), m_type, _tag);
+
+ validateParent(item);
+
+ m_itemsByTag.insert({ _tag, item });
+
+ item->onParentSearchChanged(getParentSearchRequest());
+
+ 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,217 @@
+#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)
+ {
+ if (!_patch)
+ {
+ clear();
+ return;
+ }
+
+ if(_patch != m_patch)
+ {
+ m_patchManager.cancelSearch(m_searchHandle);
+
+ pluginLib::patchDB::SearchRequest req;
+ req.patch = _patch;
+
+ m_searchHandle = m_patchManager.search(std::move(req));
+
+ m_patch = _patch;
+ }
+
+ 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()
+ {
+ m_patch.reset();
+ m_patchManager.cancelSearch(m_searchHandle);
+ m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle;
+
+ 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));
+ }
+
+ void Info::processDirty(const pluginLib::patchDB::Dirty& _dirty)
+ {
+ if(_dirty.searches.find(m_searchHandle) == _dirty.searches.end())
+ return;
+
+ setPatch(m_patch);
+ }
+
+ 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,52 @@
+#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);
+ void clear();
+
+ static std::string toText(const pluginLib::patchDB::Tags& _tags);
+ static std::string toText(const pluginLib::patchDB::DataSourceNodePtr& _source);
+
+ void paint(juce::Graphics& g) override;
+
+ void processDirty(const pluginLib::patchDB::Dirty& _dirty);
+
+ 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;
+
+ pluginLib::patchDB::PatchPtr m_patch;
+ pluginLib::patchDB::SearchHandle m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle;
+ };
+}
diff --git a/source/jucePluginEditorLib/patchmanager/list.cpp b/source/jucePluginEditorLib/patchmanager/list.cpp
@@ -0,0 +1,590 @@
+#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::clear()
+ {
+ m_search.reset();
+ m_patches.clear();
+ m_filteredPatches.clear();
+ updateContent();
+ getPatchManager().setListStatus(0, 0);
+ }
+
+ 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,115 @@
+#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 clear();
+
+ 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,202 @@
+#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
+ const auto part = savePatchDesc->getPart();
+ pm.copyPatchesTo(source, {patch}, row, [this, part](const std::vector<pluginLib::patchDB::PatchPtr>& _patches)
+ {
+ juce::MessageManager::callAsync([this, part, _patches]
+ {
+ m_list.getPatchManager().setSelectedPatch(part, _patches.front());
+ });
+ });
+#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,767 @@
+#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);
+ }
+
+ m_searchList->setTextToShowWhenEmpty("Search...", m_searchList->findColour(juce::TextEditor::textColourId).withAlpha(0.5f));
+ m_searchTreeDS->setTextToShowWhenEmpty("Search...", m_searchTreeDS->findColour(juce::TextEditor::textColourId).withAlpha(0.5f));
+ m_searchTreeTags->setTextToShowWhenEmpty("Search...", m_searchTreeTags->findColour(juce::TextEditor::textColourId).withAlpha(0.5f));
+
+ 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());
+
+ m_info->processDirty(dirty);
+
+ 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;
+ }
+
+ pluginLib::patchDB::DataSourceNodePtr PatchManager::getSelectedDataSource() const
+ {
+ const auto* item = dynamic_cast<DatasourceTreeItem*>(m_treeDS->getSelectedItem(0));
+ if(!item)
+ return {};
+ return item->getDataSource();
+ }
+
+ 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;
+ }
+
+ void PatchManager::copyPatchesToLocalStorage(const pluginLib::patchDB::DataSourceNodePtr& _ds, const std::vector<pluginLib::patchDB::PatchPtr>& _patches, int _part)
+ {
+ copyPatchesTo(_ds, _patches, -1, [this, _part](const std::vector<pluginLib::patchDB::PatchPtr>& _savedPatches)
+ {
+ if(_part == -1)
+ return;
+
+ juce::MessageManager::callAsync([this, _part, _savedPatches]
+ {
+ setSelectedPatch(_part, _savedPatches.front());
+ });
+ });
+ }
+
+ uint32_t PatchManager::createSaveMenuEntries(juce::PopupMenu& _menu, uint32_t _part)
+ {
+ const auto& state = getState();
+ const auto key = state.getPatch(_part);
+
+ uint32_t countAdded = 0;
+
+ if(key.isValid() && key.source->type == pluginLib::patchDB::SourceType::LocalStorage)
+ {
+ // the key that is stored in the state might not contain patches, find the real data source in the DB
+ const auto ds = getDataSource(*key.source);
+
+ if(ds)
+ {
+ if(const auto p = ds->getPatch(key))
+ {
+ if(*p == key)
+ {
+ ++countAdded;
+ _menu.addItem("Overwrite patch '" + p->getName() + "' in user bank '" + ds->name + "'", true, false, [this, p, _part]
+ {
+ const auto newPatch = requestPatchForPart(_part);
+ if(newPatch)
+ {
+ replacePatch(p, newPatch);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ if(const auto ds = getSelectedDataSource())
+ {
+ if(ds->type == pluginLib::patchDB::SourceType::LocalStorage)
+ {
+ ++countAdded;
+ _menu.addItem("Add to user bank '" + ds->name + "'", true, false, [this, ds, _part]
+ {
+ const auto newPatch = requestPatchForPart(_part);
+
+ if(!newPatch)
+ return;
+
+ copyPatchesToLocalStorage(ds, {newPatch}, static_cast<int>(_part));
+ });
+ }
+ }
+
+ return countAdded;
+ }
+
+ 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;
+
+ const 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,134 @@
+#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;
+ pluginLib::patchDB::DataSourceNodePtr getSelectedDataSource() const;
+
+ const State& getState() const { return m_state; }
+
+ bool setSelectedPatch(uint32_t _part, const pluginLib::patchDB::PatchPtr& _patch);
+ bool setSelectedPatch(uint32_t _part, const pluginLib::patchDB::PatchKey& _patch);
+
+ void copyPatchesToLocalStorage(const pluginLib::patchDB::DataSourceNodePtr& _ds, const std::vector<pluginLib::patchDB::PatchPtr>& _patches, int _part);
+
+ uint32_t createSaveMenuEntries(juce::PopupMenu& _menu, uint32_t _part);
+
+ private:
+ bool selectPatch(uint32_t _part, int _offset);
+
+ 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(*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,31 @@
+#include "tagstree.h"
+
+#include "grouptreeitem.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");
+ getItem(GroupType::Categories)->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,123 @@
+#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));
+ }
+
+ setDeselectonSecondClick(true);
+ }
+
+ 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 SavePatchDesc*/* _savePatchDesc = nullptr*/)
+ {
+ 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())
+ {
+ TreeItem::itemClicked(_mouseEvent);
+ return;
+ }
+
+ 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, const SavePatchDesc* _savePatchDesc = nullptr) 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,316 @@
+#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)
+ {
+ if(_isNowSelected && m_forceDeselect)
+ {
+ m_forceDeselect = false;
+
+ juce::MessageManager::callAsync([this]()
+ {
+ setSelected(false, false);
+ });
+ return;
+ }
+
+ m_selectedWasChanged = true;
+
+ 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}, desc);
+ }
+ }
+
+ 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()));
+ }
+
+ const pluginLib::patchDB::SearchRequest& TreeItem::getParentSearchRequest() const
+ {
+ return m_parentSearchRequest;
+ }
+
+ 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 = juce::String::fromUTF8(m_text.c_str());
+ _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::itemClicked(const juce::MouseEvent& _mouseEvent)
+ {
+ if(_mouseEvent.mods.isPopupMenu())
+ {
+ TreeViewItem::itemClicked(_mouseEvent);
+ return;
+ }
+
+ if(!m_deselectOnSecondClick)
+ {
+ TreeViewItem::itemClicked(_mouseEvent);
+ return;
+ }
+
+ // we have the (for Juce) overly complex task to deselect a tree item on left click
+ // Juce does not let us though, this click is sent on mouse down and it reselects the item
+ // again on mouse up.
+ const auto selectedWasChanged = m_selectedWasChanged;
+ m_selectedWasChanged = false;
+
+ if(!selectedWasChanged && isSelected() && getOwnerView()->isMultiSelectEnabled())
+ {
+ m_forceDeselect = true;
+ setSelected(false, false);
+ }
+ }
+
+ 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, const SavePatchDesc* _savePatchDesc/* = nullptr*/)
+ {
+ for (const auto& patch : _patches)
+ patchDropped(patch);
+ }
+}
diff --git a/source/jucePluginEditorLib/patchmanager/treeitem.h b/source/jucePluginEditorLib/patchmanager/treeitem.h
@@ -0,0 +1,105 @@
+#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, const SavePatchDesc* _savePatchDesc = nullptr);
+
+ 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; }
+
+ void itemClicked(const juce::MouseEvent&) override;
+
+ void setDeselectonSecondClick(const bool _deselect) { m_deselectOnSecondClick = _deselect; }
+
+ 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) {}
+ const pluginLib::patchDB::SearchRequest& getParentSearchRequest() const;
+
+ 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;
+
+ bool m_deselectOnSecondClick = false;
+ bool m_selectedWasChanged = false;
+ bool m_forceDeselect = false;
+ };
+}
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)
@@ -15,8 +17,11 @@ namespace jucePluginEditorLib
, m_binding(_binding)
, m_skinFolder(std::move(_skinFolder))
{
+ showDisclaimer();
}
+ 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 +29,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 +49,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 +73,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 +105,7 @@ namespace jucePluginEditorLib
fclose(hFile);
return true;
}
+#endif
std::string Editor::createValidFilename(FileType& _type, const juce::File& _file)
{
@@ -110,6 +121,69 @@ 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);
+ }
+
+ void Editor::showDisclaimer() const
+ {
+ if(!m_processor.getConfig().getBoolValue("disclaimerSeen", false))
+ {
+ const juce::MessageBoxOptions options = juce::MessageBoxOptions::makeOptionsOk(juce::MessageBoxIconType::WarningIcon, m_processor.getProperties().name,
+ "It is the sole responsibility of the user to operate this emulator within the bounds of all applicable laws.\n\n"
+
+ "Usage of emulators in conjunction with ROM images you are not legally entitled to own is forbidden by copyright law.\n\n"
+
+ "If you are not legally entitled to use this emulator please discontinue usage immediately.\n\n",
+
+ "I Agree"
+ );
+
+ juce::NativeMessageBox::showAsync(options, [this](int)
+ {
+ m_processor.getConfig().setValue("disclaimerSeen", true);
+ });
+ }
+ }
+
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,53 @@ 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;
+
+ void showDisclaimer() const;
+
private:
const char* getResourceByFilename(const std::string& _name, uint32_t& _dataSize) override;
int getParameterIndexByName(const std::string& _name) override;
@@ -45,5 +76,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
@@ -10,17 +10,18 @@ namespace jucePluginEditorLib
{
PluginEditorState::PluginEditorState(Processor& _processor, pluginLib::Controller& _controller, std::vector<Skin> _includedSkins)
: m_processor(_processor), m_parameterBinding(_controller), m_includedSkins(std::move(_includedSkins))
+ , m_skinFolderName("skins_" + _processor.getProperties().name)
{
}
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 +31,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 +56,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 +84,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,46 +103,59 @@ 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]);
}
}
-void PluginEditorState::setGuiScale(int _scale) const
+void PluginEditorState::setGuiScale(const int _scale) const
{
if(evSetGuiScale)
evSetGuiScale(_scale);
}
+genericUI::Editor* PluginEditorState::getEditor() const
+{
+ return static_cast<genericUI::Editor*>(m_editor.get());
+}
+
void PluginEditorState::openMenu()
{
const auto& config = m_processor.getConfig();
- const auto scale = config.getIntValue("scale", 100);
+ const auto scale = juce::roundToInt(config.getDoubleValue("scale", 100));
juce::PopupMenu menu;
juce::PopupMenu skinMenu;
- auto addSkinEntry = [this, &skinMenu](const PluginEditorState::Skin& _skin)
+ bool loadedSkinIsPartOfList = false;
+
+ auto addSkinEntry = [this, &skinMenu, &loadedSkinIsPartOfList](const Skin& _skin)
{
- skinMenu.addItem(_skin.displayName, true, _skin == getCurrentSkin(),[this, _skin] {loadSkin(_skin);});
+ const auto isCurrent = _skin == getCurrentSkin();
+ if(isCurrent)
+ loadedSkinIsPartOfList = true;
+ skinMenu.addItem(_skin.displayName, true, isCurrent,[this, _skin] {loadSkin(_skin);});
};
- for (const auto & skin : PluginEditorState::getIncludedSkins())
+ for (const auto & skin : getIncludedSkins())
addSkinEntry(skin);
bool haveSkinsOnDisk = false;
@@ -127,7 +164,7 @@ void PluginEditorState::openMenu()
const auto modulePath = synthLib::getModulePath();
std::vector<std::string> entries;
- synthLib::getDirectoryEntries(entries, modulePath + "skins");
+ synthLib::getDirectoryEntries(entries, modulePath + m_skinFolderName);
for (const auto& entry : entries)
{
@@ -155,13 +192,16 @@ void PluginEditorState::openMenu()
}
}
- if(m_virusEditor && m_currentSkin.folder.empty())
+ if(!loadedSkinIsPartOfList)
+ addSkinEntry(getCurrentSkin());
+
+ if(m_editor && m_currentSkin.folder.empty() || m_currentSkin.folder.find(m_skinFolderName) == std::string::npos)
{
- auto* editor = m_virusEditor.get();
+ auto* editor = m_editor.get();
if(editor)
{
skinMenu.addSeparator();
- skinMenu.addItem("Export current skin to 'skins' folder on disk", true, false, [this]{exportCurrentSkin();});
+ skinMenu.addItem("Export current skin to '" + m_skinFolderName + "' folder on disk", true, false, [this]{exportCurrentSkin();});
}
}
@@ -178,13 +218,22 @@ void PluginEditorState::openMenu()
scaleMenu.addItem("250%", true, scale == 250, [this] { setGuiScale(250); });
scaleMenu.addItem("300%", true, scale == 300, [this] { setGuiScale(300); });
+ auto adjustLatency = [this](const int _blocks)
+ {
+ m_processor.setLatencyBlocks(_blocks);
+
+ juce::NativeMessageBox::showMessageBox(juce::AlertWindow::WarningIcon, "Warning",
+ "Most hosts cannot handle if a plugin changes its latency while being in use.\n"
+ "It is advised to save, close & reopen the project to prevent synchronization issues.");
+ };
+
const auto latency = m_processor.getPlugin().getLatencyBlocks();
juce::PopupMenu latencyMenu;
- latencyMenu.addItem("0 (DAW will report proper CPU usage)", true, latency == 0, [this] { m_processor.setLatencyBlocks(0); });
- latencyMenu.addItem("1 (default)", true, latency == 1, [this] { m_processor.setLatencyBlocks(1); });
- latencyMenu.addItem("2", true, latency == 2, [this] { m_processor.setLatencyBlocks(2); });
- latencyMenu.addItem("4", true, latency == 4, [this] { m_processor.setLatencyBlocks(4); });
- latencyMenu.addItem("8", true, latency == 8, [this] { m_processor.setLatencyBlocks(8); });
+ latencyMenu.addItem("0 (DAW will report proper CPU usage)", true, latency == 0, [this, adjustLatency] { adjustLatency(0); });
+ latencyMenu.addItem("1 (default)", true, latency == 1, [this, adjustLatency] { adjustLatency(1); });
+ latencyMenu.addItem("2", true, latency == 2, [this, adjustLatency] { adjustLatency(2); });
+ latencyMenu.addItem("4", true, latency == 4, [this, adjustLatency] { adjustLatency(4); });
+ latencyMenu.addItem("8", true, latency == 8, [this, adjustLatency] { adjustLatency(8); });
menu.addSubMenu("GUI Skin", skinMenu);
menu.addSubMenu("GUI Scale", scaleMenu);
@@ -192,24 +241,96 @@ 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;
- const auto res = editor->exportToFolder(synthLib::getModulePath() + "skins/");
+ const auto res = editor->exportToFolder(synthLib::getModulePath() + m_skinFolderName + '/');
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,13 @@ 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;
+ std::string m_skinFolderName;
};
}
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
{
@@ -18,7 +20,7 @@ EditorWindow::EditorWindow(juce::AudioProcessor& _p, PluginEditorState& _s, juce
m_state.evSetGuiScale = [&](const int _scale)
{
if(getNumChildComponents() > 0)
- setGuiScale(getChildComponent(0), _scale);
+ setGuiScale(getChildComponent(0), static_cast<float>(_scale));
};
m_state.enableBindings();
@@ -36,18 +38,55 @@ EditorWindow::~EditorWindow()
setUiRoot(nullptr);
}
-void EditorWindow::setGuiScale(juce::Component* _comp, int percent)
+void EditorWindow::resized()
{
- if(!_comp)
+ AudioProcessorEditor::resized();
+
+ auto* comp = getChildComponent(0);
+ if(!comp)
return;
- const auto s = static_cast<float>(percent)/100.0f * m_state.getRootScale();
- _comp->setTransform(juce::AffineTransform::scale(s,s));
+ if(!m_state.getWidth() || !m_state.getHeight())
+ return;
+
+ const auto targetW = getWidth();
+ const auto targetH = getHeight();
+
+ const auto scaleX = static_cast<float>(getWidth()) / static_cast<float>(m_state.getWidth());
+ const auto scaleY = static_cast<float>(getHeight()) / static_cast<float>(m_state.getHeight());
+
+ const auto scale = std::min(scaleX, scaleY);
+
+ const auto w = scale * static_cast<float>(m_state.getWidth());
+ const auto h = scale * static_cast<float>(m_state.getHeight());
- setSize(static_cast<int>(m_state.getWidth() * s), static_cast<int>(m_state.getHeight() * s));
+ const auto offX = (static_cast<float>(targetW) - w) * 0.5f;
+ const auto offY = (static_cast<float>(targetH) - h) * 0.5f;
+ comp->setTransform(juce::AffineTransform::scale(scale,scale).translated(offX, offY));
+
+ const auto percent = 100.f * scale / m_state.getRootScale();
m_config.setValue("scale", percent);
m_config.saveIfNeeded();
+
+ AudioProcessorEditor::resized();
+}
+
+void EditorWindow::setGuiScale(juce::Component* _comp, const float _percent)
+{
+ if(!m_state.getWidth() || !m_state.getHeight())
+ return;
+
+ const auto s = _percent / 100.0f * m_state.getRootScale();
+ _comp->setTransform(juce::AffineTransform::scale(s,s));
+
+ const auto w = static_cast<int>(static_cast<float>(m_state.getWidth()) * s);
+ const auto h = static_cast<int>(static_cast<float>(m_state.getHeight()) * s);
+
+ setSize(w, h);
+
+ m_config.setValue("scale", _percent);
+ m_config.saveIfNeeded();
}
void EditorWindow::setUiRoot(juce::Component* _component)
@@ -57,10 +96,22 @@ void EditorWindow::setUiRoot(juce::Component* _component)
if(!_component)
return;
- const auto scale = m_config.getIntValue("scale", 100);
+ if(!m_state.getWidth() || !m_state.getHeight())
+ return;
+
+ const auto scale = static_cast<float>(m_config.getDoubleValue("scale", 100));
- setGuiScale(_component, scale);
addAndMakeVisible(_component);
+ setGuiScale(_component, scale);
+
+ m_sizeConstrainer.setMinimumSize(m_state.getWidth() / 10, m_state.getHeight() / 10);
+ m_sizeConstrainer.setMaximumSize(m_state.getWidth() * 4, m_state.getHeight() * 4);
+
+ m_sizeConstrainer.setFixedAspectRatio(static_cast<double>(m_state.getWidth()) / static_cast<double>(m_state.getHeight()));
+
+ setResizable(true, true);
+ setConstrainer(nullptr);
+ setConstrainer(&m_sizeConstrainer);
}
void EditorWindow::mouseDown(const juce::MouseEvent& event)
@@ -71,12 +122,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/pluginEditorWindow.h b/source/jucePluginEditorLib/pluginEditorWindow.h
@@ -2,8 +2,6 @@
#include <juce_audio_processors/juce_audio_processors.h>
-class AudioPluginAudioProcessor;
-
namespace jucePluginEditorLib
{
class PluginEditorState;
@@ -19,13 +17,17 @@ namespace jucePluginEditorLib
void paint(juce::Graphics& g) override {}
+ void resized() override;
+
private:
- void setGuiScale(juce::Component* _component, int percent);
+ void setGuiScale(juce::Component* _comp, float _percent);
void setUiRoot(juce::Component* _component);
PluginEditorState& m_state;
juce::PropertiesFile& m_config;
+ juce::ComponentBoundsConstrainer m_sizeConstrainer;
+
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(EditorWindow)
};
}
diff --git a/source/jucePluginEditorLib/pluginProcessor.cpp b/source/jucePluginEditorLib/pluginProcessor.cpp
@@ -1,13 +1,24 @@
#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)
+ Processor::Processor(const BusesProperties& _busesProperties, const juce::PropertiesFile::Options& _configOptions, const pluginLib::Processor::Properties& _properties)
+ : pluginLib::Processor(_busesProperties, _properties)
+ , m_configOptions(_configOptions)
, m_config(_configOptions)
{
}
+ Processor::~Processor()
+ {
+ assert(!m_editorState && "call destroyEditorState in destructor of derived class");
+ }
+
bool Processor::setLatencyBlocks(const uint32_t _blocks)
{
if(!pluginLib::Processor::setLatencyBlocks(_blocks))
@@ -23,4 +34,65 @@ 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::destroyEditorState()
+ {
+ m_editorState.reset();
+ }
+
+ void Processor::saveChunkData(synthLib::BinaryStream& s)
+ {
+ pluginLib::Processor::saveChunkData(s);
+
+ if(m_editorState)
+ {
+ m_editorStateData.clear();
+ m_editorState->getPerInstanceConfig(m_editorStateData);
+ }
+
+ if(!m_editorStateData.empty())
+ {
+ synthLib::ChunkWriter cw(s, "EDST", 1);
+ s.write(m_editorStateData);
+ }
+ }
+
+ bool Processor::loadCustomData(const std::vector<uint8_t>& _sourceBuffer)
+ {
+ // if there is no chunk in the data, but the data is not empty, it's an old non-Vavra chunk that only carries the editor state
+ if(!pluginLib::Processor::loadCustomData(_sourceBuffer))
+ m_editorStateData = _sourceBuffer;
+
+ if(m_editorState)
+ m_editorState->setPerInstanceConfig(m_editorStateData);
+
+ return true;
+ }
+
+ void Processor::loadChunkData(synthLib::ChunkReader& _cr)
+ {
+ pluginLib::Processor::loadChunkData(_cr);
+
+ _cr.add("EDST", 1, [this](synthLib::BinaryStream& _binaryStream, unsigned _version)
+ {
+ _binaryStream.read(m_editorStateData);
+ });
+ }
}
diff --git a/source/jucePluginEditorLib/pluginProcessor.h b/source/jucePluginEditorLib/pluginProcessor.h
@@ -4,18 +4,35 @@
namespace jucePluginEditorLib
{
+ class PluginEditorState;
+
class Processor : public pluginLib::Processor
{
public:
- Processor(const BusesProperties& _busesProperties, const juce::PropertiesFile::Options& _configOptions);
+ Processor(const BusesProperties& _busesProperties, const juce::PropertiesFile::Options& _configOptions, const pluginLib::Processor::Properties& _properties);
+ ~Processor() override;
+ 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 destroyEditorState();
+
+ void saveChunkData(synthLib::BinaryStream& s) override;
+ bool loadCustomData(const std::vector<uint8_t>& _sourceBuffer) override;
+ void loadChunkData(synthLib::ChunkReader& _cr) 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,14 +11,32 @@ set(SOURCES
parameterdescription.cpp parameterdescription.h
parameterdescriptions.cpp parameterdescriptions.h
parameterlink.cpp parameterlink.h
+ parameterregion.cpp parameterregion.h
processor.cpp processor.h
+ softknob.cpp softknob.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)
target_compile_definitions(jucePluginLib PRIVATE JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1)
+set_property(TARGET jucePluginLib PROPERTY FOLDER "Gearmulator")
diff --git a/source/jucePluginLib/controller.cpp b/source/jucePluginLib/controller.cpp
@@ -16,7 +16,12 @@ namespace pluginLib
Controller::Controller(pluginLib::Processor& _processor, const std::string& _parameterDescJson) : m_processor(_processor), m_descriptions(_parameterDescJson)
{
}
-
+
+ Controller::~Controller()
+ {
+ m_softKnobs.clear();
+ }
+
void Controller::registerParams(juce::AudioProcessor& _processor)
{
auto globalParams = std::make_unique<juce::AudioProcessorParameterGroup>("global", "Global", "|");
@@ -112,8 +117,29 @@ namespace pluginLib
}
_processor.addParameterGroup(std::move(group));
}
+
_processor.addParameterGroup(std::move(globalParams));
- }
+
+ // initialize all soft knobs for all parts
+ std::vector<size_t> softKnobs;
+
+ for (size_t i=0; i<m_descriptions.getDescriptions().size(); ++i)
+ {
+ const auto& desc = m_descriptions.getDescriptions()[i];
+ if(!desc.isSoftKnob())
+ continue;
+ softKnobs.push_back(i);
+ }
+
+ for(size_t part = 0; part<m_paramsByParamType.size(); ++part)
+ {
+ for (const auto& softKnobParam : softKnobs)
+ {
+ auto* sk = new SoftKnob(*this, static_cast<uint8_t>(part), static_cast<uint32_t>(softKnobParam));
+ m_softKnobs.insert({sk->getParameter(), std::unique_ptr<SoftKnob>(sk)});
+ }
+ }
+ }
void Controller::sendSysEx(const pluginLib::SysEx& msg) const
{
@@ -272,32 +298,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;
- if(!m->create(_sysex, _params, paramValues))
+ 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, _data, _values))
{
assert(false && "failed to create midi packet");
_sysex.clear();
@@ -306,6 +364,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 +379,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 +429,87 @@ 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::getLockedParameterNames() 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;
+ }
+
+ std::unordered_set<const Parameter*> Controller::getLockedParameters(const uint8_t _part) const
+ {
+ const auto paramNames = getLockedParameterNames();
+
+ std::unordered_set<const Parameter*> results;
+
+ for (const auto& paramName : paramNames)
+ {
+ const auto idx = getParameterIndexByName(paramName);
+ assert(idx != InvalidParameterIndex);
+ const auto* p = getParameter(idx, _part);
+ assert(p != nullptr);
+ results.insert(p);
+ }
+
+ return results;
+ }
+
+ bool Controller::isParameterLocked(const std::string& _name) const
+ {
+ const auto& regions = getLockedRegions();
+ for (const auto& region : regions)
+ {
+ const auto& it = m_descriptions.getRegions().find(region);
+ if(it == m_descriptions.getRegions().end())
+ continue;
+
+ if(it->second.containsParameter(_name))
+ return true;
+ }
+ return false;
+ }
+
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,14 @@
#include <string>
+#include "softknob.h"
+
+namespace juce
+{
+ class AudioProcessor;
+ class Value;
+}
+
namespace pluginLib
{
class Processor;
@@ -18,7 +26,7 @@ namespace pluginLib
static constexpr uint32_t InvalidParameterIndex = 0xffffffff;
explicit Controller(pluginLib::Processor& _processor, const std::string& _parameterDescJson);
- virtual ~Controller() = default;
+ virtual ~Controller();
virtual void sendParameterChange(const Parameter& _parameter, uint8_t _value) = 0;
@@ -30,9 +38,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 +62,20 @@ 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> getLockedParameterNames() const;
+ std::unordered_set<const Parameter*> getLockedParameters(uint8_t _part) const;
+ bool isParameterLocked(const std::string& _name) const;
+ const ParameterDescriptions& getParameterDescriptions() const { return m_descriptions; }
+
+ const SoftKnob* getSoftknob(const Parameter* _parameter) const
+ {
+ const auto it = m_softKnobs.find(_parameter);
+ return it->second.get();
+ }
protected:
virtual Parameter* createParameter(Controller& _controller, const Description& _desc, uint8_t _part, int _uid);
void registerParams(juce::AudioProcessor& _processor);
@@ -95,6 +123,7 @@ namespace pluginLib
std::mutex m_pluginMidiOutLock;
std::vector<synthLib::SMidiEvent> m_pluginMidiOut;
+ std::map<const Parameter*, std::unique_ptr<SoftKnob>> m_softKnobs;
protected:
// tries to find synth param in both internal and host
@@ -105,5 +134,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/event.h b/source/jucePluginLib/event.h
@@ -5,19 +5,46 @@
namespace pluginLib
{
+ template<typename ...Ts>
class Event
{
public:
- using Callback = std::function<void()>;
+ using ListenerId = size_t;
+ using Callback = std::function<void(const Ts&...)>;
+
+ static constexpr ListenerId InvalidListenerId = ~0;
Event() = default;
- void addListener(size_t _id, const Callback& _callback)
+ ListenerId addListener(const Callback& _callback)
+ {
+ ListenerId id;
+
+ if(m_listeners.empty())
+ {
+ id = 0;
+ }
+ else
+ {
+ id = m_listeners.rbegin()->first + 1;
+
+ // ReSharper disable once CppUseAssociativeContains - wrong, exists in cpp20+ only
+ while(m_listeners.find(id) != m_listeners.end())
+ ++id;
+ }
+ addListener(id, _callback);
+ return id;
+ }
+
+ void addListener(ListenerId _id, const Callback& _callback)
{
m_listeners.insert(std::make_pair(_id, _callback));
+
+ if(m_hasRetainedValue)
+ std::apply(_callback, m_retainedValue);
}
- void removeListener(const size_t _id)
+ void removeListener(const ListenerId _id)
{
m_listeners.erase(_id);
}
@@ -27,18 +54,86 @@ namespace pluginLib
m_listeners.clear();
}
- void invoke() const
+ void invoke(const Ts& ..._args) const
{
for (const auto& it : m_listeners)
- it.second();
+ it.second(_args...);
+ }
+
+ void operator ()(const Ts& ..._args) const
+ {
+ invoke(_args...);
+ }
+
+ void retain(Ts ..._args)
+ {
+ invoke(_args...);
+ m_hasRetainedValue = true;
+ m_retainedValue = std::tuple<Ts...>(_args...);
+ }
+
+ void clearRetained()
+ {
+ m_hasRetainedValue = false;
+ }
+
+ private:
+ std::map<ListenerId, Callback> m_listeners;
+
+ bool m_hasRetainedValue = false;
+ std::tuple<Ts...> m_retainedValue;
+ };
+
+ template<typename ...Ts>
+ class EventListener
+ {
+ public:
+ using MyEvent = Event<Ts...>;
+ using MyCallback = typename MyEvent::Callback;
+ using MyListenerId = typename MyEvent::ListenerId;
+
+ static constexpr MyListenerId InvalidListenerId = MyEvent::InvalidListenerId;
+
+ explicit EventListener(MyEvent& _event) : m_event(_event), m_listenerId(InvalidListenerId)
+ {
+ }
+
+ EventListener(MyEvent& _event, const MyCallback& _callback) : m_event(_event), m_listenerId(_event.addListener(_callback))
+ {
}
- void operator ()() const
+ EventListener(EventListener&& _listener) noexcept : m_event(_listener), m_listenerId(_listener.m_listenerId)
{
- invoke();
+ _listener.m_listenerId = InvalidListenerId;
+ }
+
+ EventListener(const EventListener&) = delete;
+ EventListener& operator = (const EventListener&) = delete;
+ EventListener& operator = (EventListener&& _source) = delete;
+
+ ~EventListener()
+ {
+ removeListener();
+ }
+
+ EventListener& operator = (const MyCallback& _callback)
+ {
+ removeListener();
+ m_listenerId = m_event.addListener(_callback);
+ return *this;
}
private:
- std::map<size_t, Callback> m_listeners;
+ void removeListener()
+ {
+ if(m_listenerId == InvalidListenerId)
+ return;
+
+ m_event.removeListener(m_listenerId);
+ m_listenerId = InvalidListenerId;
+ }
+
+ MyEvent& m_event;
+ MyListenerId m_listenerId;
};
}
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,16 @@ namespace pluginLib
m_param->setValueNotifyingHost(m_param->convertTo0to1(static_cast<float>(m_slider->getValue())), Parameter::ChangedBy::ControlChange);
}
+ void ParameterBinding::MouseListener::mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel)
+ {
+ 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 +99,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 +182,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,8 @@ namespace pluginLib
void mouseDown(const juce::MouseEvent& event) override;
void mouseUp(const juce::MouseEvent& event) override;
void mouseDrag(const juce::MouseEvent& event) override;
+ void mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel) override;
+ void mouseDoubleClick(const juce::MouseEvent& event) override;
private:
pluginLib::Parameter *m_param;
diff --git a/source/jucePluginLib/parameterdescription.cpp b/source/jucePluginLib/parameterdescription.cpp
@@ -4,6 +4,13 @@ namespace pluginLib
{
bool Description::isNonPartSensitive() const
{
+ if(isSoftKnob())
+ return false;
return classFlags & static_cast<int>(pluginLib::ParameterClass::Global) || (classFlags & static_cast<int>(pluginLib::ParameterClass::NonPartSensitive));
}
+
+ bool Description::isSoftKnob() const
+ {
+ return !softKnobTargetSelect.empty() && !softKnobTargetList.empty();
+ }
}
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
{
@@ -52,7 +53,11 @@ namespace pluginLib
bool isBipolar;
int step = 0;
std::string toText;
+ std::string softKnobTargetSelect;
+ std::string softKnobTargetList;
bool isNonPartSensitive() const;
+
+ bool isSoftKnob() const;
};
}
diff --git a/source/jucePluginLib/parameterdescriptions.cpp b/source/jucePluginLib/parameterdescriptions.cpp
@@ -53,6 +53,14 @@ namespace pluginLib
return true;
}
+ const ValueList* ParameterDescriptions::getValueList(const std::string& _key) const
+ {
+ const auto it = m_valueLists.find(_key);
+ if(it == m_valueLists.end())
+ return nullptr;
+ return &it->second;
+ }
+
std::string ParameterDescriptions::loadJson(const std::string& _jsonString)
{
// juce' JSON parser doesn't like JSON5-style comments
@@ -75,7 +83,7 @@ namespace pluginLib
return "Parameter descriptions are empty";
{
- const auto valueLists = json["valuelists"];
+ const auto& valueLists = json["valuelists"];
auto* entries = valueLists.getDynamicObject();
@@ -243,6 +251,8 @@ namespace pluginLib
d.isBool = readPropertyBool("isBool");
d.isBipolar = readPropertyBool("isBipolar");
d.step = readPropertyIntWithDefault("step", 0);
+ d.softKnobTargetSelect = readProperty("softknobTargetSelect", false).toString().toStdString();
+ d.softKnobTargetList = readProperty("softknobTargetList", false).toString().toStdString();
d.toText = valueList;
@@ -325,18 +335,71 @@ namespace pluginLib
m_nameToIndex.insert(std::make_pair(d.name, static_cast<uint32_t>(i)));
}
+ // verify soft knob parameters
+ for (auto& desc : m_descriptions)
+ {
+ if(desc.softKnobTargetSelect.empty())
+ continue;
+
+ const auto it = m_nameToIndex.find(desc.softKnobTargetSelect);
+
+ if(it == m_nameToIndex.end())
+ {
+ errors << desc.name << ": soft knob target parameter " << desc.softKnobTargetSelect << " not found" << '\n';
+ continue;
+ }
+
+ if(desc.softKnobTargetList.empty())
+ {
+ errors << desc.name << ": soft knob target list not specified\n";
+ continue;
+ }
+
+ const auto& targetParam = m_descriptions[it->second];
+ auto itList = m_valueLists.find(desc.softKnobTargetList);
+ if(itList == m_valueLists.end())
+ {
+ errors << desc.name << ": soft knob target list '" << desc.softKnobTargetList << "' not found\n";
+ continue;
+ }
+
+ const auto& sourceParamNames = itList->second.texts;
+ if(static_cast<int>(sourceParamNames.size()) != (targetParam.range.getLength() + 1))
+ {
+ errors << desc.name << ": soft knob target list " << desc.softKnobTargetList << " has " << sourceParamNames.size() << " entries but target select parameter " << targetParam.name << " has a range of " << (targetParam.range.getLength()+1) << '\n';
+ continue;
+ }
+
+ for (const auto& paramName : sourceParamNames)
+ {
+ if(paramName.empty())
+ continue;
+
+ const auto itsourceParam = m_nameToIndex.find(paramName);
+
+ if(itsourceParam == m_nameToIndex.end())
+ {
+ errors << desc.name << " - " << targetParam.name << ": soft knob source parameter " << paramName << " not found" << '\n';
+ continue;
+ }
+ }
+ }
+
const auto midipackets = json["midipackets"].getDynamicObject();
parseMidiPackets(errors, midipackets);
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())
{
LOG("ParameterDescription parsing issues:\n" << res);
- assert(false);
+ assert(false && "failed to parse parameter descriptions");
}
return res;
@@ -466,7 +529,7 @@ namespace pluginLib
const int last = entry["last"];
const int init = entry["init"];
- if(first < 0 || last < 0 || last <= first)
+ if(first < 0 || last < 0 || last < first)
{
_errors << "specified checksum range " << first << "-" << last << " is not valid, midi packet " << _key << ", index " << i << std::endl;
return;
@@ -527,7 +590,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 +676,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,13 @@
#include "midipacket.h"
#include "parameterdescription.h"
#include "parameterlink.h"
+#include "parameterregion.h"
+
+namespace juce
+{
+ class var;
+ class DynamicObject;
+}
namespace pluginLib
{
@@ -28,18 +35,26 @@ namespace pluginLib
const std::unordered_map<std::string, MidiPacket>& getMidiPackets() const { return m_midiPackets; }
+ const auto& getRegions() const { return m_regions; }
+
+ const ValueList* getValueList(const std::string& _key) const;
+
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/parameterlink.h b/source/jucePluginLib/parameterlink.h
@@ -1,7 +1,6 @@
#pragma once
#include <set>
-#include <vector>
namespace pluginLib
{
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,29 @@
+#pragma once
+
+#include <string>
+#include <unordered_map>
+
+#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; }
+
+ bool containsParameter(const std::string& _name) const
+ {
+ return m_params.find(_name) != m_params.end();
+ }
+
+ 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,265 @@
+#include "datasource.h"
+
+#include <algorithm>
+#include <sstream>
+#include <memory>
+
+#include "patch.h"
+#include "patchmodifications.h"
+
+#include "../../synthLib/binarystream.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);
+ }
+
+ PatchPtr DataSource::getPatch(const PatchKey& _key) const
+ {
+ for (const auto& patch : patches)
+ {
+ if(*patch == _key)
+ return patch;
+ }
+ return {};
+ }
+
+ 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();
+ }
+
+ void DataSource::write(synthLib::BinaryStream& _outStream) const
+ {
+ synthLib::ChunkWriter cw(_outStream, chunks::g_datasource, 1);
+
+ _outStream.write(static_cast<uint8_t>(type));
+ _outStream.write(static_cast<uint8_t>(origin));
+ _outStream.write(name);
+ _outStream.write(bank);
+
+ _outStream.write(static_cast<uint32_t>(patches.size()));
+
+ for (const auto& patch : patches)
+ patch->write(_outStream);
+ }
+
+ bool DataSource::read(synthLib::BinaryStream& _inStream)
+ {
+ auto in = _inStream.tryReadChunk(chunks::g_datasource);
+ if(!in)
+ return false;
+
+ type = static_cast<SourceType>(in.read<uint8_t>());
+ origin = static_cast<DataSourceOrigin>(in.read<uint8_t>());
+ name = in.readString();
+ bank = in.read<uint32_t>();
+
+ const auto numPatches = in.read<uint32_t>();
+
+ for(uint32_t i=0; i<numPatches; ++i)
+ {
+ const auto patch = std::make_shared<Patch>();
+ if(!patch->read(in))
+ return false;
+
+ if(patch->modifications)
+ {
+ patch->modifications->patch = patch;
+ patch->modifications->updateCache();
+ }
+ patches.insert(patch);
+ }
+
+ return true;
+ }
+
+ 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,149 @@
+#pragma once
+
+#include <string>
+
+#include "patchdbtypes.h"
+
+namespace synthLib
+{
+ class BinaryStream;
+}
+
+namespace pluginLib::patchDB
+{
+ struct PatchKey;
+
+ 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);
+ }
+
+ PatchPtr getPatch(const PatchKey& _key) const;
+
+ 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;
+
+ void write(synthLib::BinaryStream& _outStream) const;
+ bool read(synthLib::BinaryStream& _inStream);
+ };
+
+ 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,2011 @@
+#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 "../../synthLib/binarystream.h"
+
+#include "dsp56kEmu/logging.h"
+
+namespace pluginLib::patchDB
+{
+ static constexpr bool g_cacheEnabled = false;
+
+ 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_cacheFileName(m_settingsDir.getChildFile("patchmanagerdb.cache"))
+ , m_loader("PatchLoader", false, dsp56k::ThreadPriority::Lowest)
+ {
+ }
+
+ DB::~DB()
+ {
+ assert(m_loader.destroyed() && "stopLoaderThread() needs to be called by derived class in destructor");
+
+ // we can only save the cache if the db is not doing anything in the background
+ const bool loading = !m_loader.empty();
+
+ stopLoaderThread();
+
+ if(!loading && m_cacheDirty && g_cacheEnabled)
+ saveCache();
+ }
+
+ 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());
+ }
+
+ if(!_file.replaceWithData(sysexBuffer.data(), sysexBuffer.size()))
+ {
+ pushError("Failed to write to file " + _file.getFullPathName().toStdString() + ", make sure that it is not write protected");
+ return false;
+ }
+ return true;
+ }
+
+ 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();
+ });
+ }
+
+ DataSourceNodePtr DB::getDataSource(const DataSource& _ds)
+ {
+ std::shared_lock lock(m_dataSourcesMutex);
+ const auto it = m_dataSources.find(_ds);
+ if(it == m_dataSources.end())
+ return {};
+ return it->second;
+ }
+
+ 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)
+ {
+ if(_handle == g_invalidSearchHandle)
+ return;
+
+ 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*/, const std::function<void(const std::vector<PatchPtr>&)>& _successCallback/* = {}*/)
+ {
+ if (_ds->type != SourceType::LocalStorage)
+ return;
+
+ runOnLoaderThread([this, _ds, _patches, _insertRow, _successCallback]
+ {
+ {
+ 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);
+
+ if(_successCallback)
+ {
+ runOnUiThread([_successCallback, newPatches]
+ {
+ _successCallback(newPatches);
+ });
+ }
+
+ 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;
+
+ 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]
+ {
+ if(!g_cacheEnabled || !loadCache())
+ 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;
+ }
+ else
+ {
+ // this type of search is used to indicate that a patch has changed its properties, mark as dirty in this case too
+ if(search->request.patch == patch)
+ 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) const
+ {
+ 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 (const 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()
+ {
+ m_cacheDirty = true;
+
+ 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()
+ {
+ 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));
+ }
+
+ bool DB::loadCache()
+ {
+ m_cacheDirty = true;
+
+ if(!m_cacheFileName.existsAsFile())
+ return false;
+
+ std::vector<uint8_t> data;
+ if(!synthLib::readFile(data, m_cacheFileName.getFullPathName().toStdString()))
+ return false;
+
+ m_cacheFileName.deleteFile();
+
+ synthLib::BinaryStream inStream(data);
+
+ auto stream = inStream.tryReadChunk(chunks::g_patchManager);
+
+ if(!stream)
+ return false;
+
+ std::unique_lock lockDS(m_dataSourcesMutex);
+ std::unique_lock lockP(m_patchesMutex);
+
+ std::map<DataSource, DataSourceNodePtr> resultDataSources;
+ std::unordered_map<TagType, std::set<Tag>> resultTags;
+ std::unordered_map<TagType, std::unordered_map<Tag, uint32_t>> resultTagColors;
+ std::map<PatchKey, PatchModificationsPtr> resultPatchModifications;
+
+ if(auto s = stream.tryReadChunk(chunks::g_patchManagerDataSources, 1))
+ {
+ const auto numDatasources = s.read<uint32_t>();
+
+ std::vector<DataSource> dataSources;
+ dataSources.reserve(numDatasources);
+
+ for(uint32_t i=0; i<numDatasources; ++i)
+ {
+ DataSource& ds = dataSources.emplace_back();
+ if(!ds.read(s))
+ return false;
+ }
+
+ // read tree information
+ std::map<uint32_t, uint32_t> childToParentMap;
+ const auto childToParentCount = s.read<uint32_t>();
+ for(uint32_t i=0; i<childToParentCount; ++i)
+ {
+ const auto child = s.read<uint32_t>();
+ const auto parent = s.read<uint32_t>();
+ childToParentMap.insert({child, parent});
+ }
+
+ std::vector<DataSourceNodePtr> nodes;
+ nodes.resize(dataSources.size());
+
+ // create root nodes first
+ for(uint32_t i=0; i<dataSources.size(); ++i)
+ {
+ if(childToParentMap.find(i) != childToParentMap.end())
+ continue;
+
+ const auto& ds = dataSources[i];
+ const auto node = std::make_shared<DataSourceNode>(ds);
+ nodes[i] = node;
+ resultDataSources.insert({ds, node});
+ }
+
+ // now iteratively create children until there are none left
+ while(!childToParentMap.empty())
+ {
+ for(auto it = childToParentMap.begin(); it != childToParentMap.end();)
+ {
+ const auto childId = it->first;
+ const auto parentId = it->second;
+
+ const auto& parent = nodes[parentId];
+ if(!parent)
+ {
+ ++it;
+ continue;
+ }
+
+ const auto& ds = dataSources[childId];
+ const auto node = std::make_shared<DataSourceNode>(ds);
+ node->setParent(parent);
+ nodes[childId] = node;
+ resultDataSources.insert({ds, node});
+
+ it = childToParentMap.erase(it);
+ }
+ }
+
+ // now as we have the datasources created as nodes, patches need to know the datasource nodes they are part of
+ for (const auto& it : resultDataSources)
+ {
+ for(auto& patch : it.second->patches)
+ patch->source = it.second->weak_from_this();
+ }
+ }
+ else
+ return false;
+
+ if(auto s = stream.tryReadChunk(chunks::g_patchManagerTags, 1))
+ {
+ const auto tagTypeCount = s.read<uint32_t>();
+
+ for(uint32_t i=0; i<tagTypeCount; ++i)
+ {
+ const auto tagType = static_cast<TagType>(s.read<uint8_t>());
+ const auto tagCount = s.read<uint32_t>();
+
+ std::set<Tag> tags;
+ for(uint32_t t=0; t<tagCount; ++t)
+ tags.insert(s.readString());
+
+ resultTags.insert({tagType, tags});
+ }
+ }
+ else
+ return false;
+
+ if(auto s = stream.tryReadChunk(chunks::g_patchManagerTagColors, 1))
+ {
+ const auto tagTypeCount = s.read<uint32_t>();
+
+ for(uint32_t i=0; i<tagTypeCount; ++i)
+ {
+ const auto tagType = static_cast<TagType>(s.read<uint8_t>());
+ std::unordered_map<Tag, Color> tagToColor;
+
+ const auto count = s.read<uint32_t>();
+ for(uint32_t c=0; c<count; ++c)
+ {
+ const auto tag = s.readString();
+ const auto color = s.read<Color>();
+ tagToColor.insert({tag, color});
+ }
+ resultTagColors.insert({tagType, tagToColor});
+ }
+ }
+ else
+ return false;
+
+ if(auto s = stream.tryReadChunk(chunks::g_patchManagerPatchModifications, 1))
+ {
+ const auto count = s.read<uint32_t>();
+
+ for(uint32_t i=0; i<count; ++i)
+ {
+ auto key = PatchKey::fromString(s.readString());
+
+ const auto itDS = resultDataSources.find(*key.source);
+ if(itDS != resultDataSources.end())
+ key.source = itDS->second;
+
+ auto mods = std::make_shared<PatchModifications>();
+ if(!mods->read(s))
+ return false;
+
+ resultPatchModifications.insert({key, mods});
+
+ for (const auto& it : resultDataSources)
+ {
+ for (const auto& patch : it.second->patches)
+ {
+ if(*patch != key)
+ continue;
+
+ patch->modifications = mods;
+ mods->patch = patch;
+ }
+ }
+ }
+ }
+ else
+ return false;
+
+ m_dataSources = resultDataSources;
+ m_tags = resultTags;
+ m_tagColors = resultTagColors;
+ m_patchModifications = resultPatchModifications;
+
+ for (const auto& it: resultDataSources)
+ {
+ const auto& patches = it.second->patches;
+ updateSearches({patches.begin(), patches.end()});
+ }
+
+ {
+ std::unique_lock lockUi(m_uiMutex);
+
+ m_dirty.dataSources = true;
+ m_dirty.patches = true;
+
+ for (const auto& it : m_tags)
+ m_dirty.tags.insert(it.first);
+ }
+
+ m_cacheFileName.deleteFile();
+ m_cacheDirty = false;
+
+ return true;
+ }
+
+ void DB::saveCache()
+ {
+ if(!m_cacheFileName.hasWriteAccess())
+ return;
+
+ synthLib::BinaryStream outStream;
+ {
+ std::shared_lock lockDS(m_dataSourcesMutex);
+ std::shared_lock lockP(m_patchesMutex);
+
+ synthLib::ChunkWriter cw(outStream, chunks::g_patchManager, 1);
+ {
+ synthLib::ChunkWriter cwDS(outStream, chunks::g_patchManagerDataSources, 1);
+
+ outStream.write<uint32_t>(static_cast<uint32_t>(m_dataSources.size()));
+
+ // create an id map to save space when writing child->parent dependencies
+ std::map<DataSource, uint32_t> idMap;
+ uint32_t index = 0;
+
+ // write datasources and create id map
+ for (const auto& it : m_dataSources)
+ {
+ idMap.insert({it.first, index++});
+
+ it.second->write(outStream);
+ }
+
+ // create child->parent map
+ std::map<uint32_t, uint32_t> childToParent;
+
+ for (const auto& it : m_dataSources)
+ {
+ const auto& childDS = it.second;
+ const auto& parentDS = childDS->getParent();
+
+ if(parentDS)
+ {
+ const auto idChild = idMap.find(*childDS)->second;
+ const auto idParent = idMap.find(*parentDS)->second;
+
+ childToParent.insert({idChild, idParent});
+ }
+ }
+
+ // write child->parent dependency tree
+ outStream.write<uint32_t>(static_cast<uint32_t>(childToParent.size()));
+
+ for (const auto& it : childToParent)
+ {
+ outStream.write(it.first);
+ outStream.write(it.second);
+ }
+ }
+
+ {
+ // write tags
+ synthLib::ChunkWriter cwDS(outStream, chunks::g_patchManagerTags, 1);
+
+ outStream.write(static_cast<uint32_t>(m_tags.size()));
+
+ for (const auto& it : m_tags)
+ {
+ const auto tagType = it.first;
+ const auto& tags = it.second;
+
+ if(tags.empty())
+ continue;
+
+ outStream.write(static_cast<uint8_t>(tagType));
+ outStream.write(static_cast<uint32_t>(tags.size()));
+
+ for (const auto& tag : tags)
+ outStream.write(tag);
+ }
+ }
+
+ {
+ // write tag colors
+ synthLib::ChunkWriter cwDS(outStream, chunks::g_patchManagerTagColors, 1);
+
+ outStream.write(static_cast<uint32_t>(m_tagColors.size()));
+
+ for (const auto& it : m_tagColors)
+ {
+ const auto tagType = it.first;
+ const auto& mapStringToColor = it.second;
+
+ if(mapStringToColor.empty())
+ continue;
+
+ outStream.write(static_cast<uint8_t>(tagType));
+ outStream.write(static_cast<uint32_t>(mapStringToColor.size()));
+
+ for (const auto& itStringToColor : mapStringToColor)
+ {
+ outStream.write(itStringToColor.first);
+ outStream.write(itStringToColor.second);
+ }
+ }
+ }
+
+ {
+ // write patch modifications
+ synthLib::ChunkWriter cwDS(outStream, chunks::g_patchManagerPatchModifications, 1);
+
+ outStream.write(static_cast<uint32_t>(m_patchModifications.size()));
+
+ for (const auto& it : m_patchModifications)
+ {
+ const auto key = it.first;
+ const auto mods = it.second;
+
+ outStream.write(key.toString(true));
+ mods->write(outStream);
+ }
+ }
+ }
+
+ std::vector<uint8_t> buffer;
+ outStream.toVector(buffer);
+
+ m_cacheFileName.replaceWithData(buffer.data(), buffer.size());
+
+ m_cacheDirty = false;
+ }
+}
diff --git a/source/jucePluginLib/patchdb/db.h b/source/jucePluginLib/patchdb/db.h
@@ -0,0 +1,172 @@
+#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);
+ DataSourceNodePtr getDataSource(const DataSource& _ds);
+
+ 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,
+ const std::function<void(const std::vector<PatchPtr>&)>& _successCallback = {});
+ 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(); }
+
+ 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) const;
+
+ 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();
+
+ void pushError(std::string _string);
+
+ bool loadCache();
+ void saveCache();
+
+ // IO
+ juce::File m_settingsDir;
+ juce::File m_jsonFileName;
+ juce::File m_cacheFileName;
+
+ // 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;
+ bool m_cacheDirty = false;
+ };
+}
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,100 @@
+#pragma once
+
+#include <deque>
+#include <functional>
+#include <mutex>
+#include <condition_variable>
+#include <thread>
+#include <vector>
+#include <memory>
+
+#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,189 @@
+#include "patch.h"
+
+#include <cassert>
+#include <sstream>
+
+#include "patchmodifications.h"
+#include "serialization.h"
+
+#include "juce_core/juce_core.h"
+
+#include "../../synthLib/binarystream.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;
+ }
+
+ void Patch::write(synthLib::BinaryStream& _s) const
+ {
+ synthLib::ChunkWriter chunkWriter(_s, chunks::g_patch, 1);
+
+ _s.write(name);
+ _s.write(bank);
+
+ if(modifications && !modifications->empty())
+ {
+ _s.write<uint8_t>(1);
+ modifications->write(_s);
+ }
+ else
+ {
+ _s.write<uint8_t>(0);
+ }
+ tags.write(_s);
+
+ _s.write(hash);
+ _s.write(sysex);
+ }
+
+ bool Patch::read(synthLib::BinaryStream& _in)
+ {
+ auto in = _in.tryReadChunk(chunks::g_patch, 1);
+ if(!in)
+ return false;
+
+ name = in.readString();
+ bank = in.read<uint32_t>();
+
+ const auto hasMods = in.read<uint8_t>();
+
+ if(hasMods)
+ {
+ modifications = std::make_shared<PatchModifications>();
+ if(!modifications->read(in))
+ return false;
+ }
+
+ if(!tags.read(in))
+ return false;
+
+ hash = in.read<PatchHash>();
+ in.read(sysex);
+
+ return true;
+ }
+
+ bool Patch::operator==(const PatchKey& _key) const
+ {
+ return program == _key.program && hash == _key.hash && PatchKey::equals(source.lock(), _key.source);
+ }
+
+ 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,123 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+#include <array>
+
+#include "datasource.h"
+#include "tags.h"
+#include "patchdbtypes.h"
+
+namespace synthLib
+{
+ class BinaryStream;
+}
+
+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);
+
+ void write(synthLib::BinaryStream& _s) const;
+ bool read(synthLib::BinaryStream& _in);
+
+ bool operator == (const PatchKey& _key) const;
+ bool operator != (const PatchKey& _key) const
+ {
+ return !(*this == _key);
+ }
+
+ 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,97 @@
+#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;
+
+ namespace chunks
+ {
+ constexpr char g_patchManager[] = "Pmpm";
+ constexpr char g_patchManagerDataSources[] = "PmDs";
+ constexpr char g_patchManagerTagColors[] = "PmTC";
+ constexpr char g_patchManagerTags[] = "PmTs";
+ constexpr char g_patchManagerPatchModifications[] = "PMds";
+
+ constexpr char g_patchModification[] = "PMod";
+ constexpr char g_datasource[] = "DatS";
+ constexpr char g_patch[] = "Patc";
+ constexpr char g_typedTags[] = "TpTg";
+ constexpr char g_tags[] = "Tags";
+ }
+}
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,132 @@
+#include "patchmodifications.h"
+
+#include "patch.h"
+
+#include "juce_core/juce_core.h"
+
+#include "../../synthLib/binarystream.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();
+ }
+
+ void PatchModifications::write(synthLib::BinaryStream& _outStream) const
+ {
+ synthLib::ChunkWriter cw(_outStream, chunks::g_patchModification, 1);
+ _outStream.write(name);
+ tags.write(_outStream);
+ }
+
+ bool PatchModifications::read(synthLib::BinaryStream& _binaryStream)
+ {
+ auto in = _binaryStream.tryReadChunk(chunks::g_patchModification, 1);
+ if(!in)
+ return false;
+
+ name = in.readString();
+
+ if(!tags.read(in))
+ return false;
+
+ return true;
+ }
+}
diff --git a/source/jucePluginLib/patchdb/patchmodifications.h b/source/jucePluginLib/patchdb/patchmodifications.h
@@ -0,0 +1,33 @@
+#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;
+
+ void write(synthLib::BinaryStream& _outStream) const;
+ bool read(synthLib::BinaryStream& _binaryStream);
+
+ 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,281 @@
+#include "tags.h"
+
+#include "juce_core/juce_core.h"
+
+#include "../../synthLib/binarystream.h"
+
+namespace pluginLib::patchDB
+{
+ void Tags::write(synthLib::BinaryStream& _s) const
+ {
+ synthLib::ChunkWriter cw(_s, chunks::g_tags, 1);
+ _s.write(m_added.size());
+ for (const auto& added : m_added)
+ _s.write(added);
+ _s.write(m_removed.size());
+ for (const auto& removed : m_removed)
+ _s.write(removed);
+ }
+
+ bool Tags::read(synthLib::BinaryStream& _stream)
+ {
+ auto in = _stream.tryReadChunk(chunks::g_tags, 1);
+ if(!in)
+ return false;
+
+ const auto numAdded = in.read<size_t>();
+
+ for(size_t i=0; i<numAdded; ++i)
+ m_added.insert(in.readString());
+
+ const auto numRemoved = in.read<size_t>();
+
+ for(size_t i=0; i<numRemoved; ++i)
+ m_removed.insert(in.readString());
+
+ return true;
+ }
+
+ 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 (const auto& e : m_added)
+ {
+ if(_t.m_added.find(e) == _t.m_added.end())
+ return false;
+ }
+
+ for (const 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;
+ }
+
+ void TypedTags::write(synthLib::BinaryStream& _s) const
+ {
+ synthLib::ChunkWriter cw(_s, chunks::g_typedTags, 1);
+ _s.write(m_tags.size());
+
+ for (const auto& t : m_tags)
+ {
+ _s.write(t.first);
+ t.second.write(_s);
+ }
+ }
+
+ bool TypedTags::read(synthLib::BinaryStream& _stream)
+ {
+ auto in = _stream.tryReadChunk(chunks::g_typedTags, 1);
+
+ if(!in)
+ return false;
+
+ const auto count = in.read<size_t>();
+
+ for(size_t i=0; i<count; ++i)
+ {
+ const auto type = in.read<TagType>();
+
+ Tags t;
+
+ if(!t.read(in))
+ return false;
+
+ m_tags.insert({type, t});
+ }
+
+ return true;
+ }
+}
diff --git a/source/jucePluginLib/patchdb/tags.h b/source/jucePluginLib/patchdb/tags.h
@@ -0,0 +1,108 @@
+#pragma once
+
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+
+#include "patchdbtypes.h"
+
+namespace synthLib
+{
+ class BinaryStream;
+}
+
+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();
+ }
+
+ void write(synthLib::BinaryStream& _s) const;
+ bool read(synthLib::BinaryStream& _stream);
+
+ 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;
+
+ void write(synthLib::BinaryStream& _s) const;
+ bool read(synthLib::BinaryStream& _stream);
+
+ private:
+ std::unordered_map<TagType, Tags> m_tags;
+ };
+}
diff --git a/source/jucePluginLib/processor.cpp b/source/jucePluginLib/processor.cpp
@@ -1,9 +1,13 @@
#include "processor.h"
#include "dummydevice.h"
+#include "types.h"
#include "../synthLib/deviceException.h"
#include "../synthLib/os.h"
#include "../synthLib/binarystream.h"
+#include "../synthLib/midiBufferParser.h"
+
+#include "dsp56kEmu/fastmath.h"
#include "dsp56kEmu/logging.h"
@@ -15,11 +19,9 @@ namespace synthLib
namespace pluginLib
{
constexpr char g_saveMagic[] = "DSP56300";
- constexpr uint32_t g_saveVersion = 1;
-
- using PluginStream = synthLib::BinaryStream<uint32_t>;
+ constexpr uint32_t g_saveVersion = 2;
- Processor::Processor(const BusesProperties& _busesProperties) : juce::AudioProcessor(_busesProperties)
+ Processor::Processor(const BusesProperties& _busesProperties, Properties _properties) : juce::AudioProcessor(_busesProperties), m_properties(std::move(_properties))
{
}
@@ -156,18 +158,33 @@ namespace pluginLib
{
LOG("Failed to create device: " << e.what());
- std::string msg = e.what();
+ // Juce loads the LV2/VST3 versions of the plugin as part of the build process, if we open a message box in this case, the build process gets stuck
+ const auto host = juce::PluginHostType::getHostPath();
+ if(!host.contains("juce_vst3_helper") && !host.contains("juce_lv2_helper"))
+ {
+ std::string msg = e.what();
- m_deviceError = e.errorCode();
+ m_deviceError = e.errorCode();
- if(e.errorCode() == synthLib::DeviceError::FirmwareMissing)
- {
- msg += "\n\n";
- msg += "The firmware file needs to be located next to the plugin.";
- msg += "\n\n";
- msg += "The plugin was loaded from path:\n\n" + synthLib::getModulePath() + "\n\nCopy the requested file to this path and reload the plugin.";
+ if(e.errorCode() == synthLib::DeviceError::FirmwareMissing)
+ {
+ msg += "\n\n";
+ msg += "The firmware file needs to be located next to the plugin.";
+ msg += "\n\n";
+ msg += "The plugin was loaded from path:\n\n";
+ msg += synthLib::getModulePath();
+#ifdef _DEBUG
+ msg += std::string("from host ") + host.toStdString();
+#endif
+ msg += "\n\nCopy the requested file to this path and reload the plugin.";
+ }
+ juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon,
+ "Device Initialization failed", msg, nullptr,
+ juce::ModalCallbackFunction::create([](int)
+ {
+ })
+ );
}
- juce::NativeMessageBox::showMessageBoxAsync(juce::AlertWindow::WarningIcon, "Device Initialization failed", msg);
}
if(!m_device)
@@ -175,6 +192,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,12 +207,169 @@ namespace pluginLib
return true;
}
+ void Processor::updateLatencySamples()
+ {
+ if(getProperties().isSynth)
+ setLatencySamples(getPlugin().getLatencyMidiToOutput());
+ else
+ setLatencySamples(getPlugin().getLatencyInputToOutput());
+ }
+
+ void Processor::saveCustomData(std::vector<uint8_t>& _targetBuffer)
+ {
+ synthLib::BinaryStream s;
+ saveChunkData(s);
+ s.toVector(_targetBuffer, true);
+ }
+
+ void Processor::saveChunkData(synthLib::BinaryStream& s)
+ {
+ {
+ std::vector<uint8_t> buffer;
+ getPlugin().getState(buffer, synthLib::StateTypeGlobal);
+
+ synthLib::ChunkWriter cw(s, "MIDI", 1);
+ s.write(buffer);
+ }
+ {
+ 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);
+ }
+
+ if(m_preferredDeviceSamplerate > 0)
+ {
+ synthLib::ChunkWriter cw(s, "DSSR", 1);
+ s.write(m_preferredDeviceSamplerate);
+ }
+ }
+
+ bool Processor::loadCustomData(const std::vector<uint8_t>& _sourceBuffer)
+ {
+ if(_sourceBuffer.empty())
+ return true;
+
+ // 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 true;
+ }
+
+ synthLib::BinaryStream s(_sourceBuffer);
+ synthLib::ChunkReader cr(s);
+
+ loadChunkData(cr);
+
+ return _sourceBuffer.empty() || (cr.tryRead() && cr.numRead() > 0);
+ }
+
+ void Processor::loadChunkData(synthLib::ChunkReader& _cr)
+ {
+ _cr.add("MIDI", 1, [this](synthLib::BinaryStream& _binaryStream, uint32_t _version)
+ {
+ std::vector<uint8_t> buffer;
+ _binaryStream.read(buffer);
+ getPlugin().setState(buffer);
+ });
+
+ _cr.add("GAIN", 1, [this](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.add("DSSR", 1, [this](synthLib::BinaryStream& _binaryStream, uint32_t _version)
+ {
+ const auto sr = _binaryStream.read<float>();
+ setPreferredDeviceSamplerate(sr);
+ });
+ }
+
+ void Processor::readGain(synthLib::BinaryStream& _s)
+ {
+ const auto version = _s.read<uint32_t>();
+ if (version != 1)
+ return;
+ m_inputGain = _s.read<float>();
+ m_outputGain = _s.read<float>();
+ }
+
+ 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();
+ }
+
+ bool Processor::setPreferredDeviceSamplerate(const float _samplerate)
+ {
+ m_preferredDeviceSamplerate = _samplerate;
+
+ if(!m_device)
+ return false;
+
+ return getPlugin().setPreferredDeviceSamplerate(_samplerate);
+ }
+
+ float Processor::getPreferredDeviceSamplerate() const
+ {
+ return m_preferredDeviceSamplerate;
+ }
+
+ std::vector<float> Processor::getDeviceSupportedSamplerates() const
+ {
+ if(!m_device)
+ return {};
+ return m_device->getSupportedSamplerates();
+ }
+
+ std::vector<float> Processor::getDevicePreferredSamplerates() const
+ {
+ if(!m_device)
+ return {};
+ return m_device->getPreferredSamplerates();
+ }
+
//==============================================================================
void Processor::prepareToPlay(double sampleRate, int samplesPerBlock)
{
// Use this method as the place to do any pre-playback
- // initialisation that you need..
- getPlugin().setSamplerate(static_cast<float>(sampleRate));
+ // initialisation that you need
+ m_hostSamplerate = static_cast<float>(sampleRate);
+
+ getPlugin().setHostSamplerate(static_cast<float>(sampleRate), m_preferredDeviceSamplerate);
getPlugin().setBlockSize(samplesPerBlock);
updateLatencySamples();
@@ -205,21 +381,33 @@ namespace pluginLib
// spare memory, etc.
}
+ bool Processor::isBusesLayoutSupported(const BusesLayout& _busesLayout) const
+ {
+ // This is the place where you check if the layout is supported.
+ // In this template code we only support mono or stereo.
+ // Some plugin hosts, such as certain GarageBand versions, will only
+ // load plugins that support stereo bus layouts.
+ if (_busesLayout.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
+ return false;
+
+ // This checks if the input is stereo
+ if (_busesLayout.getMainInputChannelSet() != juce::AudioChannelSet::stereo())
+ return false;
+
+ return true;
+ }
+
//==============================================================================
void Processor::getStateInformation (juce::MemoryBlock& destData)
{
// You should use this method to store your parameters in the memory block.
// You could do that either as raw data, or use the XML or ValueTree classes
// as intermediaries to make it easy to save and load complex data.
-
- std::vector<uint8_t> buffer;
- getPlugin().getState(buffer, synthLib::StateTypeGlobal);
-
+#if !SYNTHLIB_DEMO_MODE
PluginStream ss;
ss.write(g_saveMagic);
ss.write(g_saveVersion);
- ss.write(buffer);
- buffer.clear();
+ std::vector<uint8_t> buffer;
saveCustomData(buffer);
ss.write(buffer);
@@ -227,27 +415,194 @@ 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
+ }
+
+ const juce::String Processor::getName() const
+ {
+ return getProperties().name;
}
+ bool Processor::acceptsMidi() const
+ {
+ return getProperties().wantsMidiInput;
+ }
+
+ bool Processor::producesMidi() const
+ {
+ return getProperties().producesMidiOut;
+ }
+
+ bool Processor::isMidiEffect() const
+ {
+ return getProperties().isMidiEffect;
+ }
+
+ void Processor::processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
+ {
+ juce::ScopedNoDenormals noDenormals;
+ const auto totalNumInputChannels = getTotalNumInputChannels();
+ const auto totalNumOutputChannels = getTotalNumOutputChannels();
+
+ const int numSamples = buffer.getNumSamples();
+
+ // In case we have more outputs than inputs, this code clears any output
+ // channels that didn't contain input data, (because these aren't
+ // guaranteed to be empty - they may contain garbage).
+ // This is here to avoid people getting screaming feedback
+ // when they first compile a plugin, but obviously you don't need to keep
+ // this code if your algorithm always overwrites all the output channels.
+ for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
+ buffer.clear (i, 0, numSamples);
+
+ // This is the place where you'd normally do the guts of your plugin's
+ // audio processing...
+ // Make sure to reset the state if your inner loop is processing
+ // the samples and the outer loop is handling the channels.
+ // Alternatively, you can process the samples with the channels
+ // interleaved by keeping the same state.
+
+ synthLib::TAudioInputs inputs{};
+ synthLib::TAudioOutputs outputs{};
+
+ for (int channel = 0; channel < totalNumInputChannels; ++channel)
+ inputs[channel] = buffer.getReadPointer(channel);
+
+ for (int channel = 0; channel < totalNumOutputChannels; ++channel)
+ outputs[channel] = buffer.getWritePointer(channel);
+
+ for(const auto metadata : midiMessages)
+ {
+ const auto message = metadata.getMessage();
+
+ synthLib::SMidiEvent ev{};
+
+ if(message.isSysEx() || message.getRawDataSize() > 3)
+ {
+ ev.sysex.resize(message.getRawDataSize());
+ 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 it's already there
+ if(ev.sysex.size() > 1)
+ {
+ if(ev.sysex.front() == 0xf0 && ev.sysex[1] == 0xf0)
+ ev.sysex.erase(ev.sysex.begin());
+
+ if(ev.sysex.size() > 1)
+ {
+ if(ev.sysex[ev.sysex.size()-1] == 0xf7 && ev.sysex[ev.sysex.size()-2] == 0xf7)
+ ev.sysex.erase(ev.sysex.begin());
+ }
+ }
+ }
+ else
+ {
+ ev.a = message.getRawData()[0];
+ ev.b = message.getRawDataSize() > 0 ? message.getRawData()[1] : 0;
+ ev.c = message.getRawDataSize() > 1 ? message.getRawData()[2] : 0;
+
+ const auto status = ev.a & 0xf0;
+
+ if(status == synthLib::M_CONTROLCHANGE || status == synthLib::M_POLYPRESSURE)
+ {
+ // forward to UI to react to control input changes that should move knobs
+ getController().addPluginMidiOut({ev});
+ }
+ }
+
+ ev.offset = metadata.samplePosition;
+
+ getPlugin().addMidiEvent(ev);
+ }
+
+ midiMessages.clear();
+
+ bool isPlaying = true;
+ float bpm = 0.0f;
+ float ppqPos = 0.0f;
+
+ if(const auto* playHead = getPlayHead())
+ {
+ if(auto pos = playHead->getPosition())
+ {
+ isPlaying = pos->getIsPlaying();
+
+ if(pos->getBpm())
+ {
+ bpm = static_cast<float>(*pos->getBpm());
+ processBpm(bpm);
+ }
+ if(pos->getPpqPosition())
+ {
+ ppqPos = static_cast<float>(*pos->getPpqPosition());
+ }
+ }
+ }
+
+ getPlugin().process(inputs, outputs, numSamples, bpm, ppqPos, isPlaying);
+
+ applyOutputGain(outputs, numSamples);
+
+ m_midiOut.clear();
+ getPlugin().getMidiOut(m_midiOut);
+
+ if (!m_midiOut.empty())
+ {
+ getController().addPluginMidiOut(m_midiOut);
+ }
+
+ for (auto& e : m_midiOut)
+ {
+ if (e.source == synthLib::MidiEventSourceEditor)
+ continue;
+
+ auto toJuceMidiMessage = [&e]()
+ {
+ if(!e.sysex.empty())
+ return juce::MidiMessage(e.sysex.data(), static_cast<int>(e.sysex.size()), 0.0);
+ const auto len = synthLib::MidiBufferParser::lengthFromStatusByte(e.a);
+ if(len == 1)
+ return juce::MidiMessage(e.a, 0.0);
+ if(len == 2)
+ return juce::MidiMessage(e.a, e.b, 0.0);
+ return juce::MidiMessage(e.a, e.b, e.c, 0.0);
+ };
+
+ const juce::MidiMessage message = toJuceMidiMessage();
+ midiMessages.addEvent(message, 0);
+
+ // additionally send to the midi output we've selected in the editor
+ if (m_midiOutput)
+ m_midiOutput->sendMessageNow(message);
+ }
+ }
+
+#if !SYNTHLIB_DEMO_MODE
void Processor::setState(const void* _data, const size_t _sizeInBytes)
{
if(_sizeInBytes < 1)
@@ -270,20 +625,28 @@ namespace pluginLib
const auto version = ss.read<uint32_t>();
- if (version != g_saveVersion)
+ if (version > g_saveVersion)
return;
std::vector<uint8_t> buffer;
- ss.read(buffer);
- getPlugin().setState(buffer);
- ss.read(buffer);
- try
+ if(version == 1)
{
- loadCustomData(buffer);
+ ss.read(buffer);
+ getPlugin().setState(buffer);
}
- catch (std::range_error&)
+
+ ss.read(buffer);
+
+ if(!buffer.empty())
{
+ try
+ {
+ loadCustomData(buffer);
+ }
+ catch (std::range_error&)
+ {
+ }
}
}
catch (std::range_error& e)
@@ -300,6 +663,7 @@ namespace pluginLib
if (hasController())
getController().onStateLoaded();
}
+#endif
//==============================================================================
diff --git a/source/jucePluginLib/processor.h b/source/jucePluginLib/processor.h
@@ -9,6 +9,8 @@
namespace synthLib
{
+ class BinaryStream;
+ class ChunkReader;
class Plugin;
struct SMidiEvent;
}
@@ -18,7 +20,16 @@ namespace pluginLib
class Processor : public juce::AudioProcessor, juce::MidiInputCallback
{
public:
- Processor(const BusesProperties& _busesProperties);
+ struct Properties
+ {
+ const std::string name;
+ const bool isSynth;
+ const bool wantsMidiInput;
+ const bool producesMidiOut;
+ const bool isMidiEffect;
+ };
+
+ Processor(const BusesProperties& _busesProperties, Properties _properties);
~Processor() override;
void getLastMidiOut(std::vector<synthLib::SMidiEvent>& dst);
@@ -44,22 +55,75 @@ namespace pluginLib
}
virtual bool setLatencyBlocks(uint32_t _blocks);
- virtual void updateLatencySamples() = 0;
+ virtual void updateLatencySamples();
+
+ virtual void saveCustomData(std::vector<uint8_t>& _targetBuffer);
+ virtual void saveChunkData(synthLib::BinaryStream& s);
+ virtual bool loadCustomData(const std::vector<uint8_t>& _sourceBuffer);
+ virtual void loadChunkData(synthLib::ChunkReader& _cr);
+
+ void readGain(synthLib::BinaryStream& _s);
+
+ 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;
+
+ bool setPreferredDeviceSamplerate(float _samplerate);
+ float getPreferredDeviceSamplerate() const;
+ std::vector<float> getDeviceSupportedSamplerates() const;
+ std::vector<float> getDevicePreferredSamplerates() const;
+
+ float getHostSamplerate() const { return m_hostSamplerate; }
- virtual void saveCustomData(std::vector<uint8_t>& _targetBuffer) {}
- virtual void loadCustomData(const std::vector<uint8_t>& _sourceBuffer) {}
+ const Properties& getProperties() const { return m_properties; }
+
+ virtual void processBpm(float _bpm) {};
private:
void prepareToPlay(double sampleRate, int maximumExpectedSamplesPerBlock) override;
void releaseResources() override;
//==============================================================================
+ bool isBusesLayoutSupported(const BusesLayout&) const override;
void getStateInformation (juce::MemoryBlock& destData) override;
void setStateInformation (const void* _data, int _sizeInBytes) override;
void getCurrentProgramStateInformation (juce::MemoryBlock& destData) override;
void setCurrentProgramStateInformation (const void* data, int sizeInBytes) override;
+ const juce::String getName() const override;
+ bool acceptsMidi() const override;
+ bool producesMidi() const override;
+ bool isMidiEffect() const override;
+ void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override;
+#if !SYNTHLIB_DEMO_MODE
void setState(const void *_data, size_t _sizeInBytes);
+#endif
//==============================================================================
int getNumPrograms() override;
@@ -84,5 +148,13 @@ namespace pluginLib
std::unique_ptr<juce::MidiOutput> m_midiOutput{};
std::unique_ptr<juce::MidiInput> m_midiInput{};
std::vector<synthLib::SMidiEvent> m_midiOut{};
+
+ private:
+ const Properties m_properties;
+ float m_outputGain = 1.0f;
+ float m_inputGain = 1.0f;
+ uint32_t m_dspClockPercent = 100;
+ float m_preferredDeviceSamplerate = 0.0f;
+ float m_hostSamplerate = 0.0f;
};
}
diff --git a/source/jucePluginLib/softknob.cpp b/source/jucePluginLib/softknob.cpp
@@ -0,0 +1,112 @@
+#include "softknob.h"
+
+#include <cassert>
+
+#include "controller.h"
+
+namespace pluginLib
+{
+ namespace
+ {
+ uint32_t g_softKnobListenerId = 0x50f750f7;
+ }
+
+ SoftKnob::SoftKnob(const Controller& _controller, const uint8_t _part, const uint32_t _parameterIndex)
+ : m_controller(_controller)
+ , m_part(_part)
+ , m_uniqueId(g_softKnobListenerId++)
+ {
+ m_param = _controller.getParameter(_parameterIndex, _part);
+ assert(m_param);
+
+ const auto& desc = m_param->getDescription();
+
+ const auto idxTargetSelect = _controller.getParameterIndexByName(desc.softKnobTargetSelect);
+ assert(idxTargetSelect != Controller::InvalidParameterIndex);
+
+ m_targetSelect = _controller.getParameter(idxTargetSelect, _part);
+ assert(m_targetSelect);
+
+ m_targetSelect->onValueChanged.emplace_back(m_uniqueId, [this]()
+ {
+ onTargetChanged();
+ });
+
+ m_param->onValueChanged.emplace_back(m_uniqueId, [this]()
+ {
+ onSourceValueChanged();
+ });
+
+ bind();
+ }
+
+ SoftKnob::~SoftKnob()
+ {
+ unbind();
+ m_param->removeListener(m_uniqueId);
+ m_targetSelect->removeListener(m_uniqueId);
+ }
+
+ void SoftKnob::onTargetChanged()
+ {
+ bind();
+ }
+
+ void SoftKnob::onSourceValueChanged()
+ {
+ if(!m_targetParam)
+ return;
+
+ const auto v = m_param->getValue();
+ m_targetParam->setValue(v, Parameter::ChangedBy::Derived);
+ }
+
+ void SoftKnob::onTargetValueChanged()
+ {
+ assert(m_targetParam);
+ const auto v = m_targetParam->getValue();
+ m_param->setValue(v, Parameter::ChangedBy::Derived);
+ }
+
+ void SoftKnob::bind()
+ {
+ unbind();
+
+ const auto* valueList = m_controller.getParameterDescriptions().getValueList(m_param->getDescription().softKnobTargetList);
+ if(!valueList)
+ return;
+
+ const auto& targets = valueList->texts;
+
+ const auto targetIndex = m_targetSelect->getUnnormalizedValue();
+
+ if(targetIndex < 0 || targetIndex >= static_cast<int>(targets.size()))
+ return;
+
+ const auto targetName = targets[targetIndex];
+ if(targetName.empty())
+ return;
+
+ const auto targetParamIdx = m_controller.getParameterIndexByName(targetName);
+ if(targetParamIdx == Controller::InvalidParameterIndex)
+ return;
+
+ m_targetParam = m_controller.getParameter(targetParamIdx, m_part);
+ if(!m_targetParam)
+ return;
+
+ m_targetParam->onValueChanged.emplace_back(m_uniqueId, [this]()
+ {
+ onTargetValueChanged();
+ });
+
+ onTargetValueChanged();
+ }
+
+ void SoftKnob::unbind()
+ {
+ if(m_targetParam)
+ m_targetParam->removeListener(m_uniqueId);
+ m_targetParam = nullptr;
+ }
+}
diff --git a/source/jucePluginLib/softknob.h b/source/jucePluginLib/softknob.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <cstdint>
+
+namespace pluginLib
+{
+ class Parameter;
+ class Controller;
+
+ class SoftKnob
+ {
+ public:
+ SoftKnob(const Controller& _controller, uint8_t _part, uint32_t _parameterIndex);
+ ~SoftKnob();
+
+ SoftKnob(const SoftKnob&) = delete;
+ SoftKnob(SoftKnob&&) = delete;
+ SoftKnob& operator = (const SoftKnob&) = delete;
+ SoftKnob& operator = (SoftKnob&&) = delete;
+
+ bool isValid() const { return m_param != nullptr && m_targetSelect != nullptr; }
+ bool isBound() const { return m_targetParam != nullptr; }
+
+ Parameter* getParameter() const { return m_param; }
+ Parameter* getTargetParameter() const { return m_targetParam; }
+
+ private:
+ void onTargetChanged();
+ void onSourceValueChanged();
+ void onTargetValueChanged();
+
+ void bind();
+ void unbind();
+
+ const Controller& m_controller;
+ const uint8_t m_part;
+ const uint32_t m_uniqueId;
+
+ Parameter* m_param = nullptr;
+ Parameter* m_targetSelect = nullptr;
+ Parameter* m_targetParam = nullptr;
+ };
+}
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
@@ -25,3 +30,5 @@ source_group("source" FILES ${SOURCES})
target_include_directories(juceUiLib PUBLIC ../JUCE/modules)
target_compile_definitions(juceUiLib PRIVATE JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED=1)
+target_compile_definitions(juceUiLib PUBLIC JUCE_MODAL_LOOPS_PERMITTED=1)
+set_property(TARGET juceUiLib PROPERTY FOLDER "Gearmulator")
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/condition.cpp b/source/juceUiLib/condition.cpp
@@ -4,26 +4,32 @@
namespace genericUI
{
- Condition::Condition(juce::Component& _target, juce::Value* _value, int32_t _parameterIndex, std::set<uint8_t> _values) : m_target(_target), m_parameterIndex(_parameterIndex), m_values(std::move(_values))
+ void Condition::setEnabled(const bool _enable)
+ {
+ m_enabled = _enable;
+ Editor::setEnabled(m_target, _enable);
+ }
+
+ ConditionByParameterValues::ConditionByParameterValues(juce::Component& _target, juce::Value* _value, int32_t _parameterIndex, std::set<uint8_t> _values)
+ : Condition(_target)
+ , m_parameterIndex(_parameterIndex)
+ , m_values(std::move(_values))
{
bind(_value);
}
- Condition::~Condition()
+ ConditionByParameterValues::~ConditionByParameterValues()
{
unbind();
}
- void Condition::valueChanged(juce::Value& _value)
+ void ConditionByParameterValues::valueChanged(juce::Value& _value)
{
const auto v = roundToInt(_value.getValueSource().getValue());
-
- const auto enable = m_values.find(static_cast<uint8_t>(v)) != m_values.end();
-
- Editor::setEnabled(m_target, enable);
+ setEnabled(m_values.find(static_cast<uint8_t>(v)) != m_values.end());
}
- void Condition::bind(juce::Value* _value)
+ void ConditionByParameterValues::bind(juce::Value* _value)
{
unbind();
@@ -36,7 +42,7 @@ namespace genericUI
valueChanged(*m_value);
}
- void Condition::unbind()
+ void ConditionByParameterValues::unbind()
{
if(!m_value)
return;
@@ -45,9 +51,23 @@ namespace genericUI
m_value = nullptr;
}
- void Condition::refresh()
+ void ConditionByParameterValues::refresh()
{
if(m_value)
valueChanged(*m_value);
}
+
+ void ConditionByParameterValues::setCurrentPart(const Editor& _editor, const uint8_t _part)
+ {
+ unbind();
+
+ const auto v = _editor.getInterface().getParameterValue(getParameterIndex(), _part);
+ if(v)
+ bind(v);
+ }
+
+ void ConditionByKeyValue::setValue(const std::string& _value)
+ {
+ setEnabled(m_values.find(_value) != m_values.end());
+ }
}
diff --git a/source/juceUiLib/condition.h b/source/juceUiLib/condition.h
@@ -1,6 +1,6 @@
#pragma once
-#include <juce_audio_processors/juce_audio_processors.h>
+#include "juce_data_structures/juce_data_structures.h"
#include <set>
@@ -12,20 +12,55 @@ namespace juce
namespace genericUI
{
- class Condition : juce::Value::Listener
+ class Editor;
+
+ class Condition
+ {
+ public:
+ Condition(juce::Component& _target) : m_target(_target) {}
+ virtual ~Condition() = default;
+
+ virtual void setCurrentPart(const Editor& _editor, uint8_t _part) {}
+ virtual void refresh() {}
+ void setEnabled(bool _enable);
+ bool isEnabled() const { return m_enabled; }
+
+ private:
+ juce::Component& m_target;
+ bool m_enabled = false;
+ };
+
+ class ConditionByParameterValues : public Condition, juce::Value::Listener
{
public:
- Condition(juce::Component& _target, juce::Value* _value, int32_t _parameterIndex, std::set<uint8_t> _values);
- ~Condition() override;
+ ConditionByParameterValues(juce::Component& _target, juce::Value* _value, int32_t _parameterIndex, std::set<uint8_t> _values);
+ ~ConditionByParameterValues() override;
void valueChanged(juce::Value& _value) override;
void bind(juce::Value* _value);
void unbind();
int32_t getParameterIndex() const { return m_parameterIndex; }
- void refresh();
+ void refresh() override;
+ void setCurrentPart(const Editor& _editor, uint8_t _part) override;
+
private:
- juce::Component& m_target;
const int32_t m_parameterIndex;
juce::Value* m_value = nullptr;
std::set<uint8_t> m_values;
};
+
+ class ConditionByKeyValue : public Condition
+ {
+ public:
+ ConditionByKeyValue(juce::Component& _target, std::string&& _key, std::set<std::string>&& _values) : Condition(_target), m_key(std::move(_key)), m_values(std::move(_values))
+ {
+ }
+
+ void setValue(const std::string& _value);
+
+ const std::string& getKey() const { return m_key; }
+
+ private:
+ const std::string m_key;
+ const std::set<std::string> m_values;
+ };
}
diff --git a/source/juceUiLib/controllerlink.h b/source/juceUiLib/controllerlink.h
@@ -2,7 +2,7 @@
#include <string>
-#include <juce_audio_processors/juce_audio_processors.h>
+#include "juce_gui_basics/juce_gui_basics.h"
namespace genericUI
{
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"))
@@ -228,8 +242,21 @@ namespace genericUI
}
}
- void Editor::setCurrentPart(uint8_t _part)
+ void Editor::setCurrentPart(const uint8_t _part)
{
m_rootObject->setCurrentPart(*this, _part);
}
+
+ void Editor::updateKeyValueConditions(const std::string& _key, const std::string& _value) const
+ {
+ m_rootObject->updateKeyValueConditions(_key, _value);
+ }
+
+ 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
@@ -2,15 +2,16 @@
#include <string>
-#include <juce_audio_processors/juce_audio_processors.h>
+#include "juce_gui_basics/juce_gui_basics.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,27 @@ 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);
+ void updateKeyValueConditions(const std::string& _key, const std::string& _value) const;
+
+ 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 +109,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/image.h b/source/juceUiLib/image.h
@@ -1,6 +1,6 @@
#pragma once
-#include <juce_audio_processors/juce_audio_processors.h>
+#include "juce_gui_basics/juce_gui_basics.h"
namespace genericUI
{
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/rotaryStyle.h b/source/juceUiLib/rotaryStyle.h
@@ -1,7 +1,5 @@
#pragma once
-#include <juce_audio_processors/juce_audio_processors.h>
-
#include "uiObjectStyle.h"
namespace genericUI
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
@@ -1,21 +1,27 @@
#include "uiObject.h"
-#include <juce_audio_processors/juce_audio_processors.h>
-
#include <utility>
#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();
@@ -61,40 +67,51 @@ namespace genericUI
_editor.registerTabGroup(&m_tabGroup);
}
- for (auto& ch : m_children)
+ for (const auto& ch : m_children)
{
ch->createTabGroups(_editor);
}
}
- void UiObject::createControllerLinks(Editor& _editor)
+ void UiObject::createControllerLinks(Editor& _editor) const
{
- for (auto& link : m_controllerLinks)
+ for (const auto& link : m_controllerLinks)
link->create(_editor);
- for (auto& ch : m_children)
+ for (const auto& ch : m_children)
{
ch->createControllerLinks(_editor);
}
}
- void UiObject::apply(Editor& _editor, juce::Component& _target)
+ 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(const Editor& _editor, juce::Component& _target)
{
const auto x = getPropertyInt("x");
const auto y = getPropertyInt("y");
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 +163,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 +221,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 +248,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"))
{
@@ -276,38 +323,35 @@ namespace genericUI
return count;
}
- void UiObject::setCurrentPart(Editor& _editor, uint8_t _part)
+ void UiObject::setCurrentPart(Editor& _editor, const uint8_t _part)
{
if(m_condition)
- {
- m_condition->unbind();
-
- const auto v = _editor.getInterface().getParameterValue(m_condition->getParameterIndex(), _part);
- if(v)
- m_condition->bind(v);
- }
+ m_condition->setCurrentPart(_editor, _part);
for (const auto& child : m_children)
child->setCurrentPart(_editor, _part);
}
- void UiObject::createCondition(Editor& _editor, juce::Component& _target)
+ void UiObject::updateKeyValueConditions(const std::string& _key, const std::string& _value) const
{
- if(!hasComponent("condition"))
- return;
-
- const auto paramName = getProperty("enableOnParameter");
+ auto* cond = dynamic_cast<ConditionByKeyValue*>(m_condition.get());
+ if(cond && cond->getKey() == _key)
+ cond->setValue(_value);
- const auto index = _editor.getInterface().getParameterIndexByName(paramName);
+ for (const auto& child : m_children)
+ child->updateKeyValueConditions(_key, _value);
+ }
- if(index < 0)
- throw std::runtime_error("Parameter named " + paramName + " not found");
+ void UiObject::createCondition(const Editor& _editor, juce::Component& _target)
+ {
+ if(!hasComponent("condition"))
+ return;
const auto conditionValues = getProperty("enableOnValues");
size_t start = 0;
- std::set<uint8_t> values;
+ std::set<std::string> values;
for(size_t i=0; i<=conditionValues.size(); ++i)
{
@@ -317,18 +361,48 @@ namespace genericUI
continue;
const auto valueString = conditionValues.substr(start, i - start);
- const int val = strtol(valueString.c_str(), nullptr, 10);
- values.insert(static_cast<uint8_t>(val));
+
+ values.insert(valueString);
start = i + 1;
}
+
+ if(values.empty())
+ throw std::runtime_error("Condition does not specify any values");
+
+ const auto paramName = getProperty("enableOnParameter");
- const auto v = _editor.getInterface().getParameterValue(index, 0);
+ if(!paramName.empty())
+ {
+ const auto paramIndex = _editor.getInterface().getParameterIndexByName(paramName);
+
+ if(paramIndex < 0)
+ throw std::runtime_error("Parameter named " + paramName + " not found");
+
+ const auto v = _editor.getInterface().getParameterValue(paramIndex, 0);
+
+ if(!v)
+ throw std::runtime_error("Parameter named " + paramName + " not found");
+
+ std::set<uint8_t> valuesInt;
+
+ for (const auto& valueString : values)
+ {
+ const int val = strtol(valueString.c_str(), nullptr, 10);
+ valuesInt.insert(static_cast<uint8_t>(val));
+ }
+
+ m_condition.reset(new ConditionByParameterValues(_target, v, static_cast<uint32_t>(paramIndex), valuesInt));
+ }
+ else
+ {
+ auto key = getProperty("enableOnKey");
- if(!v)
- throw std::runtime_error("Parameter named " + paramName + " not found");
+ if(key.empty())
+ throw std::runtime_error("Unknown condition type, neither 'enableOnParameter' nor 'enableOnKey' specified");
- m_condition.reset(new Condition(_target, v, static_cast<uint32_t>(index), values));
+ m_condition.reset(new ConditionByKeyValue(_target, std::move(key), std::move(values)));
+ }
}
bool UiObject::parse(juce::DynamicObject* _obj)
@@ -361,6 +435,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 +539,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 +596,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 createControllerLinks(Editor& _editor) const;
+ void registerTemplates(Editor& _editor) const;
- void apply(Editor& _editor, juce::Component& _target);
+ void apply(const 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,11 +71,15 @@ namespace genericUI
void setCurrentPart(Editor& _editor, uint8_t _part);
+ const auto& getName() const { return m_name; }
+
+ void updateKeyValueConditions(const std::string& _key, const std::string& _value) const;
+
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);
+ void createCondition(const Editor& _editor, juce::Component& _target);
bool parse(juce::DynamicObject* _obj);
@@ -77,10 +90,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
@@ -42,17 +42,14 @@ namespace genericUI
auto parseColor = [&_object](juce::Colour& _target, const std::string& _prop)
{
const auto color = _object.getProperty(_prop);
-
- if(color.size() != 8)
- 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));
+ UiObjectStyle::parseColor(_target, color);
};
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 +91,43 @@ namespace genericUI
m_offsetB = _object.getPropertyInt("offsetB", -2);
}
- juce::Font UiObjectStyle::getComboBoxFont(juce::ComboBox& _comboBox)
+ std::optional<juce::Font> UiObjectStyle::getFont() const
{
- if(!m_fontFile.empty())
+ if (m_fontFile.empty())
+ return {};
+
+ auto font = juce::Font(m_editor.getFont(m_fontFile).getTypefacePtr());
+ applyFontProperties(font);
+ return font;
+ }
+
+ bool UiObjectStyle::parseColor(juce::Colour& _color, const std::string& _colorString)
+ {
+ uint32_t r,g,b,a;
+
+ if(_colorString.size() == 8)
+ {
+ sscanf(_colorString.c_str(), "%02x%02x%02x%02x", &r, &g, &b, &a);
+ }
+ else if(_colorString.size() == 6)
{
- auto font = juce::Font(m_editor.getFont(m_fontFile).getTypeface());
- applyFontProperties(font);
- return font;
+ sscanf(_colorString.c_str(), "%02x%02x%02x", &r, &g, &b);
+ a = 255;
}
+ else
+ {
+ return false;
+ }
+
+ _color = juce::Colour(static_cast<uint8_t>(r), static_cast<uint8_t>(g), static_cast<uint8_t>(b), static_cast<uint8_t>(a));
+ return true;
+ }
+
+ juce::Font UiObjectStyle::getComboBoxFont(juce::ComboBox& _comboBox)
+ {
+ if (const auto f = getFont())
+ return *f;
+
auto font = LookAndFeel_V4::getComboBoxFont(_comboBox);
applyFontProperties(font);
return font;
@@ -109,12 +135,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 +152,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 +176,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
@@ -1,6 +1,8 @@
#pragma once
-#include <juce_audio_processors/juce_audio_processors.h>
+#include "juce_gui_basics/juce_gui_basics.h"
+
+#include <optional>
namespace genericUI
{
@@ -18,6 +20,14 @@ 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;
+
+ static bool parseColor(juce::Colour& _color, const std::string& _colorString);
+
protected:
juce::Font getComboBoxFont(juce::ComboBox&) override;
juce::Font getLabelFont(juce::Label&) override;
@@ -28,19 +38,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/libresample/CMakeLists.txt b/source/libresample/CMakeLists.txt
@@ -6,4 +6,5 @@ set(SOURCES
src/resample.c
src/resamplesubs.c)
-add_library(resample STATIC ${SOURCES})
-\ No newline at end of file
+add_library(resample STATIC ${SOURCES})
+set_property(TARGET resample PROPERTY FOLDER "Gearmulator")
diff --git a/source/mqConsoleLib/CMakeLists.txt b/source/mqConsoleLib/CMakeLists.txt
@@ -22,3 +22,4 @@ target_sources(mqConsoleLib PRIVATE ${SOURCES})
source_group("source" FILES ${SOURCES})
target_link_libraries(mqConsoleLib PUBLIC mqLib cpp-terminal)
+set_property(TARGET mqConsoleLib PROPERTY FOLDER "Vavra")
diff --git a/source/mqConsoleLib/mqGui.h b/source/mqConsoleLib/mqGui.h
@@ -2,6 +2,7 @@
#include <cpp-terminal/base.hpp>
#include <cpp-terminal/window.hpp>
+#undef B0 // added on macOS via termios.h, collides with DSP Register::B0
#include "mqGuiBase.h"
diff --git a/source/mqConsoleLib/mqSettingsGui.h b/source/mqConsoleLib/mqSettingsGui.h
@@ -3,6 +3,7 @@
#include <array>
#include <cpp-terminal/window.hpp>
+#undef B0 // added on macOS via termios.h, collides with DSP Register::B0
#include "../mqConsoleLib/mqGuiBase.h"
diff --git a/source/mqJucePlugin/CMakeLists.txt b/source/mqJucePlugin/CMakeLists.txt
@@ -13,8 +13,10 @@ set(SOURCES
mqLcd.cpp mqLcd.h
mqLcdBase.cpp mqLcdBase.h
mqLcdText.cpp mqLcdText.h
+ mqPartButton.cpp mqPartButton.h
mqPartSelect.cpp mqPartSelect.h
- mqPatchBrowser.cpp mqPatchBrowser.h
+ mqPatchManager.cpp mqPatchManager.h
+ mqPatchBrowser.h
version.h
)
diff --git a/source/mqJucePlugin/PluginEditorState.cpp b/source/mqJucePlugin/PluginEditorState.cpp
@@ -20,7 +20,7 @@ void PluginEditorState::initContextMenu(juce::PopupMenu& _menu)
{
jucePluginEditorLib::PluginEditorState::initContextMenu(_menu);
- auto& p = static_cast<AudioPluginAudioProcessor&>(m_processor);
+ auto& p = m_processor;
const auto gain = static_cast<int>(std::roundf(p.getOutputGain()));
@@ -33,6 +33,38 @@ void PluginEditorState::initContextMenu(juce::PopupMenu& _menu)
_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;
+}
+
+
genericUI::Editor* PluginEditorState::createEditor(const Skin& _skin, std::function<void()> _openMenuCallback)
{
return new mqJucePlugin::Editor(m_processor, m_parameterBinding, _skin.folder, _skin.jsonFilename);
diff --git a/source/mqJucePlugin/PluginEditorState.h b/source/mqJucePlugin/PluginEditorState.h
@@ -16,6 +16,7 @@ class PluginEditorState : public jucePluginEditorLib::PluginEditorState
public:
explicit PluginEditorState(AudioPluginAudioProcessor& _processor);
void initContextMenu(juce::PopupMenu& _menu) override;
+ bool initAdvancedContextMenu(juce::PopupMenu& _menu, bool _enabled) override;
private:
genericUI::Editor* createEditor(const Skin& _skin, std::function<void()> _openMenuCallback) override;
};
diff --git a/source/mqJucePlugin/PluginProcessor.cpp b/source/mqJucePlugin/PluginProcessor.cpp
@@ -4,23 +4,24 @@
#include <juce_audio_processors/juce_audio_processors.h>
#include <juce_audio_devices/juce_audio_devices.h>
-#include "../jucePluginEditorLib/pluginEditorWindow.h"
-
#include "mqController.h"
-#include "../synthLib/os.h"
-#include "../synthLib/binarystream.h"
+#include "../jucePluginLib/processor.h"
+#include "../mqLib/device.h"
class Controller;
-static juce::PropertiesFile::Options getConfigOptions()
+namespace
{
- juce::PropertiesFile::Options opts;
- opts.applicationName = "DSP56300EmulatorVavra";
- opts.filenameSuffix = ".settings";
- opts.folderName = "DSP56300EmulatorVavra";
- opts.osxLibrarySubFolder = "Application Support/DSP56300EmulatorVavra";
- return opts;
+ juce::PropertiesFile::Options getOptions()
+ {
+ juce::PropertiesFile::Options opts;
+ opts.applicationName = "DSP56300EmulatorVavra";
+ opts.filenameSuffix = ".settings";
+ opts.folderName = "DSP56300EmulatorVavra";
+ opts.osxLibrarySubFolder = "Application Support/DSP56300EmulatorVavra";
+ return opts;
+ }
}
//==============================================================================
@@ -32,256 +33,32 @@ AudioPluginAudioProcessor::AudioPluginAudioProcessor() :
.withOutput("Out 2", juce::AudioChannelSet::stereo(), true)
.withOutput("Out 3", juce::AudioChannelSet::stereo(), true)
#endif
- , getConfigOptions())
+ , getOptions(), pluginLib::Processor::Properties{JucePlugin_Name, JucePlugin_IsSynth, JucePlugin_WantsMidiInput, JucePlugin_ProducesMidiOutput, JucePlugin_IsMidiEffect})
{
getController();
const auto latencyBlocks = getConfig().getIntValue("latencyBlocks", static_cast<int>(getPlugin().getLatencyBlocks()));
- setLatencyBlocks(latencyBlocks);
-}
-
-AudioPluginAudioProcessor::~AudioPluginAudioProcessor() = default;
-
-//==============================================================================
-const juce::String AudioPluginAudioProcessor::getName() const
-{
- return JucePlugin_Name;
-}
-
-bool AudioPluginAudioProcessor::acceptsMidi() const
-{
- #if JucePlugin_WantsMidiInput
- return true;
- #else
- return false;
- #endif
-}
-
-bool AudioPluginAudioProcessor::producesMidi() const
-{
- #if JucePlugin_ProducesMidiOutput
- return true;
- #else
- return false;
- #endif
-}
-
-bool AudioPluginAudioProcessor::isMidiEffect() const
-{
- #if JucePlugin_IsMidiEffect
- return true;
- #else
- return false;
- #endif
+ Processor::setLatencyBlocks(latencyBlocks);
}
-bool AudioPluginAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
+AudioPluginAudioProcessor::~AudioPluginAudioProcessor()
{
- // This is the place where you check if the layout is supported.
- // In this template code we only support mono or stereo.
- // Some plugin hosts, such as certain GarageBand versions, will only
- // load plugins that support stereo bus layouts.
- if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
- return false;
-
- // This checks if the input is stereo
- if (layouts.getMainInputChannelSet() != juce::AudioChannelSet::stereo())
- return false;
-
- return true;
-}
-
-void AudioPluginAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer,
- juce::MidiBuffer& midiMessages)
-{
- juce::ignoreUnused (midiMessages);
-
- juce::ScopedNoDenormals noDenormals;
- const auto totalNumInputChannels = getTotalNumInputChannels();
- const auto totalNumOutputChannels = getTotalNumOutputChannels();
-
- const int numSamples = buffer.getNumSamples();
-
- // In case we have more outputs than inputs, this code clears any output
- // channels that didn't contain input data, (because these aren't
- // guaranteed to be empty - they may contain garbage).
- // This is here to avoid people getting screaming feedback
- // when they first compile a plugin, but obviously you don't need to keep
- // this code if your algorithm always overwrites all the output channels.
- for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
- buffer.clear (i, 0, numSamples);
-
- // This is the place where you'd normally do the guts of your plugin's
- // audio processing...
- // Make sure to reset the state if your inner loop is processing
- // the samples and the outer loop is handling the channels.
- // Alternatively, you can process the samples with the channels
- // interleaved by keeping the same state.
-
- synthLib::TAudioInputs inputs{};
- synthLib::TAudioOutputs outputs{};
-
- for (int channel = 0; channel < totalNumInputChannels; ++channel)
- inputs[channel] = buffer.getReadPointer(channel);
-
- for (int channel = 0; channel < totalNumOutputChannels; ++channel)
- outputs[channel] = buffer.getWritePointer(channel);
-
- for(const auto metadata : midiMessages)
- {
- const auto message = metadata.getMessage();
-
- synthLib::SMidiEvent ev{};
-
- if(message.isSysEx() || message.getRawDataSize() > 3)
- {
- ev.sysex.resize(message.getRawDataSize());
- memcpy( &ev.sysex[0], 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
- if(ev.sysex.size() > 1)
- {
- if(ev.sysex.front() == 0xf0 && ev.sysex[1] == 0xf0)
- ev.sysex.erase(ev.sysex.begin());
-
- if(ev.sysex.size() > 1)
- {
- if(ev.sysex[ev.sysex.size()-1] == 0xf7 && ev.sysex[ev.sysex.size()-2] == 0xf7)
- ev.sysex.erase(ev.sysex.begin());
- }
- }
- }
- else
- {
- ev.a = message.getRawData()[0];
- ev.b = message.getRawDataSize() > 0 ? message.getRawData()[1] : 0;
- ev.c = message.getRawDataSize() > 1 ? message.getRawData()[2] : 0;
-
- const auto status = ev.a & 0xf0;
-
- if(status == synthLib::M_CONTROLCHANGE || status == synthLib::M_POLYPRESSURE)
- {
- // forward to UI to react to control input changes that should move knobs
- getController().addPluginMidiOut({ev});
- }
- }
-
- ev.offset = metadata.samplePosition;
-
- getPlugin().addMidiEvent(ev);
- }
-
- midiMessages.clear();
-
- juce::AudioPlayHead::CurrentPositionInfo pos{};
-
- auto* playHead = getPlayHead();
- if(playHead) {
- playHead->getCurrentPosition(pos);
-/*
- if(pos.bpm > 0) { // sync interal clock to host
- const uint8_t bpmValue = juce::jmin(127, juce::jmax(0, (int)pos.bpm-63)); // clamp to virus range, 63-190
- const auto clockParam = getController().getParameter(m_clockTempoParam, 0);
- if (clockParam != nullptr && (int)clockParam->getValueObject().getValue() != bpmValue) {
- clockParam->getValueObject().setValue(bpmValue);
- }
- }
-*/ }
-
- getPlugin().process(inputs, outputs, numSamples, static_cast<float>(pos.bpm),
- static_cast<float>(pos.ppqPosition), pos.isPlaying);
-
- for (float* output : outputs)
- {
- if (output)
- {
- for (int i = 0; i < numSamples; ++i)
- output[i] *= m_outputGain;
- }
- }
-
- m_midiOut.clear();
- getPlugin().getMidiOut(m_midiOut);
-
- if (!m_midiOut.empty())
- {
- getController().addPluginMidiOut(m_midiOut);
- }
-
- for (auto& e : m_midiOut)
- {
- if (e.source == synthLib::MidiEventSourceEditor)
- continue;
-
- auto toJuceMidiMessage = [&e]()
- {
- if(!e.sysex.empty())
- return juce::MidiMessage(&e.sysex[0], static_cast<int>(e.sysex.size()), 0.0);
- const auto len = synthLib::MidiBufferParser::lengthFromStatusByte(e.a);
- if(len == 1)
- return juce::MidiMessage(e.a, 0.0);
- if(len == 2)
- return juce::MidiMessage(e.a, e.b, 0.0);
- return juce::MidiMessage(e.a, e.b, e.c, 0.0);
- };
-
- const juce::MidiMessage message = toJuceMidiMessage();
- midiMessages.addEvent(message, 0);
-
- // additionally send to the midi output we've selected in the editor
- if (m_midiOutput)
- m_midiOutput->sendMessageNow(message);
- }
+ destroyEditorState();
}
-juce::AudioProcessorEditor* AudioPluginAudioProcessor::createEditor()
+jucePluginEditorLib::PluginEditorState* AudioPluginAudioProcessor::createEditorState()
{
- if(!m_editorState)
- m_editorState.reset(new PluginEditorState(*this));
- return new jucePluginEditorLib::EditorWindow(*this, *m_editorState, getConfig());
+ return new PluginEditorState(*this);
}
-
synthLib::Device* AudioPluginAudioProcessor::createDevice()
{
return new mqLib::Device();
}
-void AudioPluginAudioProcessor::updateLatencySamples()
-{
- if constexpr(JucePlugin_IsSynth)
- setLatencySamples(getPlugin().getLatencyMidiToOutput());
- else
- setLatencySamples(getPlugin().getLatencyInputToOutput());
-}
-
pluginLib::Controller* AudioPluginAudioProcessor::createController()
{
return new Controller(*this);
}
-void AudioPluginAudioProcessor::loadCustomData(const std::vector<uint8_t>& _sourceBuffer)
-{
- Processor::loadCustomData(_sourceBuffer);
-
- synthLib::BinaryStream<uint32_t> ss(_sourceBuffer);
- const auto version = ss.read<uint32_t>();
- if (version != 1)
- return;
- m_inputGain = ss.read<float>();
- m_outputGain = ss.read<float>();
-}
-
-void AudioPluginAudioProcessor::saveCustomData(std::vector<uint8_t>& _targetBuffer)
-{
- Processor::saveCustomData(_targetBuffer);
-
- synthLib::BinaryStream<uint32_t> ss;
- ss.write<uint32_t>(1);
- ss.write(m_inputGain);
- ss.write(m_outputGain);
-
- ss.toVector(_targetBuffer);
-}
-
//==============================================================================
// This creates new instances of the plugin..
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
diff --git a/source/mqJucePlugin/PluginProcessor.h b/source/mqJucePlugin/PluginProcessor.h
@@ -1,15 +1,7 @@
#pragma once
-#include <juce_audio_processors/juce_audio_processors.h>
-#include <juce_audio_devices/juce_audio_devices.h>
-
-#include "../synthLib/plugin.h"
#include "../jucePluginEditorLib/pluginProcessor.h"
-#include "../mqLib/device.h"
-
-class PluginEditorState;
-
//==============================================================================
class AudioPluginAudioProcessor : public jucePluginEditorLib::Processor
{
@@ -17,37 +9,16 @@ public:
AudioPluginAudioProcessor();
~AudioPluginAudioProcessor() override;
- bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
- void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
- using AudioProcessor::processBlock;
- juce::AudioProcessorEditor* createEditor() override;
-
- const juce::String getName() const override;
- bool acceptsMidi() const override;
- bool producesMidi() const override;
- bool isMidiEffect() const override;
+ jucePluginEditorLib::PluginEditorState* createEditorState() override;
// _____________
//
synthLib::Device* createDevice() override;
- void updateLatencySamples() override;
-
pluginLib::Controller* createController() override;
- void loadCustomData(const std::vector<uint8_t>& _sourceBuffer) override;
- void saveCustomData(std::vector<uint8_t>& _targetBuffer) override;
-
- float getOutputGain() const { return m_outputGain; }
- void setOutputGain(const float _gain) { m_outputGain = _gain; }
-
private:
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessor)
-
- std::unique_ptr<PluginEditorState> m_editorState;
-
- float m_inputGain = 1.0f;
- float m_outputGain = 1.0f;
};
diff --git a/source/mqJucePlugin/mqController.cpp b/source/mqJucePlugin/mqController.cpp
@@ -3,8 +3,11 @@
#include <fstream>
#include "mqEditor.h"
+#include "mqFrontPanel.h"
#include "PluginProcessor.h"
+#include "../mqLib/mqstate.h"
+
#include "../synthLib/os.h"
#include "dsp56kEmu/logging.h"
@@ -49,7 +52,7 @@ Controller::Controller(AudioPluginAudioProcessor& p, unsigned char _deviceId) :
startTimer(50);
- onPlayModeChanged.addListener(0, [this]()
+ onPlayModeChanged.addListener(0, [this](bool multiMode)
{
requestAllPatches();
});
@@ -64,11 +67,16 @@ void Controller::setFrontPanel(mqJucePlugin::FrontPanel* _frontPanel)
void Controller::sendSingle(const std::vector<uint8_t>& _sysex)
{
+ sendSingle(_sysex, getCurrentPart());
+}
+
+void Controller::sendSingle(const std::vector<uint8_t>& _sysex, const uint8_t _part)
+{
auto data = _sysex;
- data[mqLib::IdxBuffer] = static_cast<uint8_t>(isMultiMode() ? mqLib::MidiBufferNum::SingleEditBufferMultiMode : mqLib::MidiBufferNum::SingleEditBufferSingleMode);
- data[mqLib::IdxLocation] = isMultiMode() ? getCurrentPart() : 0;
- data[mqLib::IdxDeviceId] = m_deviceId;
+ data[wLib::IdxBuffer] = static_cast<uint8_t>(isMultiMode() ? mqLib::MidiBufferNum::SingleEditBufferMultiMode : mqLib::MidiBufferNum::SingleEditBufferSingleMode);
+ data[wLib::IdxLocation] = isMultiMode() ? _part : 0;
+ data[wLib::IdxDeviceId] = m_deviceId;
const auto* p = getMidiPacket(g_midiPacketNames[SingleDump]);
@@ -140,6 +148,47 @@ std::string Controller::getSingleName(const pluginLib::MidiPacket::ParamValues&
return name;
}
+std::string Controller::getSingleName(const pluginLib::MidiPacket::AnyPartParamValues& _values) const
+{
+ return getString(_values, "Name", 16);
+}
+
+std::string Controller::getCategory(const pluginLib::MidiPacket::AnyPartParamValues& _values) const
+{
+ return getString(_values, "Category", 4);
+}
+
+std::string Controller::getString(const pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _prefix, const size_t _len) const
+{
+ std::string name;
+ for(uint32_t i=0; i<_len; ++i)
+ {
+ char paramName[64];
+ snprintf(paramName, sizeof(paramName), "%s%02u", _prefix.c_str(), 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;
+}
+
+bool Controller::setSingleName(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _value) const
+{
+ return setString(_values, "Name", 16, _value);
+}
+
+bool Controller::setCategory(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _value) const
+{
+ return setString(_values, "Category", 4, _value);
+}
+
void Controller::applyPatchParameters(const pluginLib::MidiPacket::ParamValues& _params, const uint8_t _part)
{
for (const auto& it : _params)
@@ -217,13 +266,13 @@ void Controller::parseSysexMessage(const pluginLib::SysEx& _msg)
}
}
- LOG("Got sysex of size " << _msg.size())
+ LOG("Got sysex of size " << _msg.size());
std::string name;
pluginLib::MidiPacket::Data data;
pluginLib::MidiPacket::ParamValues parameterValues;
- if(parseMidiPacket(name, data, parameterValues, _msg))
+ if(pluginLib::Controller::parseMidiPacket(name, data, parameterValues, _msg))
{
if(name == midiPacketName(SingleDump))
{
@@ -240,7 +289,7 @@ void Controller::parseSysexMessage(const pluginLib::SysEx& _msg)
const auto newPlayMode = isMultiMode();
if(lastPlayMode != newPlayMode)
- onPlayModeChanged();
+ onPlayModeChanged(newPlayMode);
else
requestAllPatches();
}
@@ -279,6 +328,13 @@ void Controller::parseSysexMessage(const pluginLib::SysEx& _msg)
}
}
+bool Controller::parseMidiPacket(MidiPacketType _type, pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _params, const pluginLib::SysEx& _sysex) const
+{
+ const auto* p = getMidiPacket(g_midiPacketNames[_type]);
+ assert(p && "midi packet not found");
+ return pluginLib::Controller::parseMidiPacket(*p, _data, _params, _sysex);
+}
+
bool Controller::sendSysEx(MidiPacketType _type) const
{
std::map<pluginLib::MidiDataType, uint8_t> params;
@@ -305,7 +361,7 @@ void Controller::setPlayMode(const bool _multiMode)
sendGlobalParameterChange(mqLib::GlobalParameter::SingleMultiMode, playMode);
- onPlayModeChanged();
+ onPlayModeChanged(_multiMode);
}
void Controller::selectNextPreset()
@@ -334,6 +390,45 @@ std::vector<uint8_t> Controller::createSingleDump(const mqLib::MidiBufferNum _bu
return dst;
}
+std::vector<uint8_t> Controller::createSingleDump(mqLib::MidiBufferNum _buffer, mqLib::MidiSoundLocation _location, const uint8_t _locationOffset, const pluginLib::MidiPacket::AnyPartParamValues& _values) const
+{
+ pluginLib::MidiPacket::Data data;
+
+ data.insert(std::make_pair(pluginLib::MidiDataType::DeviceId, m_deviceId));
+ data.insert(std::make_pair(pluginLib::MidiDataType::Bank, static_cast<uint8_t>(_buffer)));
+ data.insert(std::make_pair(pluginLib::MidiDataType::Program, static_cast<uint8_t>(_location) + _locationOffset));
+
+ std::vector<uint8_t> dst;
+
+ if (!createMidiDataFromPacket(dst, midiPacketName(SingleDump), data, _values))
+ return {};
+
+ return dst;
+}
+
+bool Controller::parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _paramValues, const std::vector<uint8_t>& _sysex) const
+{
+ if(parseMidiPacket(SingleDump, _data, _paramValues, _sysex))
+ return true;
+ return parseMidiPacket(SingleDumpQ, _data, _paramValues, _sysex);
+}
+
+bool Controller::setString(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _prefix, size_t _len, const std::string& _value) const
+{
+ for(uint32_t i=0; i<_len && i <_value.size(); ++i)
+ {
+ char paramName[64];
+ snprintf(paramName, sizeof(paramName), "%s%02u", _prefix.c_str(), i);
+
+ const auto idx = getParameterIndexByName(paramName);
+ if(idx == InvalidParameterIndex)
+ break;
+
+ _values[idx] = static_cast<uint8_t>(_value[i]);
+ }
+ return true;
+}
+
void Controller::selectPreset(int _offset)
{
auto& current = isMultiMode() ? m_currentSingles[getCurrentPart()] : m_currentSingle;
@@ -379,7 +474,7 @@ void Controller::sendParameterChange(const pluginLib::Parameter& _parameter, con
if (!combineParameterChange(v, g_midiPacketNames[MultiDump], _parameter, _value))
return;
- const auto& dump = mqLib::State::g_dumps[static_cast<int>(mqLib::State::DumpType::Multi)];
+ const auto& dump = mqLib::State::Dumps[static_cast<int>(mqLib::State::DumpType::Multi)];
uint32_t idx = desc.index;
diff --git a/source/mqJucePlugin/mqController.h b/source/mqJucePlugin/mqController.h
@@ -46,13 +46,14 @@ public:
std::vector<uint8_t> data;
};
- pluginLib::Event onPlayModeChanged;
+ pluginLib::Event<bool> onPlayModeChanged;
Controller(AudioPluginAudioProcessor &, unsigned char _deviceId = 0);
~Controller() override;
void setFrontPanel(mqJucePlugin::FrontPanel* _frontPanel);
void sendSingle(const std::vector<uint8_t>& _sysex);
+ void sendSingle(const std::vector<uint8_t>& _sysex, uint8_t _part);
bool sendSysEx(MidiPacketType _type) const;
bool sendSysEx(MidiPacketType _type, std::map<pluginLib::MidiDataType, uint8_t>& _params) const;
@@ -64,6 +65,18 @@ public:
void selectPrevPreset();
std::vector<uint8_t> createSingleDump(mqLib::MidiBufferNum _buffer, mqLib::MidiSoundLocation _location, uint8_t _locationOffset, uint8_t _part) const;
+ std::vector<uint8_t> createSingleDump(mqLib::MidiBufferNum _buffer, mqLib::MidiSoundLocation _location, uint8_t _locationOffset, const pluginLib::MidiPacket
+ ::AnyPartParamValues& _values) const;
+ bool parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _paramValues, const std::vector<uint8_t>& _sysex) const;
+
+ std::string getSingleName(const pluginLib::MidiPacket::ParamValues& _values) const;
+ std::string getSingleName(const pluginLib::MidiPacket::AnyPartParamValues& _values) const;
+ std::string getCategory(const pluginLib::MidiPacket::AnyPartParamValues& _values) const;
+ std::string getString(const pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _prefix, size_t _len) const;
+
+ bool setSingleName(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _value) const;
+ bool setCategory(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _value) const;
+ bool setString(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _prefix, size_t _len, const std::string& _value) const;
private:
void selectPreset(int _offset);
@@ -73,11 +86,11 @@ private:
void timerCallback() override;
void onStateLoaded() override;
- std::string getSingleName(const pluginLib::MidiPacket::ParamValues& _values) const;
void applyPatchParameters(const pluginLib::MidiPacket::ParamValues& _params, uint8_t _part);
void parseSingle(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _params);
void parseMulti(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _params);
void parseSysexMessage(const pluginLib::SysEx&) override;
+ bool parseMidiPacket(MidiPacketType _type, pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _params, const pluginLib::SysEx& _sysex) const;
void sendParameterChange(const pluginLib::Parameter& _parameter, uint8_t _value) override;
bool sendGlobalParameterChange(mqLib::GlobalParameter _param, uint8_t _value);
diff --git a/source/mqJucePlugin/mqEditor.cpp b/source/mqJucePlugin/mqEditor.cpp
@@ -4,8 +4,10 @@
#include "PluginProcessor.h"
#include "mqController.h"
-
-#include "../jucePluginLib/parameterbinding.h"
+#include "mqFrontPanel.h"
+#include "mqPartButton.h"
+#include "mqPartSelect.h"
+#include "mqPatchManager.h"
#include "../mqLib/mqbuildconfig.h"
@@ -21,8 +23,22 @@ namespace mqJucePlugin
m_frontPanel.reset(new FrontPanel(*this, m_controller));
- if(findComponent("ContainerPatchList", false))
- m_patchBrowser.reset(new PatchBrowser(*this, _processor.getController(), _processor.getConfig()));
+ if(auto* container = findComponent("patchbrowser", false))
+ {
+ constexpr auto scale = 1.3f;
+ const float x = static_cast<float>(container->getX());
+ const float y = static_cast<float>(container->getY());
+ const float w = static_cast<float>(container->getWidth());
+ const float h = static_cast<float>(container->getHeight());
+ container->setTransform(juce::AffineTransform::scale(scale, scale));
+ container->setSize(static_cast<int>(w / scale),static_cast<int>(h / scale));
+ container->setTopLeftPosition(static_cast<int>(x / scale),static_cast<int>(y / scale));
+
+ const auto configOptions = getProcessor().getConfigOptions();
+ const auto dir = configOptions.getDefaultFile().getParentDirectory();
+
+ setPatchManager(new PatchManager(*this, container, dir));
+ }
auto disableButton = [](juce::Component* _comp)
{
@@ -90,7 +106,7 @@ namespace mqJucePlugin
addMouseListener(this, true);
- m_controller.onPlayModeChanged.addListener(PlayModeListenerId, [this]()
+ m_controller.onPlayModeChanged.addListener(PlayModeListenerId, [this](bool)
{
if(m_btPlayModeMulti)
m_btPlayModeMulti->setToggleState(m_controller.isMultiMode(), juce::dontSendNotification);
@@ -128,16 +144,35 @@ namespace mqJucePlugin
return findEmbeddedResource(_filename, _size);
}
+ std::pair<std::string, std::string> Editor::getDemoRestrictionText() const
+ {
+ return {"Vavra",
+ "Vavra runs in demo mode\n"
+ "\n"
+ "The following features are disabled:\n"
+ "- Saving/Exporting Presets\n"
+ "- Plugin state is not preserve"
+ };
+ }
+
+ genericUI::Button<juce::DrawableButton>* Editor::createJuceComponent(genericUI::Button<juce::DrawableButton>* _button, genericUI::UiObject& _object, const std::string& _name, juce::DrawableButton::ButtonStyle _buttonStyle)
+ {
+ if(_object.getName() == "partSelectButton")
+ return new mqPartButton(*this, _name, _buttonStyle);
+
+ return jucePluginEditorLib::Editor::createJuceComponent(_button, _object, _name, _buttonStyle);
+ }
+
void Editor::mouseEnter(const juce::MouseEvent& _event)
{
m_focusedParameter->onMouseEnter(_event);
}
- void Editor::savePreset(const FileType _type)
+ void Editor::savePreset(const jucePluginEditorLib::FileType _type)
{
jucePluginEditorLib::Editor::savePreset([&](const juce::File& _file)
{
- FileType type = _type;
+ jucePluginEditorLib::FileType type = _type;
const auto file = createValidFilename(type, _file);
const auto part = m_controller.getCurrentPart();
@@ -154,17 +189,17 @@ namespace mqJucePlugin
{
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)
{
savePreset(_type);
});
@@ -174,15 +209,15 @@ namespace mqJucePlugin
void Editor::onBtPresetPrev()
{
- if (m_patchBrowser->selectPrevPreset())
+ if (getPatchManager() && getPatchManager()->selectPrevPreset(m_controller.getCurrentPart()))
return;
-// m_controller.selectPrevPreset();
+ m_controller.selectPrevPreset();
}
void Editor::onBtPresetNext()
{
- if (m_patchBrowser->selectNextPreset())
+ if (getPatchManager() && getPatchManager()->selectNextPreset(m_controller.getCurrentPart()))
return;
-// m_controller.selectNextPreset();
+ m_controller.selectNextPreset();
}
}
diff --git a/source/mqJucePlugin/mqEditor.h b/source/mqJucePlugin/mqEditor.h
@@ -3,9 +3,8 @@
#include "../jucePluginEditorLib/pluginEditor.h"
#include "../jucePluginEditorLib/focusedParameter.h"
-#include "mqFrontPanel.h"
-#include "mqPatchBrowser.h"
-#include "mqPartSelect.h"
+class mqPartSelect;
+class Controller;
namespace jucePluginEditorLib
{
@@ -20,18 +19,37 @@ namespace pluginLib
namespace mqJucePlugin
{
+ class FrontPanel;
+ class PatchManager;
+
class Editor final : public jucePluginEditorLib::Editor
{
public:
Editor(jucePluginEditorLib::Processor& _processor, pluginLib::ParameterBinding& _binding, std::string _skinFolder, const std::string& _jsonFilename);
~Editor() override;
+
+ Editor(Editor&&) = delete;
+ Editor(const Editor&) = delete;
+ Editor& operator = (Editor&&) = delete;
+ Editor& operator = (const Editor&) = delete;
+
static const char* findEmbeddedResource(const std::string& _filename, uint32_t& _size);
const char* findResourceByFilename(const std::string& _filename, uint32_t& _size) override;
+ std::pair<std::string, std::string> getDemoRestrictionText() const override;
+
+ Controller& getMqController() const { return m_controller; }
+
+ genericUI::Button<juce::DrawableButton>* createJuceComponent(genericUI::Button<juce::DrawableButton>*, genericUI::UiObject& _object, const std::string& _name, juce::DrawableButton::ButtonStyle) override;
+
+ mqPartSelect* getPartSelect() const
+ {
+ return m_partSelect.get();
+ }
private:
void mouseEnter(const juce::MouseEvent& _event) override;
- void savePreset(FileType _type);
+ void savePreset(jucePluginEditorLib::FileType _type);
void onBtSave();
void onBtPresetPrev();
@@ -40,7 +58,6 @@ namespace mqJucePlugin
Controller& m_controller;
std::unique_ptr<FrontPanel> m_frontPanel;
- std::unique_ptr<PatchBrowser> m_patchBrowser;
std::unique_ptr<mqPartSelect> m_partSelect;
juce::Button* m_btPlayModeMulti = nullptr;
juce::Button* m_btSave = nullptr;
diff --git a/source/mqJucePlugin/mqFrontPanel.h b/source/mqJucePlugin/mqFrontPanel.h
@@ -5,7 +5,7 @@
#include "mqLcdBase.h"
-#include <juce_audio_processors/juce_audio_processors.h>
+#include "juce_gui_basics/juce_gui_basics.h"
class Controller;
diff --git a/source/mqJucePlugin/mqLcd.cpp b/source/mqJucePlugin/mqLcd.cpp
@@ -1,179 +1,44 @@
#include "mqLcd.h"
#include "version.h"
-#include "../mqLib/lcdfonts.h"
-// EW20290GLW display simulation
+#include "../wLib/lcdfonts.h"
-constexpr int g_pixelsPerCharW = 5;
-constexpr int g_pixelsPerCharH = 8;
+// EW20290GLW display simulation (20*2)
-constexpr int g_numCharsW = 20;
-constexpr int g_numCharsH = 2;
-
-constexpr float g_pixelSpacingAdjust = 3.0f; // 1.0f = 100% as on the hardware, but it looks better on screen if its a bit more
-
-constexpr float g_pixelSpacingW = 0.05f * g_pixelSpacingAdjust;
-constexpr float g_pixelSizeW = 0.6f;
-constexpr float g_charSpacingW = 0.4f;
-
-constexpr float g_charSizeW = g_pixelsPerCharW * g_pixelSizeW + g_pixelSpacingW * (g_pixelsPerCharW - 1);
-constexpr float g_sizeW = g_numCharsW * g_charSizeW + g_charSpacingW * (g_numCharsW - 1);
-constexpr float g_pixelStrideW = g_pixelSizeW + g_pixelSpacingW;
-constexpr float g_charStrideW = g_charSizeW + g_charSpacingW;
-
-constexpr float g_pixelSpacingH = 0.05f * g_pixelSpacingAdjust;
-constexpr float g_pixelSizeH = 0.65f;
-constexpr float g_charSpacingH = 0.4f;
-
-constexpr float g_charSizeH = g_pixelsPerCharH * g_pixelSizeH + g_pixelSpacingH * (g_pixelsPerCharH - 1);
-constexpr float g_sizeH = g_numCharsH * g_charSizeH + g_charSpacingH * (g_numCharsH - 1);
-constexpr float g_pixelStrideH = g_pixelSizeH + g_pixelSpacingH;
-constexpr float g_charStrideH = g_charSizeH + g_charSpacingH;
-
-constexpr int g_numPixelsW = g_numCharsW * g_pixelsPerCharW + g_numCharsW - 1;
-constexpr int g_numPixelsH = g_numCharsH * g_pixelsPerCharH + g_numCharsH - 1;
-
-constexpr float g_pixelBorder = 0.1f;
-
-MqLcd::MqLcd(Component& _parent) : juce::Button("mqLCDButton"), m_scaleW(static_cast<float>(_parent.getWidth()) / g_sizeW), m_scaleH(static_cast<float>(_parent.getHeight()) / g_sizeH)
+MqLcd::MqLcd(Component& _parent) : Lcd(_parent, 20, 2)
{
- setSize(_parent.getWidth(), _parent.getHeight());
-
- _parent.addAndMakeVisible(this);
-
- for (uint32_t i=16; i<256; ++i)
- {
- m_characterPaths[i] = createPath(static_cast<uint8_t>(i));
- }
-
- const std::string color = _parent.getProperties().getWithDefault("lcdBackgroundColor", juce::String()).toString().toStdString();
-
- if (color.size() == 6)
- {
- m_charBgColor = strtol(color.c_str(), nullptr, 16) | 0xff000000;
- }
-
- m_text.fill(255); // block character
- m_cgData.fill({0});
-
- onClick = [&]()
- {
- onClicked();
- };
-
- setEnabled(true);
+ postConstruct();
}
MqLcd::~MqLcd() = default;
void MqLcd::setText(const std::array<uint8_t, 40>& _text)
{
- if (m_text == _text)
- return;
-
- m_text = _text;
-
- repaint();
+ const std::vector<uint8_t> text{_text.begin(), _text.end()};
+ Lcd::setText(text);
}
void MqLcd::setCgRam(std::array<uint8_t, 64>& _data)
{
- for (auto i=0; i<m_cgData.size(); ++i)
- {
- std::array<uint8_t, 8> c{};
- memcpy(c.data(), &_data[i*8], 8);
-
- if (c != m_cgData[i])
- {
- m_cgData[i] = c;
- m_characterPaths[i] = createPath(i);
- }
- }
-
- repaint();
+ Lcd::setCgRam(_data);
}
-void MqLcd::paint(juce::Graphics& _g)
+bool MqLcd::getOverrideText(std::vector<std::vector<uint8_t>>& _lines)
{
- const auto& text = m_overrideText[0] ? m_overrideText : m_text;
-
- uint32_t charIdx = 0;
-
- for (auto y=0; y<2; ++y)
- {
- const auto ty = static_cast<float>(y) * g_charStrideH * m_scaleH;
-
- for (auto x = 0; x < 20; ++x, ++charIdx)
- {
- const auto tx = static_cast<float>(x) * g_charStrideW * m_scaleW;
-
- const auto t = juce::AffineTransform::translation(tx, ty);
-
- const auto c = text[charIdx];
- const auto& p = m_characterPaths[c];
-
- if (m_charBgColor)
- {
- _g.setColour(juce::Colour(m_charBgColor));
- _g.fillPath(m_characterPaths[255], t);
- }
- _g.setColour(juce::Colour(0xff000000));
- _g.fillPath(p, t);
- }
- }
-}
-
-juce::Path MqLcd::createPath(const uint8_t _character) const
-{
- const auto* data = _character < m_cgData.size() ? m_cgData[_character].data() : mqLib::getCharacterData(_character);
-
- juce::Path path;
-
- const auto h = g_pixelSizeH * m_scaleH;
- const auto w = g_pixelSizeW * m_scaleW;
-
- for (auto y=0; y<8; ++y)
- {
- const auto y0 = static_cast<float>(y) * g_pixelStrideH * m_scaleH;
-
- for (auto x=0; x<=4; ++x)
- {
- const auto bit = 4-x;
-
- const auto set = data[y] & (1<<bit);
-
- if(!set)
- continue;
-
- const auto x0 = static_cast<float>(x) * g_pixelStrideW * m_scaleW;
-
- path.addRectangle(x0, y0, w, h);
- }
- }
-
- return path;
-}
-
-void MqLcd::onClicked()
-{
- if(isTimerRunning())
- return;
-
const std::string lineA(std::string("Vavra v") + g_pluginVersionString);
const std::string lineB = __DATE__ " " __TIME__;
- m_overrideText.fill(' ');
-
- memcpy(m_overrideText.data(), lineA.c_str(), std::min(std::size(lineA), static_cast<size_t>(20)));
- memcpy(&m_overrideText[20], lineB.c_str(), std::min(std::size(lineB), static_cast<size_t>(20)));
+ _lines =
+ {
+ std::vector<uint8_t>(lineA.begin(), lineA.end()),
+ std::vector<uint8_t>(lineB.begin(), lineB.end())
+ };
- startTimer(3000);
+ return true;
}
-void MqLcd::timerCallback()
+const uint8_t* MqLcd::getCharacterData(uint8_t _character) const
{
- stopTimer();
- m_overrideText[0] = 0;
- repaint();
+ return wLib::getCharacterData(_character);
}
diff --git a/source/mqJucePlugin/mqLcd.h b/source/mqJucePlugin/mqLcd.h
@@ -1,33 +1,20 @@
#pragma once
-#include <juce_audio_processors/juce_audio_processors.h>
+#include "juce_gui_basics/juce_gui_basics.h"
#include "mqLcdBase.h"
-class MqLcd final : juce::Button, public MqLcdBase, juce::Timer
+#include "../jucePluginEditorLib/lcd.h"
+
+class MqLcd final : public jucePluginEditorLib::Lcd, public MqLcdBase
{
public:
- explicit MqLcd(juce::Component& _parent);
+ explicit MqLcd(Component& _parent);
~MqLcd() override;
void setText(const std::array<uint8_t, 40> &_text) override;
- void setCgRam(std::array<uint8_t, 64> &_data) override;
-
-private:
- void paintButton(juce::Graphics& g, bool shouldDrawButtonAsHighlighted, bool shouldDrawButtonAsDown) override {}
- void paint(juce::Graphics& _g) override;
- juce::Path createPath(uint8_t _character) const;
- void onClicked();
- void timerCallback() override;
-
-private:
- std::array<juce::Path, 256> m_characterPaths;
-
- const float m_scaleW;
- const float m_scaleH;
+ void setCgRam(std::array<uint8_t, 64>& _data) override;
- std::array<uint8_t, 40> m_overrideText{0};
- std::array<uint8_t, 40> m_text{' '};
- std::array<std::array<uint8_t, 8>, 8> m_cgData{0};
- uint32_t m_charBgColor = 0;
+ bool getOverrideText(std::vector<std::vector<uint8_t>>& _lines) override;
+ const uint8_t* getCharacterData(uint8_t _character) const override;
};
diff --git a/source/mqJucePlugin/mqLcdBase.h b/source/mqJucePlugin/mqLcdBase.h
@@ -1,6 +1,7 @@
#pragma once
-#include <juce_audio_processors/juce_audio_processors.h>
+#include <array>
+#include <cstdint>
class MqLcdBase
{
diff --git a/source/mqJucePlugin/mqLcdText.cpp b/source/mqJucePlugin/mqLcdText.cpp
@@ -1,5 +1,7 @@
#include "mqLcdText.h"
+#include "juce_gui_basics/juce_gui_basics.h"
+
MqLcdText::MqLcdText(juce::Label &_lineA, juce::Label &_lineB) : m_lineA(_lineA), m_lineB(_lineB)
{
m_lineA.setJustificationType(juce::Justification::centredLeft);
diff --git a/source/mqJucePlugin/mqPartButton.cpp b/source/mqJucePlugin/mqPartButton.cpp
@@ -0,0 +1,19 @@
+#include "mqPartButton.h"
+#include "mqEditor.h"
+#include "mqPartSelect.h"
+
+mqPartButton::mqPartButton(mqJucePlugin::Editor& _editor, const std::string& _name, ButtonStyle _buttonStyle)
+: PartButton(_editor, _name, _buttonStyle)
+, m_mqEditor(_editor)
+{
+}
+
+bool mqPartButton::isInterestedInDragSource(const SourceDetails& dragSourceDetails)
+{
+ return PartButton<DrawableButton>::isInterestedInDragSource(dragSourceDetails);
+}
+
+void mqPartButton::onClick()
+{
+ m_mqEditor.getPartSelect()->selectPart(getPart());
+}
diff --git a/source/mqJucePlugin/mqPartButton.h b/source/mqJucePlugin/mqPartButton.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "../jucePluginEditorLib/partbutton.h"
+
+class mqPartSelect;
+
+namespace mqJucePlugin
+{
+ class Editor;
+}
+
+class mqPartButton : public jucePluginEditorLib::PartButton<juce::DrawableButton>
+{
+public:
+ explicit mqPartButton(mqJucePlugin::Editor& _editor, const std::string& _name, ButtonStyle _buttonStyle);
+
+ bool isInterestedInDragSource(const SourceDetails& dragSourceDetails) override;
+
+ void onClick() override;
+
+private:
+ mqJucePlugin::Editor& m_mqEditor;
+};
diff --git a/source/mqJucePlugin/mqPartSelect.cpp b/source/mqJucePlugin/mqPartSelect.cpp
@@ -2,13 +2,14 @@
#include "mqController.h"
#include "mqEditor.h"
+#include "mqPartButton.h"
mqPartSelect::mqPartSelect(mqJucePlugin::Editor& _editor, Controller& _controller, pluginLib::ParameterBinding& _parameterBinding)
: m_editor(_editor)
, m_controller(_controller)
, m_parameterBinding(_parameterBinding)
{
- std::vector<juce::Button*> buttons;
+ std::vector<mqPartButton*> buttons;
std::vector<juce::Button*> leds;
_editor.findComponents(buttons, "partSelectButton", 16);
@@ -23,7 +24,8 @@ mqPartSelect::mqPartSelect(mqJucePlugin::Editor& _editor, Controller& _controlle
auto index = static_cast<uint8_t>(i);
- part.button->onClick = [this,index] { selectPart(index); };
+ part.button->initalize(static_cast<uint8_t>(i));
+
part.led->onClick = [this, index] { selectPart(index); };
}
diff --git a/source/mqJucePlugin/mqPartSelect.h b/source/mqJucePlugin/mqPartSelect.h
@@ -2,6 +2,7 @@
#include "../jucePluginLib/parameterbinding.h"
+class mqPartButton;
class Controller;
namespace mqJucePlugin
@@ -16,13 +17,14 @@ public:
void onPlayModeChanged() const;
+ void selectPart(uint8_t _index) const;
+
private:
void updateUiState() const;
- void selectPart(uint8_t _index) const;
struct Part
{
- juce::Button* button = nullptr;
+ mqPartButton* button = nullptr;
juce::Button* led = nullptr;
};
diff --git a/source/mqJucePlugin/mqPatchManager.cpp b/source/mqJucePlugin/mqPatchManager.cpp
@@ -0,0 +1,120 @@
+#include "mqPatchManager.h"
+
+#include "mqController.h"
+#include "mqEditor.h"
+#include "../jucePluginEditorLib/pluginProcessor.h"
+
+namespace mqJucePlugin
+{
+ PatchManager::PatchManager(Editor& _editor, juce::Component* _root, const juce::File& _dir)
+ : jucePluginEditorLib::patchManager::PatchManager(_editor, _root, _dir)
+ , m_editor(_editor)
+ , m_controller(_editor.getMqController())
+ {
+ startLoaderThread();
+ }
+
+ PatchManager::~PatchManager()
+ {
+ stopLoaderThread();
+ }
+
+ bool PatchManager::requestPatchForPart(pluginLib::patchDB::Data& _data, uint32_t _part)
+ {
+ _data = m_controller.createSingleDump(mqLib::MidiBufferNum::SingleBankA, static_cast<mqLib::MidiSoundLocation>(0), _part, _part);
+ return !_data.empty();
+ }
+
+ bool PatchManager::loadRomData(pluginLib::patchDB::DataList& _results, uint32_t _bank, uint32_t _program)
+ {
+ return false;
+ }
+
+ pluginLib::patchDB::PatchPtr PatchManager::initializePatch(pluginLib::patchDB::Data&& _sysex)
+ {
+ pluginLib::MidiPacket::Data data;
+ pluginLib::MidiPacket::AnyPartParamValues parameters;
+ if(!m_controller.parseSingle(data, parameters, _sysex))
+ return {};
+
+ auto p = std::make_shared<pluginLib::patchDB::Patch>();
+
+ p->sysex = std::move(_sysex);
+ p->name = m_controller.getSingleName(parameters);
+
+ auto category = m_controller.getCategory(parameters);
+
+ while(!category.empty() && isspace(category.back()))
+ category.pop_back();
+ while(!category.empty() && isspace(category.front()))
+ category.erase(0);
+
+ if(!category.empty())
+ p->tags.add(pluginLib::patchDB::TagType::Category, category);
+
+ return p;
+ }
+
+ 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.setSingleName(parameterValues, _patch->getName());
+
+ // apply program
+ uint32_t program = 0;
+ uint32_t bank = 0;
+ if(_patch->program != pluginLib::patchDB::g_invalidProgram)
+ {
+ program = std::clamp(_patch->program, 0u, 299u);
+
+ bank = program / 100;
+ program -= bank * 100;
+ }
+
+ // apply category
+ const auto& tags = _patch->getTags(pluginLib::patchDB::TagType::Category).getAdded();
+
+ if(!tags.empty())
+ m_controller.setCategory(parameterValues, *tags.begin());
+
+ return m_controller.createSingleDump(
+ static_cast<mqLib::MidiBufferNum>(static_cast<uint8_t>(mqLib::MidiBufferNum::DeprecatedSingleBankA) + bank),
+ static_cast<mqLib::MidiSoundLocation>(static_cast<uint8_t>(mqLib::MidiSoundLocation::AllSinglesBankA) + bank),
+ static_cast<uint8_t>(program), parameterValues);
+ }
+
+ bool PatchManager::equals(const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b) const
+ {
+ if(_a == _b)
+ return true;
+
+ if(_a->hash == _b->hash)
+ return true;
+
+ return false;
+ }
+
+ uint32_t PatchManager::getCurrentPart() const
+ {
+ return m_editor.getProcessor().getController().getCurrentPart();
+ }
+
+ bool PatchManager::activatePatch(const pluginLib::patchDB::PatchPtr& _patch)
+ {
+ m_controller.sendSingle(_patch->sysex);
+ return true;
+ }
+
+ bool PatchManager::activatePatch(const pluginLib::patchDB::PatchPtr& _patch, uint32_t _part)
+ {
+ m_controller.sendSingle(_patch->sysex, static_cast<uint8_t>(_part));
+ return true;
+ }
+}
diff --git a/source/mqJucePlugin/mqPatchManager.h b/source/mqJucePlugin/mqPatchManager.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "../jucePluginEditorLib/patchmanager/patchmanager.h"
+
+class Controller;
+
+namespace mqJucePlugin
+{
+ class Editor;
+
+ class PatchManager : public jucePluginEditorLib::patchManager::PatchManager
+ {
+ public:
+ PatchManager(Editor& _editor, juce::Component* _root, const juce::File& _dir);
+ ~PatchManager();
+
+ // PatchManager overrides
+ bool requestPatchForPart(pluginLib::patchDB::Data& _data, uint32_t _part) override;
+ bool loadRomData(pluginLib::patchDB::DataList& _results, uint32_t _bank, uint32_t _program) override;
+ pluginLib::patchDB::PatchPtr initializePatch(pluginLib::patchDB::Data&& _sysex) override;
+ pluginLib::patchDB::Data prepareSave(const pluginLib::patchDB::PatchPtr& _patch) const override;
+ bool equals(const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b) const override;
+ uint32_t getCurrentPart() const override;
+ bool activatePatch(const pluginLib::patchDB::PatchPtr& _patch) override;
+ bool activatePatch(const pluginLib::patchDB::PatchPtr& _patch, uint32_t _part) override;
+
+ private:
+ Editor& m_editor;
+ Controller& m_controller;
+ };
+}
diff --git a/source/mqJucePlugin/skins/mqDefault/mqDefault.json b/source/mqJucePlugin/skins/mqDefault/mqDefault.json
@@ -26,6 +26,87 @@
"pageFx"
]
},
+ "templates" : [
+ {
+ "name" : "pm_treeview",
+ "treeview" : {
+ "alignH" : "L",
+ "alignV" : "C",
+ "color" : "ffda94",
+ "backgroundColor" : "1c1f22ff",
+ "selectedItemBackgroundColor" : "a87a24"
+ }
+ },
+ {
+ "name" : "pm_search",
+ "texteditor" : {
+ "alignH" : "L",
+ "alignV" : "C",
+ "color" : "ffda94",
+ "outlineColor" : "cf972e",
+ "backgroundColor" : "0e1216ff",
+ "text": "Filter..."
+ }
+ },
+ {
+ "name" : "pm_scrollbar",
+ "scrollbar" : {
+ "color" : "cf972e",
+ }
+ },
+ {
+ "name" : "pm_listbox",
+ "listbox" : {
+ "alignH" : "L",
+ "alignV" : "C",
+ "color" : "ffda94",
+ "backgroundColor" : "1c1f22ff",
+ "selectedItemBackgroundColor" : "a87a24",
+ "bold": "1",
+ "textHeight": 30
+ }
+ },
+ {
+ "name" : "pm_info_name",
+ "label" : {
+ "alignH" : "L",
+ "alignV" : "T",
+ "color" : "ffda94",
+ "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" : "ffda94",
+ "textHeight" : "18"
+ }
+ },
+ {
+ "name" : "pm_status_label",
+ "label" : {
+ "alignH" : "L",
+ "alignV" : "C",
+ "color" : "ffda94",
+ "backgroundColor" : "1c1f22ff",
+ "textHeight" : "18",
+ "bold": "0"
+ }
+ }
+ ],
"children" : [
{
"name" : "bakedBG",
@@ -233,6 +314,7 @@
"height" : "108"
},
"componentProperties" : {
+ "lcdTextColor" : "000000",
"lcdBackgroundColor" : "248727"
}
},
@@ -2227,7 +2309,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "1806.7",
+ "x" : "1793.657",
"y" : "177.7593",
"width" : "128",
"height" : "128",
@@ -2244,7 +2326,24 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "1989.3",
+ "x" : "1950.171",
+ "y" : "177.7593",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FilterEnvAttackLevel"
+ }
+ },
+ {
+ "name" : "knob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2106.686",
"y" : "177.7593",
"width" : "128",
"height" : "128",
@@ -2261,7 +2360,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2171.9",
+ "x" : "2263.2",
"y" : "177.7593",
"width" : "128",
"height" : "128",
@@ -2278,7 +2377,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2354.5",
+ "x" : "2419.714",
"y" : "177.7593",
"width" : "128",
"height" : "128",
@@ -2295,7 +2394,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2537.1",
+ "x" : "2576.229",
"y" : "177.7593",
"width" : "128",
"height" : "128",
@@ -2312,7 +2411,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2719.7",
+ "x" : "2732.743",
"y" : "177.7593",
"width" : "128",
"height" : "128",
@@ -2395,7 +2494,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "1806.7",
+ "x" : "1793.657",
"y" : "559.0093",
"width" : "128",
"height" : "128",
@@ -2412,7 +2511,24 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "1989.3",
+ "x" : "1950.171",
+ "y" : "559.0093",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpEnvAttackLevel"
+ }
+ },
+ {
+ "name" : "knob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2106.686",
"y" : "559.0093",
"width" : "128",
"height" : "128",
@@ -2429,7 +2545,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2171.9",
+ "x" : "2263.2",
"y" : "559.0093",
"width" : "128",
"height" : "128",
@@ -2446,7 +2562,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2354.5",
+ "x" : "2419.714",
"y" : "559.0093",
"width" : "128",
"height" : "128",
@@ -2463,7 +2579,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2537.1",
+ "x" : "2576.229",
"y" : "559.0093",
"width" : "128",
"height" : "128",
@@ -2480,7 +2596,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2719.7",
+ "x" : "2732.743",
"y" : "559.0093",
"width" : "128",
"height" : "128",
@@ -2563,7 +2679,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "1806.7",
+ "x" : "1793.657",
"y" : "940.2593",
"width" : "128",
"height" : "128",
@@ -2580,7 +2696,24 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "1989.3",
+ "x" : "1950.171",
+ "y" : "940.2593",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Env3AttackLevel"
+ }
+ },
+ {
+ "name" : "knob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2106.686",
"y" : "940.2593",
"width" : "128",
"height" : "128",
@@ -2597,7 +2730,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2171.9",
+ "x" : "2263.2",
"y" : "940.2593",
"width" : "128",
"height" : "128",
@@ -2614,7 +2747,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2354.5",
+ "x" : "2419.714",
"y" : "940.2593",
"width" : "128",
"height" : "128",
@@ -2631,7 +2764,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2537.1",
+ "x" : "2576.229",
"y" : "940.2593",
"width" : "128",
"height" : "128",
@@ -2648,7 +2781,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2719.7",
+ "x" : "2732.743",
"y" : "940.2593",
"width" : "128",
"height" : "128",
@@ -2731,7 +2864,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "1806.7",
+ "x" : "1793.657",
"y" : "1321.509",
"width" : "128",
"height" : "128",
@@ -2748,7 +2881,24 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "1989.3",
+ "x" : "1950.171",
+ "y" : "1321.509",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Env4AttackLevel"
+ }
+ },
+ {
+ "name" : "knob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2106.686",
"y" : "1321.509",
"width" : "128",
"height" : "128",
@@ -2765,7 +2915,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2171.9",
+ "x" : "2263.2",
"y" : "1321.509",
"width" : "128",
"height" : "128",
@@ -2782,7 +2932,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2354.5",
+ "x" : "2419.714",
"y" : "1321.509",
"width" : "128",
"height" : "128",
@@ -2799,7 +2949,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2537.1",
+ "x" : "2576.229",
"y" : "1321.509",
"width" : "128",
"height" : "128",
@@ -2816,7 +2966,7 @@
"rotary" : {
},
"spritesheet" : {
- "x" : "2719.7",
+ "x" : "2732.743",
"y" : "1321.509",
"width" : "128",
"height" : "128",
@@ -12259,10 +12409,10 @@
{
"name" : "patchbrowser",
"component" : {
- "x" : "-0.0004882813",
+ "x" : "5",
"y" : "55",
"width" : "2890",
- "height" : "1460"
+ "height" : "1465"
},
"children" : [
{
@@ -12271,7 +12421,7 @@
"x" : "0",
"y" : "0",
"width" : "1445",
- "height" : "1460"
+ "height" : "1465"
}
},
{
@@ -12280,14 +12430,14 @@
"x" : "1445",
"y" : "0",
"width" : "1445",
- "height" : "1408"
+ "height" : "1413"
}
},
{
"name" : "ContainerPatchListSearchBox",
"component" : {
"x" : "1445",
- "y" : "1410",
+ "y" : "1415",
"width" : "1451",
"height" : "50"
}
diff --git a/source/mqJucePlugin/skins/mqDefault/mqDefaultBG.png b/source/mqJucePlugin/skins/mqDefault/mqDefaultBG.png
Binary files differ.
diff --git a/source/mqJucePlugin/skins/mqDefault/pageFilters.png b/source/mqJucePlugin/skins/mqDefault/pageFilters.png
Binary files differ.
diff --git a/source/mqJucePlugin/skins/mqDefault/pagePatchBrowser.png b/source/mqJucePlugin/skins/mqDefault/pagePatchBrowser.png
Binary files differ.
diff --git a/source/mqJucePlugin/skins/mqFrontPanel/mqFrontPanel.json b/source/mqJucePlugin/skins/mqFrontPanel/mqFrontPanel.json
@@ -59,6 +59,7 @@
"height" : "108"
},
"componentProperties" : {
+ "lcdTextColor" : "000000",
"lcdBackgroundColor" : "248727"
}
},
diff --git a/source/mqLib/CMakeLists.txt b/source/mqLib/CMakeLists.txt
@@ -19,12 +19,9 @@ else()
endif()
set(SOURCES
- am29f.cpp am29f.h
buttons.cpp buttons.h
device.cpp device.h
- dspBootCode.h
lcd.cpp lcd.h
- lcdfonts.cpp lcdfonts.h
leds.cpp leds.h
rom.cpp rom.h
microq.cpp microq.h
@@ -32,7 +29,6 @@ set(SOURCES
mqdsp.cpp mqdsp.h
mqhardware.cpp mqhardware.h
mqmc.cpp mqmc.h
- mqmidi.cpp mqmidi.h
mqstate.cpp mqstate.h
mqmiditypes.h
mqsysexremotecontrol.cpp mqsysexremotecontrol.h
@@ -46,8 +42,9 @@ endif()
target_sources(mqLib PRIVATE ${SOURCES})
source_group("source" FILES ${SOURCES})
-target_link_libraries(mqLib PUBLIC synthLib 68kEmu)
+target_link_libraries(mqLib PUBLIC synthLib wLib 68kEmu)
if(DSP56300_DEBUGGER)
target_link_libraries(mqLib PUBLIC dsp56kDebugger)
endif()
+set_property(TARGET mqLib PROPERTY FOLDER "Vavra")
diff --git a/source/mqLib/device.cpp b/source/mqLib/device.cpp
@@ -1,9 +1,5 @@
#include "device.h"
-#include "mqmiditypes.h"
-
-#include <cstring>
-
#include "../synthLib/deviceTypes.h"
#include "mqbuildconfig.h"
@@ -109,13 +105,6 @@ namespace mqLib
m_sysexRemote.handleDirtyFlags(m_customSysexOut, dirty);
}
- void Device::process(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _size, const std::vector<synthLib::SMidiEvent>& _midiIn, std::vector<synthLib::SMidiEvent>& _midiOut)
- {
- synthLib::Device::process(_inputs, _outputs, _size, _midiIn, _midiOut);
-
- m_numSamplesProcessed += static_cast<uint32_t>(_size);
- }
-
bool Device::sendMidi(const synthLib::SMidiEvent& _ev, std::vector<synthLib::SMidiEvent>& _response)
{
const auto& sysex = _ev.sysex;
@@ -151,8 +140,15 @@ namespace mqLib
m_mq.sendMidiEvent(_ev);
}
-
return true;
}
+ dsp56k::EsxiClock* Device::getDspEsxiClock() const
+ {
+ auto& mq = const_cast<MicroQ&>(m_mq);
+ auto* p = dynamic_cast<dsp56k::Peripherals56362*>(mq.getHardware()->getDSP().dsp().getPeriph(dsp56k::MemArea_X));
+ if(!p)
+ return nullptr;
+ return &p->getEsaiClock();
+ }
}
diff --git a/source/mqLib/device.h b/source/mqLib/device.h
@@ -4,12 +4,16 @@
#include "mqstate.h"
#include "mqsysexremotecontrol.h"
-#include "../synthLib/device.h"
-#include "../synthLib/midiBufferParser.h"
+#include "../wLib/wDevice.h"
+
+namespace dsp56k
+{
+ class EsaiClock;
+}
namespace mqLib
{
- class Device : public synthLib::Device
+ class Device : public wLib::Device
{
public:
Device();
@@ -26,16 +30,13 @@ namespace mqLib
protected:
void readMidiOut(std::vector<synthLib::SMidiEvent>& _midiOut) override;
void processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _samples) override;
- 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;
bool sendMidi(const synthLib::SMidiEvent& _ev, std::vector<synthLib::SMidiEvent>& _response) override;
+ dsp56k::EsxiClock* getDspEsxiClock() const override;
+
private:
MicroQ m_mq;
State m_state;
SysexRemoteControl m_sysexRemote;
- std::vector<uint8_t> m_midiOutBuffer;
- synthLib::MidiBufferParser m_midiOutParser;
- std::vector<synthLib::SMidiEvent> m_customSysexOut;
- uint32_t m_numSamplesProcessed = 0;
};
}
diff --git a/source/mqLib/lcd.cpp b/source/mqLib/lcd.cpp
@@ -2,17 +2,11 @@
#include "mqtypes.h"
-#include <cassert>
-
#include "../mc68k/port.h"
-#include "dsp56kEmu/logging.h"
-
namespace mqLib
{
- LCD::LCD() = default;
-
- bool LCD::exec(mc68k::Port& _portGp, mc68k::Port& _portF)
+ bool LCD::exec(mc68k::Port& _portGp, const mc68k::Port& _portF)
{
if(_portF.getWriteCounter() == m_lastWriteCounter)
return false;
@@ -36,202 +30,10 @@ namespace mqLib
if(!execute)
return false;
- bool changed = false;
- bool cgRamChanged = false;
-
- if(!read)
- {
- if(!registerSelect)
- {
- if(g == 0x01)
- {
- LOG("LCD Clear Display");
- m_dramData.fill(' ');
- changed = true;
- }
- else if(g == 0x02)
- {
- LOG("LCD Return Home");
- m_dramAddr = 0;
- m_cursorPos = 0;
- }
- else if((g & 0xfc) == 0x04)
- {
- const int increment = (g >> 1) & 1;
- const int shift = g & 1;
- LOG("LCD Entry Mode Set, inc=" << increment << ", shift=" << shift);
-
- m_addrIncrement = increment ? 1 : -1;
- }
- else if((g & 0xf8) == 0x08)
- {
- const int displayOnOff = (g >> 2) & 1;
- const int cursorOnOff = (g >> 1) & 1;
- const int cursorBlinking = g & 1;
-
- LOG("LCD Display ON/OFF, display=" << displayOnOff << ", cursor=" << cursorOnOff << ", blinking=" << cursorBlinking);
-
- m_displayOn = displayOnOff != 0;
- m_cursorOn = cursorOnOff != 0;
- m_cursorBlinking = cursorBlinking != 0;
- }
- else if((g & 0xf3) == 0x10)
- {
- const int scrl = (g >> 2) & 3;
-
- LOG("LCD Cursor/Display Shift, scrl=" << scrl);
- m_cursorShift = static_cast<CursorShiftMode>(scrl);
- }
- else if((g & 0xec) == 0x28)
- {
- const int dl = (g >> 4) & 1;
- const int ft = g & 3;
-
- LOG("LCD Function Set, dl=" << dl << ", ft=" << ft);
- m_dataLength = static_cast<DataLength>(dl);
- m_fontTable = static_cast<FontTable>(ft);
- }
- else if(g & (1<<7))
- {
- const int addr = g & 0x7f;
-// LOG("LCD Set DDRAM address, addr=" << addr);
- m_dramAddr = addr;
- m_addressMode = AddressMode::DDRam;
- }
- else if(g & (1<<6))
- {
- const int acg = g & 0x3f;
-
-// LOG("LCD Set CGRAM address, acg=" << acg);
- m_cgramAddr = acg;
- m_addressMode = AddressMode::CGRam;
- }
- else
- {
- LOG("LCD unknown command");
- assert(false);
- }
- }
- else
- {
- if(m_addressMode == AddressMode::CGRam)
- {
-// changed = true;
-// LOG("LCD write data to CGRAM addr " << m_cgramAddr << ", data=" << static_cast<int>(g));
-
- if (m_cgramData[m_cgramAddr] != g)
- {
- m_cgramData[m_cgramAddr] = g;
- cgRamChanged = true;
- }
-
- m_cgramAddr += m_addrIncrement;
-
- if((false && (m_cgramAddr & 0x7) == 0))
- {
- std::stringstream ss;
- ss << "CG RAM character " << (m_cgramAddr/8 - 1) << ':' << std::endl;
- ss << "##################" << std::endl;
- for(auto i = m_cgramAddr - 8; i < m_cgramAddr - 1; ++i)
- {
- ss << '#';
- for(int x=7; x >= 0; --x)
- {
- if(m_cgramData[i] & (1<<x))
- ss << "[]";
- else
- ss << " ";
- }
- ss << '#' << std::endl;
- }
- ss << "##################" << std::endl;
- const auto s(ss.str());
- LOG(s);
- }
- }
- else
- {
-// LOG("LCD write data to DDRAM addr " << m_dramAddr << ", data=" << static_cast<int>(g) << ", char=" << static_cast<char>(g));
-
- const auto old = m_dramData;
-
- if(m_dramAddr >= 20 && m_dramAddr < 0x40)
- {
- for(size_t i=1; i<=20; ++i)
- m_dramData[i-1] = m_dramData[i];
- m_dramData[19] = g;
- }
- else if(m_dramAddr > 0x53)
- {
- for(size_t i=21; i<=40; ++i)
- m_dramData[i-1] = m_dramData[i];
-
- m_dramData[39] = g;
- }
- else
- {
- if(m_dramAddr < 20)
- m_dramData[m_dramAddr] = g;
- else
- m_dramData[m_dramAddr - 0x40 + 20] = g;
- }
-
- if(m_dramAddr != 20 && m_dramAddr != 0x54)
- m_dramAddr += m_addrIncrement;
-
- if(m_dramData != old)
- changed = true;
- }
- }
- }
- else
- {
- if(registerSelect)
- {
- LOG("LCD read data from CGRAM or DDRAM");
- if(m_addressMode == AddressMode::CGRam)
- _portGp.writeRX(m_cgramData[m_cgramAddr]);
- else
- _portGp.writeRX(m_dramData[m_dramAddr]);
- }
- else
- {
- LOG("LCD read busy flag & address");
- if(m_addressMode == AddressMode::CGRam)
- {
- _portGp.writeRX(static_cast<uint8_t>(m_cgramAddr));
- }
- else
- {
- auto a = m_dramAddr;
- if(a > 0x53)
- a = 0x53;
- if(a == 20)
- a = 19;
- _portGp.writeRX(static_cast<uint8_t>(m_dramAddr));
- }
- }
- }
-
- if(changed && m_changeCallback)
- m_changeCallback();
-
- if(cgRamChanged && m_cgRamChangeCallback)
- m_cgRamChangeCallback();
-
- return true;
- }
-
- bool LCD::getCgData(std::array<uint8_t, 8>& _data, uint32_t _charIndex) const
- {
- const auto idx = _charIndex * 8;
- if(idx + 8 >= getCgRam().size())
- return false;
-
- uint32_t j = 0;
+ const auto res = wLib::LCD::exec(registerSelect, read, g);
- for(auto i = idx; i<idx+8; ++i)
- _data[j++] = getCgRam()[i];
+ if(res)
+ _portGp.writeRX(*res);
return true;
}
diff --git a/source/mqLib/lcd.h b/source/mqLib/lcd.h
@@ -1,8 +1,6 @@
#pragma once
-#include <array>
-#include <cstdint>
-#include <functional>
+#include "../wLib/lcd.h"
namespace mc68k
{
@@ -11,82 +9,13 @@ namespace mc68k
namespace mqLib
{
- class LCD
+ class LCD : public wLib::LCD
{
public:
- using ChangeCallback = std::function<void()>;
+ bool exec(mc68k::Port& _portGp, const mc68k::Port& _portF);
- LCD();
- bool exec(mc68k::Port& _portGp, mc68k::Port& _portF);
-
- const std::array<char, 40>& getDdRam() const { return m_dramData; }
- const auto& getCgRam() const { return m_cgramData; }
- bool getCgData(std::array<uint8_t, 8>& _data, uint32_t _charIndex) const;
-
- void setChangeCallback(const ChangeCallback& _callback)
- {
- m_changeCallback = _callback;
- }
-
- void setCgRamChangeCallback(const ChangeCallback& _callback)
- {
- m_cgRamChangeCallback = _callback;
- }
private:
- enum class CursorShiftMode
- {
- CursorLeft,
- CursorRight,
- DisplayLeft,
- DisplayRight
- };
- enum class DisplayShiftMode
- {
- Right,
- Left
- };
- enum class FontTable
- {
- EnglishJapanese,
- WesternEuropean1,
- EnglishRussian,
- WesternEuropean2,
- };
- enum class DataLength
- {
- Bit8,
- Bit4
- };
-
- enum class AddressMode
- {
- DDRam,
- CGRam,
- };
-
uint32_t m_lastWriteCounter = 0xffffffff;
-
- uint32_t m_cursorPos = 0;
- uint32_t m_dramAddr = 0;
- uint32_t m_cgramAddr = 0;
-
- CursorShiftMode m_cursorShift = CursorShiftMode::CursorLeft;
- DisplayShiftMode m_displayShift = DisplayShiftMode::Left;
- FontTable m_fontTable = FontTable::EnglishJapanese;
- DataLength m_dataLength = DataLength::Bit8;
- AddressMode m_addressMode = AddressMode::DDRam;
-
- bool m_displayOn = true;
- bool m_cursorOn = false;
- bool m_cursorBlinking = false;
-
- int m_addrIncrement = 1;
-
- std::array<uint8_t, 0x40> m_cgramData{};
- std::array<char, 40> m_dramData{};
uint32_t m_lastOpState = 0;
-
- ChangeCallback m_changeCallback;
- ChangeCallback m_cgRamChangeCallback;
};
}
diff --git a/source/mqLib/mqdsp.cpp b/source/mqLib/mqdsp.cpp
@@ -1,6 +1,5 @@
#include "mqdsp.h"
-#include "dspBootCode.h"
#include "mqhardware.h"
#if DSP56300_DEBUGGER
@@ -9,6 +8,8 @@
#include "../mc68k/hdi08.h"
+#include "../wLib/dspBootCode.h"
+
namespace mqLib
{
static dsp56k::DefaultMemoryValidator g_memoryValidator;
@@ -24,6 +25,7 @@ namespace mqLib
{
m_periphX.getEsaiClock().setExternalClockFrequency(44100 * 768); // measured as being roughly 33,9MHz, this should be exact
m_periphX.getEsaiClock().setSamplerate(44100); // verified
+ m_periphX.getEsaiClock().setClockSource(dsp56k::EsaiClock::ClockSource::Cycles);
auto config = m_dsp.getJit().getConfig();
@@ -36,18 +38,22 @@ namespace mqLib
// fill P memory with something that reminds us if we jump to garbage
for(dsp56k::TWord i=0; i<m_memory.sizeP(); ++i)
+ {
m_memory.set(dsp56k::MemArea_P, i, 0x000200); // debug instruction
+ m_dsp.getJit().notifyProgramMemWrite(i);
+ }
// rewrite bootloader to work at address g_bootCodeBase instead of $ff0000
- for(uint32_t i=0; i<std::size(g_dspBootCode); ++i)
+ for(uint32_t i=0; i<std::size(wLib::g_dspBootCode); ++i)
{
- uint32_t code = g_dspBootCode[i];
- if((g_dspBootCode[i] & 0xffff00) == 0xff0000)
+ uint32_t code = wLib::g_dspBootCode[i];
+ if((wLib::g_dspBootCode[i] & 0xffff00) == 0xff0000)
{
- code = g_bootCodeBase | (g_dspBootCode[i] & 0xff);
+ code = g_bootCodeBase | (wLib::g_dspBootCode[i] & 0xff);
}
m_memory.set(dsp56k::MemArea_P, i + g_bootCodeBase, code);
+ m_dsp.getJit().notifyProgramMemWrite(i + g_bootCodeBase);
}
// m_memory.saveAssembly("dspBootDisasm.asm", g_bootCodeBase, static_cast<uint32_t>(std::size(g_dspBootCode)), true, true, &m_periphX, nullptr);
@@ -175,7 +181,7 @@ namespace mqLib
// else
// LOG('[' << m_name << "] Inject interrupt" << HEXN(_irq,2));
- dsp().injectInterrupt(_irq);
+ dsp().injectExternalInterrupt(_irq);
m_hardware.ucYieldLoop([&]()
{
diff --git a/source/mqLib/mqdsp.h b/source/mqLib/mqdsp.h
@@ -4,6 +4,8 @@
#include "dsp56kEmu/dspthread.h"
#include "dsp56kEmu/peripherals.h"
+#include "../wLib/wDsp.h"
+
namespace mc68k
{
class Hdi08;
@@ -13,7 +15,7 @@ namespace mqLib
{
class Hardware;
- class MqDsp
+ class MqDsp : public wLib::Dsp
{
public:
static constexpr dsp56k::TWord g_bridgedAddr = 0x080000; // start of external SRAM, mapped to X and Y
diff --git a/source/mqLib/mqhardware.cpp b/source/mqLib/mqhardware.cpp
@@ -4,7 +4,7 @@
#include "../synthLib/midiBufferParser.h"
#include "../synthLib/deviceException.h"
-#include "dsp56kEmu/interrupts.h"
+#include <cstring> // memcpy
#if EMBED_ROM
#include "romData.h"
@@ -18,8 +18,8 @@ namespace mqLib
static_assert(ROM_DATA_SIZE == ROM::g_romSize);
#endif
- constexpr uint32_t g_syncEsaiFrameRate = 16;
- constexpr uint32_t g_syncHaltDspEsaiThreshold = 32;
+ constexpr uint32_t g_syncEsaiFrameRate = 8;
+ constexpr uint32_t g_syncHaltDspEsaiThreshold = 16;
static_assert((g_syncEsaiFrameRate & (g_syncEsaiFrameRate - 1)) == 0, "esai frame sync rate must be power of two");
static_assert(g_syncHaltDspEsaiThreshold >= g_syncEsaiFrameRate * 2, "esai DSP halt threshold must be greater than two times the sync rate");
@@ -38,9 +38,9 @@ namespace mqLib
if(!m_rom.isValid())
throw synthLib::DeviceException(synthLib::DeviceError::FirmwareMissing);
- m_uc.getPortF().setDirectionChangeCallback([&](const mc68k::Port& port)
+ m_uc.getPortF().setDirectionChangeCallback([&](const mc68k::Port& _port)
{
- if(port.getDirection() == 0xff)
+ if(_port.getDirection() == 0xff)
setGlobalDefaultParameters();
});
}
@@ -50,10 +50,9 @@ namespace mqLib
m_dsps.front().getPeriph().getEsai().setCallback({}, 0);
}
- bool Hardware::process()
+ void Hardware::process()
{
processUcCycle();
- return true;
}
void Hardware::sendMidi(const synthLib::SMidiEvent& _ev)
@@ -126,6 +125,7 @@ namespace mqLib
void Hardware::hdiProcessUCtoDSPNMIIrq()
{
// QS6 is connected to DSP NMI pin but I've never seen this being triggered
+#if SUPPORT_NMI_INTERRUPT
const uint8_t requestNMI = m_uc.requestDSPinjectNMI();
if(m_requestNMI && !requestNMI)
@@ -135,6 +135,7 @@ namespace mqLib
m_requestNMI = requestNMI;
}
+#endif
}
void Hardware::ucYieldLoop(const std::function<bool()>& _continue)
@@ -167,7 +168,7 @@ namespace mqLib
setupEsaiListener();
return;
}
-
+ /*
m_dsps[1].getPeriph().getPortC().hostWrite(0x10); // set bit 4 of GPIO Port C, vexp DSPs are waiting for this
m_dsps[2].getPeriph().getPortC().hostWrite(0x10); // set bit 4 of GPIO Port C, vexp DSPs are waiting for this
@@ -211,6 +212,7 @@ namespace mqLib
}
LOG("Voice Expansion initialization completed");
setupEsaiListener();
+ */
}
void Hardware::setupEsaiListener()
@@ -221,8 +223,7 @@ namespace mqLib
{
++m_esaiFrameIndex;
- if (m_esaiFrameIndex & 1)
- processMidiInput();
+ processMidiInput();
if((m_esaiFrameIndex & (g_syncEsaiFrameRate-1)) == 0)
m_esaiFrameAddedCv.notify_one();
@@ -322,7 +323,7 @@ namespace mqLib
const auto ucClock = m_uc.getSim().getSystemClockHz();
- constexpr double divInv = 1.0 / (44100.0 * 2.0); // stereo interleaved
+ constexpr double divInv = 1.0 / 44100.0; // stereo interleaved
const double ucCyclesPerFrame = static_cast<double>(ucClock) * divInv;
const auto esaiDelta = esaiFrameIndex - m_lastEsaiFrameIndex;
@@ -388,8 +389,17 @@ namespace mqLib
const dsp56k::TWord* inputs[16]{nullptr};
dsp56k::TWord* outputs[16]{nullptr};
+ // TODO: Right audio input channel needs to be delayed by one frame
+ ::memcpy(&m_delayedAudioIn[1], m_audioInputs[1].data(), sizeof(dsp56k::TWord) * _frames);
+
inputs[1] = &m_audioInputs[0].front();
- inputs[0] = &m_audioInputs[1].front();
+ inputs[0] = m_delayedAudioIn.data();
+ inputs[2] = m_dummyInput.data();
+ inputs[3] = m_dummyInput.data();
+ inputs[4] = m_dummyInput.data();
+ inputs[5] = m_dummyInput.data();
+ inputs[6] = m_dummyInput.data();
+ inputs[7] = m_dummyInput.data();
outputs[1] = &m_audioOutputs[0].front();
outputs[0] = &m_audioOutputs[1].front();
@@ -397,6 +407,14 @@ namespace mqLib
outputs[2] = &m_audioOutputs[3].front();
outputs[5] = &m_audioOutputs[4].front();
outputs[4] = &m_audioOutputs[5].front();
+ outputs[6] = m_dummyOutput.data();
+ outputs[7] = m_dummyOutput.data();
+ outputs[8] = m_dummyOutput.data();
+ outputs[9] = m_dummyOutput.data();
+ outputs[10] = m_dummyOutput.data();
+ outputs[11] = m_dummyOutput.data();
+
+ const auto totalFrames = _frames;
while (_frames)
{
@@ -445,7 +463,7 @@ namespace mqLib
esai.processAudioInputInterleaved(inputs, processCount, _latency);
}
- const auto requiredSize = processCount > 4 ? (processCount << 1) - 8 : 0;
+ const auto requiredSize = processCount > 8 ? processCount - 8 : 0;
if(esai.getAudioOutputs().size() < requiredSize)
{
@@ -487,6 +505,8 @@ namespace mqLib
outputs[5] += processCount;
}
+ m_delayedAudioIn[0] = m_audioInputs[1][totalFrames-1];
+
m_processAudio = false;
}
@@ -498,10 +518,18 @@ namespace mqLib
input.resize(_frames);
}
+ if(m_delayedAudioIn.size() < _frames + 1)
+ m_delayedAudioIn.resize(_frames + 1);
+
if(m_audioOutputs.front().size() < _frames)
{
for (auto& output : m_audioOutputs)
output.resize(_frames);
}
+
+ if(m_dummyInput.size() < _frames)
+ m_dummyInput.resize(_frames);
+ if(m_dummyOutput.size() < _frames)
+ m_dummyOutput.resize(_frames);
}
}
diff --git a/source/mqLib/mqhardware.h b/source/mqLib/mqhardware.h
@@ -6,16 +6,18 @@
#include "mqdsp.h"
#include "mqmc.h"
#include "mqtypes.h"
-#include "mqmidi.h"
#include "rom.h"
#include "dsp56kEmu/dspthread.h"
#include "../synthLib/midiTypes.h"
+#include "../wLib/wMidi.h"
+#include "../wLib/wHardware.h"
+
namespace mqLib
{
- class Hardware
+ class Hardware : public wLib::Hardware
{
static constexpr uint32_t g_dspCount = g_useVoiceExpansion ? 3 : 1;
@@ -23,7 +25,7 @@ namespace mqLib
explicit Hardware(std::string _romFilename);
~Hardware();
- bool process();
+ void process();
MqMc& getUC() { return m_uc; }
MqDsp& getDSP(uint32_t _index = 0) { return m_dsps[_index]; }
@@ -81,10 +83,15 @@ namespace mqLib
MqMc m_uc;
std::array<MqDsp,g_dspCount> m_dsps;
- Midi m_midi;
- dsp56k::RingBuffer<synthLib::SMidiEvent, 1024, true> m_midiIn;
+ wLib::Midi m_midi;
+ dsp56k::RingBuffer<synthLib::SMidiEvent, 16384, true> m_midiIn;
uint32_t m_midiOffsetCounter = 0;
+ std::vector<dsp56k::TWord> m_delayedAudioIn;
+
+ std::vector<dsp56k::TWord> m_dummyInput;
+ std::vector<dsp56k::TWord> m_dummyOutput;
+
TAudioInputs m_audioInputs;
TAudioOutputs m_audioOutputs;
diff --git a/source/mqLib/mqmc.cpp b/source/mqLib/mqmc.cpp
@@ -30,10 +30,10 @@ namespace mqLib
{
if(!_rom.isValid())
return;
- m_romRuntimeData.resize(ROM::getSize());
- memcpy(m_romRuntimeData.data(), m_rom.getData(), ROM::getSize());
+ m_romRuntimeData.resize(ROM::size());
+ memcpy(m_romRuntimeData.data(), m_rom.getData(), ROM::size());
- m_flash.reset(new Am29f(m_romRuntimeData.data(), m_romRuntimeData.size(), false, true));
+ m_flash.reset(new wLib::Am29f(m_romRuntimeData.data(), m_romRuntimeData.size(), false, true));
m_memory.resize(g_memorySize, 0);
@@ -41,6 +41,31 @@ namespace mqLib
setPC(g_pcInitial);
+ getPortGP().setWriteTXCallback([this](const mc68k::Port&)
+ {
+ onPortGPWritten();
+ });
+
+ getPortE().setWriteTXCallback([this](const mc68k::Port&)
+ {
+ onPortEWritten();
+ });
+
+ getPortF().setWriteTXCallback([this](const mc68k::Port&)
+ {
+ onPortFWritten();
+ });
+
+ getPortQS().setDirectionChangeCallback([this](const mc68k::Port&)
+ {
+ onPortQSWritten();
+ });
+
+ getPortQS().setWriteTXCallback([this](const mc68k::Port&)
+ {
+ onPortQSWritten();
+ });
+
#if 0
dumpAssembly(g_romAddress, g_romSize);
#endif
@@ -72,50 +97,8 @@ namespace mqLib
m_hdi08c.exec(deltaCycles);
}
- const bool resetIsOutput = getPortQS().getDirection() & (1<<3);
- if(resetIsOutput)
- {
- if(!(getPortQS().read() & (1<<3)))
- {
- if(!m_dspResetRequest)
- {
-#ifdef _DEBUG
- MCLOG("Request DSP RESET");
-#endif
- m_dspResetRequest = true;
- m_dspResetCompleted = false;
- }
- }
- }
- else
- {
- if(m_dspResetCompleted)
- {
- m_dspResetRequest = false;
- getPortQS().writeRX(1<<3);
- }
- }
-
- if(getPortQS().getDirection() & (1<<6))
- {
- m_dspInjectNmiRequest = (getPortQS().read() >> 6) & 1;
- if(m_dspInjectNmiRequest)
- int debug=0;
- }
-
m_buttons.processButtons(getPortGP(), getPortE());
- if(m_lcd.exec(getPortGP(), getPortF()))
- {
-// const std::string s(&m_lcd.getDdRam().front());
-// if(s.find("SIG") != std::string::npos)
-// dumpMemory("SIG");
- }
- else
- {
- m_leds.exec(getPortF(), getPortGP(), getPortE());
- }
-
return deltaCycles;
}
@@ -133,7 +116,7 @@ namespace mqLib
return readW(m_memory, addr);
}
- if(addr >= g_romAddress && addr < g_romAddress + ROM::getSize())
+ if(addr >= g_romAddress && addr < g_romAddress + ROM::size())
{
const auto r = readW(m_romRuntimeData, addr - g_romAddress);
// LOG("read16 from ROM addr=" << HEXN(addr, 8) << " val=" << HEXN(r, 4));
@@ -150,7 +133,7 @@ namespace mqLib
return readW(m_memory, addr);
}
- if(addr >= g_romAddress && addr < g_romAddress + ROM::getSize())
+ if(addr >= g_romAddress && addr < g_romAddress + ROM::size())
{
const auto r = readW(m_romRuntimeData, addr - g_romAddress);
// LOG("read16 from ROM addr=" << HEXN(addr, 8) << " val=" << HEXN(r, 4));
@@ -180,7 +163,7 @@ namespace mqLib
if(addr < g_memorySize)
return m_memory[addr];
- if(addr >= g_romAddress && addr < g_romAddress + ROM::getSize())
+ if(addr >= g_romAddress && addr < g_romAddress + ROM::size())
return m_romRuntimeData[addr - g_romAddress];
const auto pa = static_cast<mc68k::PeriphAddress>(addr & mc68k::g_peripheralMask);
@@ -215,7 +198,7 @@ namespace mqLib
return;
}
- if(addr >= g_romAddress && addr < g_romAddress + ROM::getSize())
+ if(addr >= g_romAddress && addr < g_romAddress + ROM::size())
{
MCLOG("write16 TO ROM addr=" << MCHEXN(addr, 8) << ", value=" << MCHEXN(val,4) << ", pc=" << MCHEXN(getPC(), 8));
m_flash->write(addr - g_romAddress, val);
@@ -262,7 +245,7 @@ namespace mqLib
return;
}
- if(addr >= g_romAddress && addr < g_romAddress + ROM::getSize())
+ if(addr >= g_romAddress && addr < g_romAddress + ROM::size())
{
MCLOG("write8 TO ROM addr=" << MCHEXN(addr, 8) << ", value=" << MCHEXN(val,2) << " char=" << logChar(val) << ", pc=" << MCHEXN(getPC(), 8));
m_flash->write(addr - g_romAddress, val);
@@ -305,7 +288,7 @@ namespace mqLib
void MqMc::dumpROM(const char* _filename) const
{
FILE* hFile = fopen((std::string(_filename) + ".bin").c_str(), "wb");
- fwrite(m_romRuntimeData.data(), 1, ROM::getSize(), hFile);
+ fwrite(m_romRuntimeData.data(), 1, ROM::size(), hFile);
fclose(hFile);
}
@@ -342,4 +325,68 @@ namespace mqLib
return Mc68k::onIllegalInstruction(opcode);
}
+
+ void MqMc::onPortEWritten()
+ {
+ processLCDandLEDs();
+ }
+
+ void MqMc::onPortFWritten()
+ {
+ processLCDandLEDs();
+ }
+
+ void MqMc::onPortGPWritten()
+ {
+ processLCDandLEDs();
+ }
+
+ void MqMc::onPortQSWritten()
+ {
+ const bool resetIsOutput = getPortQS().getDirection() & (1<<3);
+ if(resetIsOutput)
+ {
+ if(!(getPortQS().read() & (1<<3)))
+ {
+ if(!m_dspResetRequest)
+ {
+#ifdef _DEBUG
+ MCLOG("Request DSP RESET");
+#endif
+ m_dspResetRequest = true;
+ m_dspResetCompleted = false;
+ }
+ }
+ }
+ else
+ {
+ if(m_dspResetCompleted)
+ {
+ m_dspResetRequest = false;
+ getPortQS().writeRX(1<<3);
+ }
+ }
+#if SUPPORT_NMI_INTERRUPT
+ if(getPortQS().getDirection() & (1<<6))
+ {
+ m_dspInjectNmiRequest = (getPortQS().read() >> 6) & 1;
+ if(m_dspInjectNmiRequest)
+ int debug=0;
+ }
+#endif
+ }
+
+ void MqMc::processLCDandLEDs()
+ {
+ if(m_lcd.exec(getPortGP(), getPortF()))
+ {
+// const std::string s(&m_lcd.getDdRam().front());
+// if(s.find("SIG") != std::string::npos)
+// dumpMemory("SIG");
+ }
+ else
+ {
+ m_leds.exec(getPortF(), getPortGP(), getPortE());
+ }
+ }
}
diff --git a/source/mqLib/mqmc.h b/source/mqLib/mqmc.h
@@ -6,11 +6,13 @@
#include "buttons.h"
#include "lcd.h"
#include "leds.h"
-#include "am29f.h"
+#include "../wLib/am29f.h"
#include "../mc68k/mc68k.h"
#include "../mc68k/hdi08periph.h"
+#define SUPPORT_NMI_INTERRUPT 0
+
namespace mqLib
{
class ROM;
@@ -37,7 +39,9 @@ namespace mqLib
bool requestDSPReset() const { return m_dspResetRequest; }
void notifyDSPBooted();
+#if SUPPORT_NMI_INTERRUPT
uint8_t requestDSPinjectNMI() const { return m_dspInjectNmiRequest; }
+#endif
void dumpMemory(const char* _filename) const;
void dumpROM(const char* _filename) const;
@@ -53,9 +57,16 @@ namespace mqLib
void onReset() override;
uint32_t onIllegalInstruction(uint32_t opcode) override;
+ void onPortEWritten();
+ void onPortFWritten();
+ void onPortGPWritten();
+ void onPortQSWritten();
+
+ void processLCDandLEDs();
+
const ROM& m_rom;
std::vector<uint8_t> m_romRuntimeData;
- std::unique_ptr<Am29f> m_flash;
+ std::unique_ptr<wLib::Am29f> m_flash;
LCD m_lcd;
Buttons m_buttons;
Leds m_leds;
@@ -68,6 +79,9 @@ namespace mqLib
std::list<uint32_t> m_lastPCs;
bool m_dspResetRequest = false;
bool m_dspResetCompleted = false;
+
+#if SUPPORT_NMI_INTERRUPT
uint8_t m_dspInjectNmiRequest = 0;
+#endif
};
}
diff --git a/source/mqLib/mqmiditypes.h b/source/mqLib/mqmiditypes.h
@@ -2,14 +2,14 @@
#include <cstdint>
+#include "../wLib/wMidiTypes.h"
+
namespace mqLib
{
enum MidiHeaderByte : uint8_t
{
- IdWaldorf = 0x3e,
IdQ = 0x0f,
IdMicroQ = 0x10,
- IdDeviceOmni = 0x7f
};
enum class SysexCommand : uint8_t
@@ -174,40 +174,30 @@ namespace mqLib
enum SysexIndex
{
- IdxSysexBegin = 0,
- IdxIdWaldorf = 1,
- IdxIdMicroQ = 2,
- IdxDeviceId = 3,
- IdxCommand = 4,
-
- // dumps / dump requests
- IdxBuffer = 5,
- IdxLocation = 6,
-
// first parameter of a dump
IdxSingleParamFirst = 7,
IdxMultiParamFirst = IdxSingleParamFirst,
IdxDrumParamFirst = IdxSingleParamFirst,
- IdxGlobalParamFirst = IdxBuffer,
- IdxModeParamFirst = IdxBuffer,
+ IdxGlobalParamFirst = wLib::IdxBuffer,
+ IdxModeParamFirst = wLib::IdxBuffer,
- IdxSingleParamIndexH = IdxBuffer + 1,
+ IdxSingleParamIndexH = wLib::IdxBuffer + 1,
IdxSingleParamIndexL = IdxSingleParamIndexH + 1,
IdxSingleParamValue = IdxSingleParamIndexL + 1,
- IdxMultiParamIndexH = IdxBuffer,
+ IdxMultiParamIndexH = wLib::IdxBuffer,
IdxMultiParamIndexL = IdxMultiParamIndexH + 1,
IdxMultiParamValue = IdxMultiParamIndexL + 1,
- IdxDrumParamIndexH = IdxBuffer,
+ IdxDrumParamIndexH = wLib::IdxBuffer,
IdxDrumParamIndexL = IdxMultiParamIndexH + 1,
IdxDrumParamValue = IdxMultiParamIndexL + 1,
- IdxGlobalParamIndexH = IdxBuffer,
+ IdxGlobalParamIndexH = wLib::IdxBuffer,
IdxGlobalParamIndexL = IdxGlobalParamIndexH + 1,
IdxGlobalParamValue = IdxGlobalParamIndexL + 1,
- IdxModeParamIndexH = IdxBuffer,
+ IdxModeParamIndexH = wLib::IdxBuffer,
IdxModeParamIndexL = IdxModeParamIndexH + 1,
IdxModeParamValue = IdxModeParamIndexL + 1
};
diff --git a/source/mqLib/mqstate.cpp b/source/mqLib/mqstate.cpp
@@ -13,7 +13,7 @@
namespace mqLib
{
- static_assert(std::size(State::g_dumps) == static_cast<uint32_t>(State::DumpType::Count), "data definition missing");
+ static_assert(std::size(State::Dumps) == static_cast<uint32_t>(State::DumpType::Count), "data definition missing");
State::State(MicroQ& _mq) : m_mq(_mq)
{
@@ -47,7 +47,7 @@ namespace mqLib
}
if (_sender == Origin::Device)
- LOG("Recv: " << HEXN(_data.a, 2) << ' ' << HEXN(_data.b, 2) << ' ' << HEXN(_data.c, 2))
+ LOG("Recv: " << HEXN(_data.a, 2) << ' ' << HEXN(_data.b, 2) << ' ' << HEXN(_data.c, 2));
switch(_data.a & 0xf0)
{
@@ -234,7 +234,7 @@ namespace mqLib
case SysexCommand::DrumDump:
{
auto m = message;
- m[IdxDeviceId] = IdDeviceOmni;
+ m[wLib::IdxDeviceId] = wLib::IdDeviceOmni;
loadState(m);
}
break;
@@ -250,8 +250,8 @@ namespace mqLib
{
auto dump = convertTo(m_romSingles[0]);
- dump[IdxBuffer] = static_cast<uint8_t>(MidiBufferNum::SingleEditBufferSingleMode);
- dump[IdxLocation] = 0;
+ dump[wLib::IdxBuffer] = static_cast<uint8_t>(MidiBufferNum::SingleEditBufferSingleMode);
+ dump[wLib::IdxLocation] = 0;
forwardToDevice(dump);
}
@@ -291,8 +291,8 @@ namespace mqLib
if(!convertTo(single, _data))
return false;
- const auto buf = static_cast<MidiBufferNum>(_data[IdxBuffer]);
- const auto loc = _data[IdxLocation];
+ const auto buf = static_cast<MidiBufferNum>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
Single* dst = getSingle(buf, loc);
@@ -309,8 +309,8 @@ namespace mqLib
if(!convertTo(multi, _data))
return false;
- const auto buf = static_cast<MidiBufferNum>(_data[IdxBuffer]);
- const auto loc = _data[IdxLocation];
+ const auto buf = static_cast<MidiBufferNum>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
auto* m = getMulti(buf, loc);
if(!m)
@@ -326,8 +326,8 @@ namespace mqLib
if(!convertTo(drum, _data))
return false;
- const auto buf = static_cast<MidiBufferNum>(_data[IdxBuffer]);
- const auto loc = _data[IdxLocation];
+ const auto buf = static_cast<MidiBufferNum>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
auto* d = getDrumMap(buf, loc);
if(!d)
@@ -408,25 +408,28 @@ namespace mqLib
*p = _data[IdxModeParamValue];
return true;
}
-
- template<size_t Size>
- static uint8_t* getParameter(std::array<uint8_t, Size>& _dump, const SysEx& _data, State::DumpType _type)
+
+ namespace
{
- const auto& dump = State::g_dumps[static_cast<uint32_t>(_type)];
+ template<size_t Size>
+ uint8_t* getParameter(std::array<uint8_t, Size>& _dump, const SysEx& _data, State::DumpType _type)
+ {
+ const auto& dump = State::Dumps[static_cast<uint32_t>(_type)];
- if(dump.idxParamIndexH >= _data.size() || dump.idxParamIndexL >= _data.size())
- return nullptr;
+ if(dump.idxParamIndexH >= _data.size() || dump.idxParamIndexL >= _data.size())
+ return nullptr;
- const auto i = dump.firstParamIndex + ((static_cast<uint32_t>(_data[dump.idxParamIndexH]) << 7) | static_cast<uint32_t>(_data[dump.idxParamIndexL]));
+ const auto i = dump.firstParamIndex + ((static_cast<uint32_t>(_data[dump.idxParamIndexH]) << 7) | static_cast<uint32_t>(_data[dump.idxParamIndexL]));
- if(i > _dump.size())
- return nullptr;
- return &_dump[i];
+ if(i > _dump.size())
+ return nullptr;
+ return &_dump[i];
+ }
}
uint8_t* State::getSingleParameter(const SysEx& _data)
{
- const auto loc = _data[IdxBuffer];
+ const auto loc = _data[wLib::IdxBuffer];
Single* s = getSingle(getGlobalParameter(GlobalParameter::SingleMultiMode) ? MidiBufferNum::SingleEditBufferMultiMode : MidiBufferNum::SingleEditBufferSingleMode, loc);
if(!s)
@@ -456,8 +459,8 @@ namespace mqLib
bool State::getSingle(Responses& _responses, const SysEx& _data)
{
- const auto buf = static_cast<MidiBufferNum>(_data[IdxBuffer]);
- const auto loc = _data[IdxLocation];
+ const auto buf = static_cast<MidiBufferNum>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
const auto* s = getSingle(buf, loc);
if(!s || !isValid(*s))
@@ -487,7 +490,7 @@ namespace mqLib
return &m_romSingles[_loc + 200];
case MidiBufferNum::SingleBankX:
case MidiBufferNum::DeprecatedSingleBankX:
- // mQ doesn't have a card, no idea why its mentioned in the MIDI implementaiton
+ // mQ doesn't have a card, no idea why it's mentioned in the MIDI implementaiton
return nullptr;
case MidiBufferNum::SingleEditBufferSingleMode:
m_isEditBuffer = true;
@@ -517,10 +520,10 @@ namespace mqLib
bool State::getMulti(Responses& _responses, const SysEx& _data)
{
- const auto buf = static_cast<MidiBufferNum>(_data[IdxBuffer]);
- const auto loc = _data[IdxLocation];
+ const auto buf = static_cast<MidiBufferNum>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
- auto* m = getMulti(buf, loc);
+ const auto* m = getMulti(buf, loc);
if(!m || !isValid(*m))
return false;
_responses.push_back(convertTo(*m));
@@ -550,10 +553,10 @@ namespace mqLib
bool State::getDrumMap(Responses& _responses, const SysEx& _data)
{
- const auto buf = static_cast<MidiBufferNum>(_data[IdxBuffer]);
- const auto loc = _data[IdxLocation];
+ const auto buf = static_cast<MidiBufferNum>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
- auto* d = getDrumMap(buf, loc);
+ const auto* d = getDrumMap(buf, loc);
if(!d || !isValid(*d))
return false;
_responses.push_back(convertTo(*d));
@@ -677,7 +680,7 @@ namespace mqLib
bool State::requestDumpParameter(DumpType _type, Responses& _responses, const SysEx& _data)
{
- auto parameterRequestResponse = [&](uint8_t* p)
+ auto parameterRequestResponse = [&](const uint8_t* p)
{
if(!p)
return false;
@@ -720,10 +723,10 @@ namespace mqLib
if (_data.front() != 0xf0 || _data.back() != 0xf7)
return SysexCommand::Invalid;
- if (_data[IdxIdWaldorf] != IdWaldorf || _data[IdxIdMicroQ] != IdMicroQ)
+ if (_data[wLib::IdxIdWaldorf] != wLib::IdWaldorf || _data[wLib::IdxIdMachine] != IdMicroQ)
return SysexCommand::Invalid;
- return static_cast<SysexCommand>(_data[IdxCommand]);
+ return static_cast<SysexCommand>(_data[wLib::IdxCommand]);
}
void State::forwardToDevice(const SysEx& _data) const
@@ -736,22 +739,22 @@ namespace mqLib
void State::requestGlobal() const
{
- sendSysex({0xf0, IdWaldorf, IdMicroQ, IdDeviceOmni, static_cast<uint8_t>(SysexCommand::GlobalRequest), 0xf7});
+ sendSysex({0xf0, wLib::IdWaldorf, IdMicroQ, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::GlobalRequest), 0xf7});
}
void State::requestSingle(MidiBufferNum _buf, MidiSoundLocation _location) const
{
- sendSysex({0xf0, IdWaldorf, IdMicroQ, IdDeviceOmni, static_cast<uint8_t>(SysexCommand::SingleRequest), static_cast<uint8_t>(_buf), static_cast<uint8_t>(_location), 0xf7});
+ sendSysex({0xf0, wLib::IdWaldorf, IdMicroQ, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::SingleRequest), static_cast<uint8_t>(_buf), static_cast<uint8_t>(_location), 0xf7});
}
void State::requestMulti(MidiBufferNum _buf, uint8_t _location) const
{
- sendSysex({0xf0, IdWaldorf, IdMicroQ, IdDeviceOmni, static_cast<uint8_t>(SysexCommand::MultiRequest), static_cast<uint8_t>(_buf), _location, 0xf7});
+ sendSysex({0xf0, wLib::IdWaldorf, IdMicroQ, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::MultiRequest), static_cast<uint8_t>(_buf), _location, 0xf7});
}
inline void State::sendMulti(const std::vector<uint8_t>& _multiData) const
{
- std::vector<uint8_t> data = { 0xf0, IdWaldorf, IdMicroQ, IdDeviceOmni, static_cast<uint8_t>(SysexCommand::MultiDump), static_cast<uint8_t>(MidiBufferNum::DeprecatedMultiBankInternal), 0};
+ std::vector<uint8_t> data = { 0xf0, wLib::IdWaldorf, IdMicroQ, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::MultiDump), static_cast<uint8_t>(MidiBufferNum::DeprecatedMultiBankInternal), 0};
data.insert(data.end(), _multiData.begin(), _multiData.end());
uint8_t checksum = 0;
@@ -768,7 +771,7 @@ namespace mqLib
const auto p = static_cast<uint8_t>(_param);
- sendSysex({0xf0, IdWaldorf, IdMicroQ, IdDeviceOmni, static_cast<uint8_t>(SysexCommand::GlobalParameterChange),
+ sendSysex({0xf0, wLib::IdWaldorf, IdMicroQ, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::GlobalParameterChange),
static_cast<uint8_t>(p >> 7), static_cast<uint8_t>(p & 0x7f), _value, 0xf7});
}
diff --git a/source/mqLib/mqstate.h b/source/mqLib/mqstate.h
@@ -10,6 +10,8 @@
#include "../synthLib/deviceTypes.h"
#include "../synthLib/midiTypes.h"
+#include "../wLib/wState.h"
+
namespace synthLib
{
struct SMidiEvent;
@@ -19,10 +21,10 @@ namespace mqLib
{
class MicroQ;
- using SysEx = std::vector<uint8_t>;
- using Responses = std::vector<SysEx>;
+ using SysEx = wLib::SysEx;
+ using Responses = wLib::Responses;
- class State
+ class State : public wLib::State
{
public:
enum class Origin
@@ -57,7 +59,7 @@ namespace mqLib
uint32_t dumpSize;
};
- static constexpr Dump g_dumps[] =
+ static constexpr Dump Dumps[] =
{
{DumpType::Single, SysexCommand::SingleRequest, SysexCommand::SingleDump, SysexCommand::SingleParameterChange, SysexCommand::SingleParameterRequest, IdxSingleParamFirst, IdxSingleParamIndexH, IdxSingleParamIndexL, IdxSingleParamValue, 392},
{DumpType::Multi, SysexCommand::MultiRequest, SysexCommand::MultiDump, SysexCommand::MultiParameterChange, SysexCommand::MultiParameterRequest, IdxMultiParamFirst, IdxMultiParamIndexH, IdxMultiParamIndexL, IdxMultiParamValue, 393},
@@ -68,13 +70,13 @@ namespace mqLib
{DumpType::SingleQ, SysexCommand::SingleRequest, SysexCommand::SingleDump, SysexCommand::SingleParameterChange, SysexCommand::SingleParameterRequest, IdxSingleParamFirst, IdxSingleParamIndexH, IdxSingleParamIndexL, IdxSingleParamValue, 393},
};
- using Single = std::array<uint8_t, g_dumps[static_cast<uint32_t>(DumpType::Single)].dumpSize>;
- using Multi = std::array<uint8_t, g_dumps[static_cast<uint32_t>(DumpType::Multi)].dumpSize>;
- using DrumMap = std::array<uint8_t, g_dumps[static_cast<uint32_t>(DumpType::Drum)].dumpSize>;
- using Global = std::array<uint8_t, g_dumps[static_cast<uint32_t>(DumpType::Global)].dumpSize>;
- using Mode = std::array<uint8_t, g_dumps[static_cast<uint32_t>(DumpType::Mode)].dumpSize>;
+ using Single = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Single)].dumpSize>;
+ using Multi = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Multi)].dumpSize>;
+ using DrumMap = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Drum)].dumpSize>;
+ using Global = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Global)].dumpSize>;
+ using Mode = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Mode)].dumpSize>;
- using SingleQ = std::array<uint8_t, g_dumps[static_cast<uint32_t>(DumpType::SingleQ)].dumpSize>;
+ using SingleQ = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::SingleQ)].dumpSize>;
State(MicroQ& _mq);
@@ -90,20 +92,6 @@ namespace mqLib
static void createSequencerMultiData(std::vector<uint8_t>& _data);
private:
- template<size_t Size> static bool convertTo(std::array<uint8_t, Size>& _dst, const SysEx& _data)
- {
- if(_data.size() != Size)
- return false;
- std::copy(_data.begin(), _data.end(), _dst.begin());
- return true;
- }
-
- template<size_t Size> static SysEx convertTo(const std::array<uint8_t, Size>& _src)
- {
- SysEx dst;
- dst.insert(dst.begin(), _src.begin(), _src.end());
- return dst;
- }
template<size_t Size> static bool append(SysEx& _dst, const std::array<uint8_t, Size>& _src)
{
@@ -119,7 +107,7 @@ namespace mqLib
{
uint8_t& c = _src[_src.size() - 2];
c = 0;
- for(size_t i=IdxCommand; i<_src.size()-2; ++i)
+ for(size_t i = wLib::IdxCommand; i<_src.size()-2; ++i)
c += _src[i];
c &= 0x7f;
}
@@ -130,7 +118,7 @@ namespace mqLib
return false;
uint8_t& c = _src[_src.size() - 2];
c = 0;
- for(size_t i=IdxCommand; i<_src.size()-2; ++i)
+ for(size_t i= wLib::IdxCommand; i<_src.size()-2; ++i)
c += _src[i];
c &= 0x7f;
return true;
diff --git a/source/mqLib/mqsysexremotecontrol.cpp b/source/mqLib/mqsysexremotecontrol.cpp
@@ -11,7 +11,7 @@ namespace mqLib
void SysexRemoteControl::createSysexHeader(std::vector<uint8_t>& _dst, SysexCommand _cmd)
{
constexpr uint8_t devId = 0;
- _dst.assign({0xf0, IdWaldorf, IdMicroQ, devId, static_cast<uint8_t>(_cmd)});
+ _dst.assign({0xf0, wLib::IdWaldorf, IdMicroQ, devId, static_cast<uint8_t>(_cmd)});
}
void SysexRemoteControl::sendSysexLCD(std::vector<synthLib::SMidiEvent>& _dst) const
@@ -102,7 +102,7 @@ namespace mqLib
if(_input.size() < 5)
return false;
- if(_input[1] != IdWaldorf || _input[2] != IdMicroQ)
+ if(_input[1] != wLib::IdWaldorf || _input[2] != IdMicroQ)
return false;
const auto cmd = _input[4];
diff --git a/source/mqLib/rom.cpp b/source/mqLib/rom.cpp
@@ -1,109 +1,5 @@
#include "rom.h"
-#include "mqmiditypes.h"
-#include "../synthLib/os.h"
-#include "../synthLib/midiToSysex.h"
-
namespace mqLib
{
- bool ROM::loadFromFile(const std::string& _filename)
- {
- FILE* hFile = fopen(_filename.c_str(), "rb");
- if(!hFile)
- return false;
-
- fseek(hFile, 0, SEEK_END);
- const auto size = ftell(hFile);
- fseek(hFile, 0, SEEK_SET);
-
- m_buffer.resize(size);
- const auto numRead = fread(m_buffer.data(), 1, size, hFile);
- fclose(hFile);
-
- if(numRead != static_cast<size_t>(size))
- {
- m_buffer.clear();
- return false;
- }
-
- if(numRead != getSize())
- {
- loadFromMidi(m_buffer, _filename);
-
- if (!m_buffer.empty() && m_buffer.size() < getSize())
- m_buffer.resize(getSize(), 0xff);
- }
-
- if(!m_buffer.empty())
- {
- m_data = m_buffer.data();
- return true;
- }
- return false;
- }
-
- bool ROM::loadFromMidi(std::vector<unsigned char>& _buffer, const std::string& _filename)
- {
- _buffer.clear();
-
- std::vector<uint8_t> data;
- if(!synthLib::MidiToSysex::readFile(data, _filename.c_str()) || data.empty())
- return false;
-
- return loadFromSysExBuffer(_buffer, data);
- }
-
- bool ROM::loadFromSysExFile(std::vector<uint8_t>& _buffer, const std::string& _filename)
- {
- _buffer.clear();
-
- std::vector<uint8_t> buf;
- if (!synthLib::readFile(buf, _filename))
- return false;
- return loadFromSysExBuffer(_buffer, buf);
- }
-
- bool ROM::loadFromSysExBuffer(std::vector<unsigned char>& _buffer, const std::vector<uint8_t>& _sysex)
- {
- _buffer.reserve(getSize());
-
- std::vector<std::vector<uint8_t>> messages;
- synthLib::MidiToSysex::splitMultipleSysex(messages, _sysex);
-
- uint16_t expectedCounter = 1;
-
- for (const auto& message : messages)
- {
- if(message.size() < 0xfc)
- continue;
-
- if(message[1] != IdWaldorf)
- continue;
-
- if(message[3] != 0x7f)
- continue;
-
- if(message[4] != 0x71 && message[4] != 0x72 && message[4] != 0x73) // MW2, Q, mQ
- continue;
-
- const auto counter = (message[6] << 7) | message[7];
- if(expectedCounter != counter && counter != 1)
- return false;
- expectedCounter = static_cast<uint16_t>(counter);
- ++expectedCounter;
-
- size_t i = 10;
- while(i + 5 < message.size())
- {
- const auto lsbs = message[i];
- _buffer.push_back((message[i+1] << 1) | ((lsbs >> 0) & 1));
- _buffer.push_back((message[i+2] << 1) | ((lsbs >> 1) & 1));
- _buffer.push_back((message[i+3] << 1) | ((lsbs >> 2) & 1));
- _buffer.push_back((message[i+4] << 1) | ((lsbs >> 3) & 1));
- i += 5;
- }
- }
-
- return true;
- }
}
diff --git a/source/mqLib/rom.h b/source/mqLib/rom.h
@@ -1,32 +1,20 @@
#pragma once
-#include <string>
-#include <vector>
+#include "../wLib/wRom.h"
namespace mqLib
{
- class ROM
+ class ROM final : public wLib::ROM
{
public:
static constexpr uint32_t g_romSize = 524288;
- explicit ROM(const std::string& _filename, const uint8_t* _data = nullptr) : m_data(_data)
+ explicit ROM(const std::string& _filename, const uint8_t* _data = nullptr) : wLib::ROM(_filename, g_romSize, _data)
{
- if (!_filename.empty())
- loadFromFile(_filename);
}
- const uint8_t* getData() const { return m_data; }
- static constexpr uint32_t getSize() { return g_romSize; }
- bool isValid() const { return !m_buffer.empty(); }
+ static constexpr uint32_t size() { return g_romSize; }
- static bool loadFromMidi(std::vector<uint8_t>& _buffer, const std::string& _filename);
- static bool loadFromSysExFile(std::vector<uint8_t>& _buffer, const std::string& _filename);
- static bool loadFromSysExBuffer(std::vector<uint8_t> &_buffer, const std::vector<uint8_t> &_sysex);
- private:
- bool loadFromFile(const std::string& _filename);
-
- const uint8_t* m_data;
- std::vector<uint8_t> m_buffer;
+ uint32_t getSize() const override { return g_romSize; }
};
}
diff --git a/source/mqPerformanceTest/CMakeLists.txt b/source/mqPerformanceTest/CMakeLists.txt
@@ -18,3 +18,4 @@ if(UNIX AND NOT APPLE)
endif()
install(TARGETS mqPerformanceTest DESTINATION . COMPONENT VavraPerformanceTest)
+set_property(TARGET mqPerformanceTest PROPERTY FOLDER "Vavra")
diff --git a/source/mqTestConsole/CMakeLists.txt b/source/mqTestConsole/CMakeLists.txt
@@ -27,3 +27,4 @@ if(UNIX AND NOT APPLE)
endif()
#install(TARGETS mqTestConsole DESTINATION . COMPONENT STANDALONE)
+set_property(TARGET mqTestConsole PROPERTY FOLDER "Vavra")
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,17 +1,24 @@
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
+ binarystream.cpp binarystream.h
+ buildconfig.h buildconfig.h.in
configFile.cpp configFile.h
device.cpp device.h
deviceException.cpp deviceException.h
deviceTypes.h
+ dspMemoryPatch.cpp dspMemoryPatch.h
hybridcontainer.h
+ md5.cpp md5.h
midiBufferParser.cpp midiBufferParser.h
midiToSysex.cpp midiToSysex.h
midiTypes.h
@@ -33,3 +40,4 @@ target_link_libraries(synthLib PUBLIC resample dsp56kEmu)
if(NOT MSVC)
target_link_libraries(synthLib PUBLIC dl)
endif()
+set_property(TARGET synthLib PROPERTY FOLDER "Gearmulator")
diff --git a/source/synthLib/binarystream.cpp b/source/synthLib/binarystream.cpp
@@ -0,0 +1,24 @@
+#include "binarystream.h"
+
+namespace synthLib
+{
+ BinaryStream BinaryStream::readChunk()
+ {
+ Chunk chunk;
+ chunk.read(*this);
+ return std::move(chunk.data);
+ }
+
+ BinaryStream BinaryStream::tryReadChunkInternal(const char* _4Cc, const uint32_t _versionMax)
+ {
+ Chunk chunk;
+ chunk.read(*this);
+ if(chunk.version > _versionMax)
+ return {};
+ if(0 != strcmp(chunk.fourCC, _4Cc))
+ return {};
+ return std::move(chunk.data);
+ }
+
+ template BinaryStream BinaryStream::tryReadChunk(char const(& _4Cc)[5], uint32_t _versionMax);
+}
diff --git a/source/synthLib/binarystream.h b/source/synthLib/binarystream.h
@@ -1,21 +1,156 @@
#pragma once
+#include <cassert>
#include <cstdint>
-#include <iosfwd>
+#include <functional>
#include <sstream>
#include <vector>
+#include <cstring>
namespace synthLib
{
- template<typename SizeType>
- class BinaryStream final : std::stringstream
+ class StreamBuffer
{
public:
+ StreamBuffer() = default;
+ explicit StreamBuffer(std::vector<uint8_t>&& _buffer) : m_vector(std::move(_buffer))
+ {
+ }
+ explicit StreamBuffer(const size_t _capacity)
+ {
+ m_vector.reserve(_capacity);
+ }
+ StreamBuffer(uint8_t* _buffer, const size_t _size) : m_buffer(_buffer), m_size(_size), m_fixedSize(true)
+ {
+ }
+ StreamBuffer(StreamBuffer& _parent, const size_t _childSize) : m_buffer(&_parent.buffer()[_parent.tellg()]), m_size(_childSize), m_fixedSize(true)
+ {
+ // force eof if range is not valid
+ if(_parent.tellg() + _childSize > _parent.size())
+ {
+ assert(false && "invalid range");
+ m_readPos = _childSize;
+ }
+
+ // seek parent forward
+ _parent.seekg(_parent.tellg() + _childSize);
+ }
+ StreamBuffer(StreamBuffer&& _source) noexcept
+ : m_buffer(_source.m_buffer)
+ , m_size(_source.m_size)
+ , m_fixedSize(_source.m_fixedSize)
+ , m_readPos(_source.m_readPos)
+ , m_writePos(_source.m_writePos)
+ , m_vector(std::move(_source.m_vector))
+ , m_fail(_source.m_fail)
+ {
+ _source.destroy();
+ }
+
+ StreamBuffer& operator = (StreamBuffer&& _source) noexcept
+ {
+ m_buffer = _source.m_buffer;
+ m_size = _source.m_size;
+ m_fixedSize = _source.m_fixedSize;
+ m_readPos = _source.m_readPos;
+ m_writePos = _source.m_writePos;
+ m_vector = std::move(_source.m_vector);
+
+ _source.destroy();
+
+ return *this;
+ }
+
+ void seekg(const size_t _pos) { m_readPos = _pos; }
+ size_t tellg() const { return m_readPos; }
+ void seekp(const size_t _pos) { m_writePos = _pos; }
+ size_t tellp() const { return m_writePos; }
+ bool eof() const { return tellg() >= size(); }
+ bool fail() const { return m_fail; }
+
+ bool read(uint8_t* _dst, size_t _size)
+ {
+ const auto remaining = size() - tellg();
+ if(remaining < _size)
+ {
+ m_fail = true;
+ return false;
+ }
+ ::memcpy(_dst, &buffer()[m_readPos], _size);
+ m_readPos += _size;
+ return true;
+ }
+ bool write(const uint8_t* _src, size_t _size)
+ {
+ const auto remaining = size() - tellp();
+ if(remaining < _size)
+ {
+ if(m_fixedSize)
+ {
+ m_fail = true;
+ return false;
+ }
+ m_vector.resize(tellp() + _size);
+ }
+ ::memcpy(&buffer()[m_writePos], _src, _size);
+ m_writePos += _size;
+ return true;
+ }
+
+ explicit operator bool () const
+ {
+ return !eof();
+ }
+
+ private:
+ size_t size() const { return m_fixedSize ? m_size : m_vector.size(); }
+
+ uint8_t* buffer()
+ {
+ if(m_fixedSize)
+ return m_buffer;
+ return m_vector.data();
+ }
+
+ void destroy()
+ {
+ m_buffer = nullptr;
+ m_size = 0;
+ m_fixedSize = false;
+ m_readPos = 0;
+ m_writePos = 0;
+ m_vector.clear();
+ m_fail = false;
+ }
+
+ uint8_t* m_buffer = nullptr;
+ size_t m_size = 0;
+ bool m_fixedSize = false;
+ size_t m_readPos = 0;
+ size_t m_writePos = 0;
+ std::vector<uint8_t> m_vector;
+ bool m_fail = false;
+ };
+
+ using StreamSizeType = uint32_t;
+
+ class BinaryStream final : StreamBuffer
+ {
+ public:
+ using Base = StreamBuffer;
+ using SizeType = StreamSizeType;
+
BinaryStream() = default;
+ using StreamBuffer::operator bool;
+
+ explicit BinaryStream(BinaryStream& _parent, SizeType _length) : StreamBuffer(_parent, _length)
+ {
+ }
+
template<typename T> explicit BinaryStream(const std::vector<T>& _data)
{
- std::stringstream::write(reinterpret_cast<const char*>(_data.data()), _data.size() * sizeof(T));
+ Base::write(reinterpret_cast<const uint8_t*>(_data.data()), _data.size() * sizeof(T));
seekg(0);
}
@@ -23,17 +158,29 @@ namespace synthLib
// tools
//
- void toVector(std::vector<uint8_t>& _buffer)
+ void toVector(std::vector<uint8_t>& _buffer, const 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);
+ Base::read(&_buffer[currentSize], size);
+ }
+ else
+ {
+ _buffer.resize(size);
+ Base::read(_buffer.data(), size);
+ }
}
bool checkString(const std::string& _str)
@@ -48,19 +195,26 @@ namespace synthLib
}
std::string s;
s.resize(size);
- std::stringstream::read(s.data(), size);
+ Base::read(reinterpret_cast<uint8_t*>(s.data()), size);
const auto result = _str == s;
seekg(pos);
return result;
}
+ uint32_t getWritePos() const { return static_cast<uint32_t>(tellp()); }
+ uint32_t getReadPos() const { return static_cast<uint32_t>(tellg()); }
+ bool endOfStream() const { return eof(); }
+
+ void setWritePos(const uint32_t _pos) { seekp(_pos); }
+ void setReadPos(const uint32_t _pos) { seekg(_pos); }
+
// ___________________________________
// write
//
template<typename T, typename = std::enable_if_t<std::is_trivially_copyable_v<T>>> void write(const T& _value)
{
- std::stringstream::write(reinterpret_cast<const char*>(&_value), sizeof(_value));
+ Base::write(reinterpret_cast<const uint8_t*>(&_value), sizeof(_value));
}
template<typename T, typename = std::enable_if_t<std::is_trivially_copyable_v<T>>> void write(const std::vector<T>& _vector)
@@ -68,14 +222,14 @@ namespace synthLib
const auto size = static_cast<SizeType>(_vector.size());
write(size);
if(size)
- std::stringstream::write(reinterpret_cast<const char*>(_vector.data()), sizeof(T) * size);
+ Base::write(reinterpret_cast<const uint8_t*>(_vector.data()), sizeof(T) * size);
}
void write(const std::string& _string)
{
const auto s = static_cast<SizeType>(_string.size());
write(s);
- std::stringstream::write(_string.c_str(), s);
+ Base::write(reinterpret_cast<const uint8_t*>(_string.c_str()), s);
}
void write(const char* const _value)
@@ -83,6 +237,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
//
@@ -90,7 +254,7 @@ namespace synthLib
template<typename T, typename = std::enable_if_t<std::is_trivially_copyable_v<T>>> T read()
{
T v{};
- std::stringstream::read(reinterpret_cast<char*>(&v), sizeof(v));
+ Base::read(reinterpret_cast<uint8_t*>(&v), sizeof(v));
checkFail();
return v;
}
@@ -105,7 +269,7 @@ namespace synthLib
return;
}
_vector.resize(size);
- std::stringstream::read(reinterpret_cast<char*>(_vector.data()), sizeof(T) * size);
+ Base::read(reinterpret_cast<uint8_t*>(_vector.data()), sizeof(T) * size);
checkFail();
}
@@ -114,11 +278,46 @@ namespace synthLib
const auto size = read<SizeType>();
std::string s;
s.resize(size);
- std::stringstream::read(s.data(), size);
+ Base::read(reinterpret_cast<uint8_t*>(s.data()), size);
checkFail();
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>();
+ }
+
+ BinaryStream readChunk();
+ template<size_t N, std::enable_if_t<N == 5, void*> = nullptr>
+ BinaryStream tryReadChunk(char const(&_4Cc)[N], uint32_t _versionMax = 1)
+ {
+ return tryReadChunkInternal(_4Cc, _versionMax);
+ }
+
+ private:
+ BinaryStream tryReadChunkInternal(const char* _4Cc, uint32_t _versionMax = 1);
+
+
// ___________________________________
// helpers
//
@@ -130,4 +329,142 @@ namespace synthLib
throw std::range_error("end-of-stream");
}
};
+
+ struct Chunk
+ {
+ using SizeType = BinaryStream::SizeType;
+
+ char fourCC[5];
+ uint32_t version;
+ SizeType length;
+ BinaryStream data;
+
+ bool read(BinaryStream& _parentStream)
+ {
+ _parentStream.read4CC(fourCC);
+ version = _parentStream.read<uint32_t>();
+ length = _parentStream.read<SizeType>();
+ data = BinaryStream(_parentStream, length);
+ return !data.endOfStream();
+ }
+ };
+
+ 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() = delete;
+ ChunkWriter(ChunkWriter&&) = delete;
+ ChunkWriter(const ChunkWriter&) = delete;
+ ChunkWriter& operator = (ChunkWriter&&) = delete;
+ ChunkWriter& operator = (const ChunkWriter&) = delete;
+
+ ~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 ChunkCallbackData
+ {
+ char fourCC[5];
+ uint32_t expectedVersion;
+ ChunkCallback callback;
+ };
+
+ explicit 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)
+ {
+ ChunkCallbackData c;
+ strcpy(c.fourCC, _4Cc);
+ c.expectedVersion = _version;
+ c.callback = _callback;
+ supportedChunks.emplace_back(std::move(c));
+ }
+
+ void read(const uint32_t _count = 0)
+ {
+ uint32_t count = 0;
+
+ while(!m_stream.endOfStream() && (!_count || ++count <= _count))
+ {
+ Chunk chunk;
+ chunk.read(m_stream);
+
+ ++m_numChunks;
+
+ for (const auto& chunkData : supportedChunks)
+ {
+ if(0 != strcmp(chunkData.fourCC, chunk.fourCC))
+ continue;
+
+ if(chunk.version > chunkData.expectedVersion)
+ break;
+
+ ++m_numRead;
+ chunkData.callback(chunk.data, chunk.version);
+ break;
+ }
+ }
+ }
+
+ bool tryRead(const uint32_t _count = 0)
+ {
+ const auto pos = m_stream.getReadPos();
+ try
+ {
+ read(_count);
+ 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<ChunkCallbackData> 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.cpp b/source/synthLib/device.cpp
@@ -4,6 +4,8 @@
#include "../dsp56300/source/dsp56kEmu/dsp.h"
#include "../dsp56300/source/dsp56kEmu/memory.h"
+#include <cmath>
+
using namespace dsp56k;
namespace synthLib
@@ -15,10 +17,10 @@ namespace synthLib
{
std::vector<float> buf;
buf.resize(_numSamples);
- const auto ptr = &buf[0];
+ const auto ptr = buf.data();
- TAudioInputs in = {ptr, ptr, nullptr, nullptr};//, nullptr, nullptr, nullptr, nullptr};
- TAudioOutputs out = {ptr, ptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
+ const TAudioInputs in = {ptr, ptr, nullptr, nullptr};//, nullptr, nullptr, nullptr, nullptr};
+ const TAudioOutputs out = {ptr, ptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
std::vector<SMidiEvent> midi;
@@ -48,4 +50,47 @@ namespace synthLib
LOG("Warning, limited requested latency " << _size << " to maximum value " << maxLatency << ", audio will be out of sync!");
}
}
+
+ bool Device::isSamplerateSupported(const float& _samplerate) const
+ {
+ for (const auto& sr : getSupportedSamplerates())
+ {
+ if(std::fabs(sr - _samplerate) < 1.0f)
+ return true;
+ }
+ return false;
+ }
+
+ bool Device::setSamplerate(const float _samplerate)
+ {
+ return isSamplerateSupported(_samplerate);
+ }
+
+ float Device::getDeviceSamplerate(const float _preferredDeviceSamplerate, const float _hostSamplerate) const
+ {
+ if(_preferredDeviceSamplerate > 0.0f && isSamplerateSupported(_preferredDeviceSamplerate))
+ return _preferredDeviceSamplerate;
+ return getDeviceSamplerateForHostSamplerate(_hostSamplerate);
+ }
+
+ float Device::getDeviceSamplerateForHostSamplerate(const float _hostSamplerate) const
+ {
+ const auto preferred = getPreferredSamplerates();
+
+ // if there is no choice we need to use the only one that is supported
+ if(preferred.size() == 1)
+ return preferred.front();
+
+ // find the lowest possible samplerate that is higher than the host samplerate
+ const std::set samplerates(preferred.begin(), preferred.end());
+
+ for (const float sr : preferred)
+ {
+ if(sr >= _hostSamplerate)
+ return sr;
+ }
+
+ // if all supported device samplerates are lower than the host samplerate, use the maximum that the device supports
+ return *samplerates.rbegin();
+ }
}
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
{
@@ -22,15 +24,38 @@ namespace synthLib
virtual uint32_t getInternalLatencyMidiToOutput() const { return 0; }
virtual uint32_t getInternalLatencyInputToOutput() const { return 0; }
+ virtual std::vector<float> getSupportedSamplerates() const
+ {
+ return {getSamplerate()};
+ }
virtual float getSamplerate() const = 0;
+ virtual std::vector<float> getPreferredSamplerates() const
+ {
+ return getSupportedSamplerates();
+ }
+
+ bool isSamplerateSupported(const float& _samplerate) const;
+
+ virtual bool setSamplerate(float _samplerate);
+
+ float getDeviceSamplerate(float _preferredDeviceSamplerate, float _hostSamplerate) const;
+ float getDeviceSamplerateForHostSamplerate(float _hostSamplerate) const;
+
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/dspMemoryPatch.cpp b/source/synthLib/dspMemoryPatch.cpp
@@ -0,0 +1,77 @@
+#include "dspMemoryPatch.h"
+
+#include "dsp56kEmu/dsp.h"
+
+namespace synthLib
+{
+ std::string DspMemoryPatch::toString() const
+ {
+ std::stringstream ss;
+ if(area >= dsp56k::MemArea_COUNT)
+ ss << '?';
+ else
+ ss << dsp56k::g_memAreaNames[area];
+
+ ss << ':' << HEX(address) << '=' << HEX(newValue);
+
+ if(newValue != expectedOldValue)
+ ss << ", expected old = " << HEX(expectedOldValue);
+ return ss.str();
+ }
+
+ bool DspMemoryPatches::apply(dsp56k::DSP& _dsp, const std::initializer_list<DspMemoryPatch>& _patches)
+ {
+ bool res = true;
+
+ for (const auto& patch : _patches)
+ res &= apply(_dsp, patch);
+
+ return res;
+ }
+
+ bool DspMemoryPatches::apply(dsp56k::DSP& _dsp, const DspMemoryPatch& _patch)
+ {
+ auto& mem = _dsp.memory();
+
+ if(_patch.area == dsp56k::MemArea_COUNT)
+ {
+ LOG("Failed to apply patch, memory area is not valid, for patch " << _patch.toString());
+ return false;
+ }
+
+ if(_patch.address >= mem.size(_patch.area))
+ {
+ LOG("Failed to apply patch, address " << HEX(_patch.address) << " is out of range, area " << dsp56k::g_memAreaNames[_patch.area] << " size is " << HEX(mem.size(_patch.area)) << ", for patch " << _patch.toString());
+ return false;
+ }
+
+ if(_patch.expectedOldValue != _patch.newValue)
+ {
+ const auto v = mem.get(_patch.area, _patch.address);
+ if(v != _patch.expectedOldValue)
+ {
+ LOG("Failed to apply patch, expected current value to be " << HEX(_patch.expectedOldValue) << " but current value is " << HEX(v) << ", for patch " << _patch.toString());
+ return false;
+ }
+ }
+
+ if(_patch.area == dsp56k::MemArea_P)
+ _dsp.memWriteP(_patch.address, _patch.newValue);
+ else
+ _dsp.memWrite(_patch.area, _patch.address, _patch.newValue);
+
+ LOG("Successfully applied patch " << _patch.toString());
+
+ return true;
+ }
+
+ bool DspMemoryPatches::apply(dsp56k::DSP& _dsp, const MD5& _md5) const
+ {
+ for (const auto& e : allowedTargets)
+ {
+ if(e == _md5)
+ return apply(_dsp, patches);
+ }
+ return false;
+ }
+}
diff --git a/source/synthLib/dspMemoryPatch.h b/source/synthLib/dspMemoryPatch.h
@@ -0,0 +1,62 @@
+#pragma once
+
+#include "md5.h"
+#include "dsp56kEmu/types.h"
+
+namespace dsp56k
+{
+ class DSP;
+}
+
+namespace synthLib
+{
+ namespace dspOpcodes
+ {
+ static constexpr dsp56k::TWord g_nop = 0x000000;
+ static constexpr dsp56k::TWord g_wait = 0x000086;
+ }
+
+ struct DspMemoryPatch
+ {
+ dsp56k::EMemArea area = dsp56k::MemArea_COUNT;
+ dsp56k::TWord address = 0;
+ dsp56k::TWord expectedOldValue = 0;
+ dsp56k::TWord newValue = 0;
+
+ constexpr bool operator == (const DspMemoryPatch& _p) const
+ {
+ return area == _p.area && address == _p.address && expectedOldValue == _p.expectedOldValue && newValue == _p.newValue;
+ }
+
+ constexpr bool operator != (const DspMemoryPatch& _p) const
+ {
+ return !(*this == _p);
+ }
+
+ constexpr bool operator < (const DspMemoryPatch& _p) const
+ {
+ if(area < _p.area) return true;
+ if(area > _p.area) return false;
+ if(address < _p.address) return true;
+ if(address > _p.address) return false;
+ if(expectedOldValue < _p.expectedOldValue) return true;
+ if(expectedOldValue > _p.expectedOldValue) return false;
+ if(newValue < _p.newValue) return true;
+ /*if(newValue > _p.newValue)*/ return false;
+ }
+
+ std::string toString() const;
+ };
+
+ struct DspMemoryPatches
+ {
+ std::initializer_list<MD5> allowedTargets;
+ std::initializer_list<DspMemoryPatch> patches;
+
+ bool apply(dsp56k::DSP& _dsp, const MD5& _md5) const;
+
+ private:
+ static bool apply(dsp56k::DSP& _dsp, const std::initializer_list<DspMemoryPatch>& _patches);
+ static bool apply(dsp56k::DSP& _dsp, const DspMemoryPatch& _patch);
+ };
+}
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/md5.cpp b/source/synthLib/md5.cpp
@@ -0,0 +1,145 @@
+#include "md5.h"
+
+#include "dsp56kEmu/logging.h"
+
+#include <cstring> // memcpy
+
+namespace synthLib
+{
+ // leftrotate function definition
+ uint32_t leftrotate(const uint32_t x, const uint32_t c)
+ {
+ return (((x) << (c)) | ((x) >> (32 - (c))));
+ }
+
+ // These vars will contain the hash: h0, h1, h2, h3
+ void md5(uint32_t& _h0, uint32_t& _h1, uint32_t& _h2, uint32_t& _h3, const uint8_t *_initialMsg, const uint32_t _initialLen)
+ {
+ // Note: All variables are unsigned 32 bit and wrap modulo 2^32 when calculating
+
+ // r specifies the per-round shift amounts
+
+ constexpr uint32_t r[] = {7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
+ 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
+ 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
+ 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 };
+
+ // Use binary integer part of the sines of integers (in radians) as constants// Initialize variables:
+ constexpr uint32_t k[] = {
+ 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
+ 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
+ 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
+ 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
+ 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
+ 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
+ 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
+ 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
+ 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
+ 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
+ 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
+ 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
+ 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
+ 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
+ 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
+ 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 };
+
+ _h0 = 0x67452301;
+ _h1 = 0xefcdab89;
+ _h2 = 0x98badcfe;
+ _h3 = 0x10325476;
+
+ // Pre-processing: adding a single 1 bit
+ //append "1" bit to message
+ /* Notice: the input bytes are considered as bits strings,
+ where the first bit is the most significant bit of the byte.[37] */
+
+ // Pre-processing: padding with zeros
+ //append "0" bit until message length in bit ≡ 448 (mod 512)
+ //append length mod (2 pow 64) to message
+
+ const uint32_t newLen = ((((_initialLen + 8) / 64) + 1) * 64) - 8;
+
+ std::vector<uint8_t> buffer;
+ buffer.resize(newLen + 64, 0);
+ auto* msg = buffer.data();
+ memcpy(msg, _initialMsg, _initialLen);
+ msg[_initialLen] = 128; // write the "1" bit
+
+ const uint32_t bitsLen = 8 * _initialLen; // note, we append the len
+ memcpy(msg + newLen, &bitsLen, 4); // in bits at the end of the buffer
+
+ // Process the message in successive 512-bit chunks:
+ //for each 512-bit chunk of message:
+ for (uint32_t offset = 0; offset < newLen; offset += (512 / 8))
+ {
+ // break chunk into sixteen 32-bit words w[j], 0 ≤ j ≤ 15
+ const auto* w = reinterpret_cast<uint32_t*>(msg + offset);
+
+ // Initialize hash value for this chunk:
+ uint32_t a = _h0;
+ uint32_t b = _h1;
+ uint32_t c = _h2;
+ uint32_t d = _h3;
+
+ // Main loop:
+ for (uint32_t i = 0; i < 64; i++)
+ {
+ uint32_t f, g;
+
+ if (i < 16) {
+ f = (b & c) | ((~b) & d);
+ g = i;
+ }
+ else if (i < 32) {
+ f = (d & b) | ((~d) & c);
+ g = (5 * i + 1) % 16;
+ }
+ else if (i < 48) {
+ f = b ^ c ^ d;
+ g = (3 * i + 5) % 16;
+ }
+ else {
+ f = c ^ (b | (~d));
+ g = (7 * i) % 16;
+ }
+
+ const uint32_t temp = d;
+ d = c;
+ c = b;
+// printf("rotateLeft(%x + %x + %x + %x, %d)\n", a, f, k[i], w[g], r[i]);
+ b = b + leftrotate((a + f + k[i] + w[g]), r[i]);
+ a = temp;
+ }
+
+ // Add this chunk's hash to result so far:
+
+ _h0 += a;
+ _h1 += b;
+ _h2 += c;
+ _h3 += d;
+
+ }
+
+ // cleanup
+ // free(msg);
+ }
+
+ MD5::MD5(const std::vector<uint8_t>& _data)
+ {
+ md5(m_h[0], m_h[1], m_h[2], m_h[3], _data.data(), static_cast<uint32_t>(_data.size()));
+ }
+
+ std::string MD5::toString() const
+ {
+ std::stringstream ss;
+
+ for (const auto& e : m_h)
+ {
+ ss << HEXN((e & 0xff), 2);
+ ss << HEXN(((e>>8u) & 0xff), 2);
+ ss << HEXN(((e>>16u) & 0xff), 2);
+ ss << HEXN(((e>>24u) & 0xff), 2);
+ }
+ return ss.str();
+ }
+}
diff --git a/source/synthLib/md5.h b/source/synthLib/md5.h
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <array>
+#include <cassert>
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace synthLib
+{
+ class MD5
+ {
+ public:
+ static constexpr uint32_t parse1(const char _b)
+ {
+ if (_b >= '0' && _b <= '9')
+ return _b - '0';
+ if (_b >= 'A' && _b <= 'F')
+ return _b - 'A' + 10;
+ if (_b >= 'a' && _b <= 'f')
+ return _b - 'a' + 10;
+ assert(false);
+ return 0;
+ }
+ static constexpr uint32_t parse2(const char _b0, const char _b1)
+ {
+ return parse1(_b1) << 4 | parse1(_b0);
+ }
+ static constexpr uint32_t parse4(const char _b0, const char _b1, const char _b2, const char _b3)
+ {
+ return parse2(_b3, _b2) << 8 | parse2(_b1, _b0);
+ }
+ static constexpr uint32_t parse8(const char _b0, const char _b1, const char _b2, const char _b3, const char _b4, const char _b5, const char _b6, const char _b7)
+ {
+ return parse4(_b4, _b5, _b6, _b7) << 16 | parse4(_b0, _b1, _b2, _b3);
+ }
+
+ template<size_t N, std::enable_if_t<N == 33, void*> = nullptr> constexpr MD5(char const(&_digest)[N])
+ : m_h
+ {
+ parse8(_digest[ 0], _digest[ 1], _digest[ 2], _digest[ 3], _digest[ 4], _digest[ 5], _digest[ 6], _digest[ 7]),
+ parse8(_digest[ 8], _digest[ 9], _digest[10], _digest[11], _digest[12], _digest[13], _digest[14], _digest[15]),
+ parse8(_digest[16], _digest[17], _digest[18], _digest[19], _digest[20], _digest[21], _digest[22], _digest[23]),
+ parse8(_digest[24], _digest[25], _digest[26], _digest[27], _digest[28], _digest[29], _digest[30], _digest[31])
+ }
+ {
+ }
+
+ explicit MD5(const std::vector<uint8_t>& _data);
+
+ MD5() : m_h({0,0,0,0}) {}
+
+ MD5(const MD5& _src) = default;
+ MD5(MD5&& _src) = default;
+
+ ~MD5() = default;
+
+ MD5& operator = (const MD5&) = default;
+ MD5& operator = (MD5&&) = default;
+
+ std::string toString() const;
+
+ constexpr bool operator == (const MD5& _md5) const
+ {
+ return m_h[0] == _md5.m_h[0] && m_h[1] == _md5.m_h[1] && m_h[2] == _md5.m_h[2] && m_h[3] == _md5.m_h[3];
+ }
+
+ constexpr bool operator != (const MD5& _md5) const
+ {
+ return !(*this == _md5);
+ }
+
+ constexpr bool operator < (const MD5& _md5) const
+ {
+ if(m_h[0] < _md5.m_h[0]) return true;
+ if(m_h[0] > _md5.m_h[0]) return false;
+ if(m_h[1] < _md5.m_h[1]) return true;
+ if(m_h[1] > _md5.m_h[1]) return false;
+ if(m_h[2] < _md5.m_h[2]) return true;
+ if(m_h[2] > _md5.m_h[2]) return false;
+ if(m_h[3] < _md5.m_h[3]) return true;
+ /*if(m_h[3] > _md5.m_h[3])*/ return false;
+ }
+
+ private:
+ std::array<uint32_t, 4> m_h;
+ };
+}
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
@@ -3,7 +3,7 @@
#include "../dsp56300/source/dsp56kEmu/logging.h"
#ifndef _WIN32
-// filesystem is only available on Mac OS Catalina 10.15+
+// filesystem is only available on macOS Catalina 10.15+
// filesystem causes linker errors in gcc-8 if linked statically
#define USE_DIRENT
#endif
@@ -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);
}
@@ -154,22 +159,31 @@ namespace synthLib
#else
try
{
- for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(_folder))
+ const auto u8Path = std::filesystem::u8path(_folder);
+ for (const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(u8Path))
{
const auto &file = entry.path();
- _files.push_back(file.string());
+ try
+ {
+ _files.push_back(file.u8string());
+ }
+ catch(std::exception& e)
+ {
+ LOG(e.what());
+ }
}
}
- catch (...)
+ catch (std::exception& e)
{
+ LOG(e.what());
return false;
}
#endif
return !_files.empty();
}
- static std::string lowercase(const std::string &_src)
+ std::string lowercase(const std::string &_src)
{
std::string str(_src);
for (char& i : str)
@@ -177,7 +191,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 +199,47 @@ namespace synthLib
return {};
}
+ std::string getFilenameWithoutPath(const std::string& _name)
+ {
+ const auto pos = _name.find_last_of("/\\");
+ if (pos != std::string::npos)
+ return _name.substr(pos + 1);
+ return _name;
+ }
+
+ std::string getPath(const std::string& _filename)
+ {
+ const auto pos = _filename.find_last_of("/\\");
+ if (pos != std::string::npos)
+ return _filename.substr(0, pos);
+ return _filename;
+ }
+
+ size_t getFileSize(const std::string& _file)
+ {
+ FILE* hFile = openFile(_file, "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 +283,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 +318,9 @@ namespace synthLib
bool hasExtension(const std::string& _filename, const std::string& _extension)
{
+ if (_extension.empty())
+ return true;
+
return lowercase(getExtension(_filename)) == lowercase(_extension);
}
@@ -283,12 +335,12 @@ namespace synthLib
bool writeFile(const std::string& _filename, const std::vector<uint8_t>& _data)
{
- return writeFile(_filename, &_data[0], _data.size());
+ return writeFile(_filename, _data.data(), _data.size());
}
bool writeFile(const std::string& _filename, const uint8_t* _data, size_t _size)
{
- auto* hFile = fopen(_filename.c_str(), "wb");
+ auto* hFile = openFile(_filename, "wb");
if(!hFile)
return false;
const auto written = fwrite(&_data[0], 1, _size, hFile);
@@ -298,7 +350,7 @@ namespace synthLib
bool readFile(std::vector<uint8_t>& _data, const std::string& _filename)
{
- auto* hFile = fopen(_filename.c_str(), "rb");
+ auto* hFile = openFile(_filename, "rb");
if(!hFile)
return false;
@@ -313,11 +365,29 @@ 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);
+ const auto read = fread(_data.data(), 1, _data.size(), hFile);
fclose(hFile);
return read == _data.size();
}
+
+ FILE* openFile(const std::string& _name, const char* _mode)
+ {
+#ifdef _WIN32
+ // convert filename
+ std::wstring nameW;
+ nameW.resize(_name.size());
+ const int newSize = MultiByteToWideChar(CP_UTF8, 0, _name.c_str(), static_cast<int>(_name.size()), const_cast<wchar_t *>(nameW.c_str()), static_cast<int>(_name.size()));
+ nameW.resize(newSize);
+
+ // convert mode
+ wchar_t mode[32]{0};
+ MultiByteToWideChar(CP_UTF8, 0, _mode, static_cast<int>(strlen(_mode)), mode, (int)std::size(mode));
+ return _wfopen(nameW.c_str(), mode);
+#else
+ return fopen(_name.c_str(), _mode);
+#endif
+ }
} // namespace synthLib
diff --git a/source/synthLib/os.h b/source/synthLib/os.h
@@ -14,13 +14,22 @@ namespace synthLib
bool getDirectoryEntries(std::vector<std::string>& _files, const std::string& _folder);
+ std::string lowercase(const std::string &_src);
+
std::string findFile(const std::string& _extension, size_t _minSize, size_t _maxSize);
bool findFiles(std::vector<std::string>& _files, const std::string& _rootPath, const std::string& _extension, size_t _minSize, size_t _maxSize);
std::string findFile(const std::string& _rootPath, const std::string& _extension, size_t _minSize, size_t _maxSize);
- std::string findROM(size_t _minSize, size_t _maxSize);
+
+ std::string findROM(size_t _minSize, size_t _maxSize);
std::string findROM(size_t _expectedSize = 524288);
+ std::string getExtension(const std::string& _name);
+ std::string getFilenameWithoutPath(const std::string& _name);
+ std::string getPath(const std::string& _filename);
+
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();
@@ -33,4 +42,6 @@ namespace synthLib
}
bool readFile(std::vector<uint8_t>& _data, const std::string& _filename);
+
+ FILE* openFile(const std::string& _name, const char* _mode);
} // namespace synthLib
diff --git a/source/synthLib/plugin.cpp b/source/synthLib/plugin.cpp
@@ -8,7 +8,7 @@
#if 0
#define LOGMC(S) LOG(S)
#else
-#define LOGMC(S) {}
+#define LOGMC(S) do{}while(false)
#endif
using namespace synthLib;
@@ -17,9 +17,11 @@ namespace synthLib
{
constexpr uint8_t g_stateVersion = 1;
- Plugin::Plugin(Device* _device) : m_resampler(_device->getChannelCountIn(), _device->getChannelCountOut()), m_device(_device)
+ Plugin::Plugin(Device* _device)
+ : m_resampler(_device->getChannelCountIn(), _device->getChannelCountOut())
+ , m_device(_device)
+ , m_deviceSamplerate(_device->getSamplerate())
{
- m_resampler.setDeviceSamplerate(_device->getSamplerate());
}
void Plugin::addMidiEvent(const SMidiEvent& _ev)
@@ -34,12 +36,36 @@ namespace synthLib
m_midiInRingBuffer.push_back(_ev);
}
- void Plugin::setSamplerate(float _samplerate)
+ bool Plugin::setPreferredDeviceSamplerate(const float _samplerate)
{
std::lock_guard lock(m_lock);
- m_resampler.setHostSamplerate(_samplerate);
+
+ const auto sr = m_device->getDeviceSamplerate(_samplerate, m_hostSamplerate);
+
+ if(sr == m_deviceSamplerate)
+ return true;
+
+ if(!m_device->setSamplerate(sr))
+ return false;
+
+ m_deviceSamplerate = sr;
+ m_resampler.setSamplerates(m_hostSamplerate, m_deviceSamplerate);
+
+ updateDeviceLatency();
+ return true;
+ }
+
+ void Plugin::setHostSamplerate(const float _samplerate, float _preferredDeviceSamplerate)
+ {
+ std::lock_guard lock(m_lock);
+
+ m_deviceSamplerate = m_device->getDeviceSamplerate(_preferredDeviceSamplerate, _samplerate);
+ m_device->setSamplerate(m_deviceSamplerate);
+ m_resampler.setSamplerates(_samplerate, m_deviceSamplerate);
+
m_hostSamplerate = _samplerate;
m_hostSamplerateInv = _samplerate > 0 ? 1.0f / _samplerate : 0.0f;
+
updateDeviceLatency();
}
@@ -85,6 +111,30 @@ namespace synthLib
return m_device->isValid();
}
+ void Plugin::setDevice(Device* _device)
+ {
+ if(!_device)
+ return;
+
+ std::lock_guard lock(m_lock);
+
+ std::vector<uint8_t> deviceState;
+ getState(deviceState, StateTypeGlobal);
+
+ delete m_device;
+
+ m_device = _device;
+
+ m_device->setSamplerate(m_deviceSamplerate);
+ setState(deviceState);
+
+ // MIDI clock has to send the start event again, some device find it confusing and do strange things if there isn't any
+ m_needsStart = true;
+
+ updateDeviceLatency();
+ }
+
+#if !SYNTHLIB_DEMO_MODE
bool Plugin::getState(std::vector<uint8_t>& _state, StateType _type) const
{
if(!m_device)
@@ -119,7 +169,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)
@@ -221,7 +271,7 @@ namespace synthLib
if(m_dummyBuffer.size() < _minimumSize)
m_dummyBuffer.resize(_minimumSize);
- return &m_dummyBuffer[0];
+ return m_dummyBuffer.data();
}
void Plugin::updateDeviceLatency()
@@ -248,7 +298,7 @@ namespace synthLib
void Plugin::processMidiInEvent(const SMidiEvent& _ev)
{
- // sysex might be send in multiple chunks. Happens if coming from hardware
+ // sysex might be sent in multiple chunks. Happens if coming from hardware
if (!_ev.sysex.empty())
{
const bool isComplete = _ev.sysex.front() == M_STARTOFSYSEX && _ev.sysex.back() == M_ENDOFSYSEX;
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"
@@ -20,7 +21,11 @@ namespace synthLib
void addMidiEvent(const SMidiEvent& _ev);
- void setSamplerate(float _samplerate);
+ bool setPreferredDeviceSamplerate(float _samplerate);
+
+ void setHostSamplerate(float _hostSamplerate, float _preferredDeviceSamplerate);
+ float getHostSamplerate() const { return m_hostSamplerate; }
+
void setBlockSize(uint32_t _blockSize);
uint32_t getLatencyMidiToOutput() const;
@@ -31,9 +36,13 @@ namespace synthLib
bool isValid() const;
+ Device* getDevice() const { return m_device; }
+ void setDevice(Device* _device);
+
+#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);
@@ -56,7 +65,7 @@ namespace synthLib
mutable std::mutex m_lock;
mutable std::mutex m_lockAddMidiEvent;
- Device* const m_device;
+ Device* m_device;
std::vector<float> m_dummyBuffer;
@@ -73,5 +82,7 @@ namespace synthLib
bool m_needsStart = false;
double m_clockTickPos = 0.0;
uint32_t m_extraLatencyBlocks = 1;
+
+ float m_deviceSamplerate = 0.0f;
};
}
diff --git a/source/synthLib/resamplerInOut.cpp b/source/synthLib/resamplerInOut.cpp
@@ -37,6 +37,17 @@ namespace synthLib
recreate();
}
+ void ResamplerInOut::setSamplerates(const float _hostSamplerate, const float _deviceSamplerate)
+ {
+ if(m_samplerateDevice == _deviceSamplerate && m_samplerateHost == _hostSamplerate)
+ return;
+
+ m_samplerateDevice = _deviceSamplerate;
+ m_samplerateHost = _hostSamplerate;
+
+ recreate();
+ }
+
void ResamplerInOut::recreate()
{
if(m_samplerateDevice < 1 || m_samplerateHost < 1)
diff --git a/source/synthLib/resamplerInOut.h b/source/synthLib/resamplerInOut.h
@@ -18,6 +18,7 @@ namespace synthLib
void setDeviceSamplerate(float _samplerate);
void setHostSamplerate(float _samplerate);
+ void setSamplerates(float _hostSamplerate, float _deviceSamplerate);
void process(const TAudioInputs& _inputs, TAudioOutputs& _outputs, const TMidiVec& _midiIn, TMidiVec& _midiOut, uint32_t _numSamples, const TProcessFunc& _processFunc);
diff --git a/source/virusConsoleLib/CMakeLists.txt b/source/virusConsoleLib/CMakeLists.txt
@@ -15,3 +15,4 @@ target_sources(virusConsoleLib PRIVATE ${SOURCES})
source_group("source" FILES ${SOURCES})
target_link_libraries(virusConsoleLib PUBLIC virusLib)
+set_property(TARGET virusConsoleLib PROPERTY FOLDER "Virus")
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
@@ -6,6 +6,9 @@
#include "esaiListenerToFile.h"
#include "../virusLib/device.h"
+#include "../virusLib/romloader.h"
+
+#include "dsp56kEmu/dsp.h"
namespace virusLib
{
@@ -19,7 +22,7 @@ class EsaiListener;
ConsoleApp::ConsoleApp(const std::string& _romFile)
: m_romName(_romFile)
-, m_rom(_romFile)
+, m_rom(ROMLoader::findROM(_romFile))
, m_preset({})
{
if (!m_rom.isValid())
@@ -29,18 +32,18 @@ ConsoleApp::ConsoleApp(const std::string& _romFile)
}
virusLib::DspSingle* dsp1 = nullptr;
- virusLib::Device::createDspInstances(dsp1, m_dsp2, m_rom);
+ virusLib::Device::createDspInstances(dsp1, m_dsp2, m_rom, 46875.0f);
m_dsp1.reset(dsp1);
- uc.reset(new Microcontroller(*m_dsp1, m_rom, false));
+ m_uc.reset(new Microcontroller(*m_dsp1, m_rom, false));
if(m_dsp2)
- uc->addDSP(*m_dsp2, false);
+ m_uc->addDSP(*m_dsp2, false);
}
ConsoleApp::~ConsoleApp()
{
m_demo.reset();
- uc.reset();
+ m_uc.reset();
m_dsp1.reset();
m_dsp2 = nullptr;
}
@@ -55,17 +58,9 @@ void ConsoleApp::waitReturn()
std::cin.ignore();
}
-std::thread ConsoleApp::bootDSP(const bool _createDebugger) const
+void ConsoleApp::bootDSP(const bool _createDebugger) const
{
- auto loader = virusLib::Device::bootDSP(*m_dsp1, m_rom, _createDebugger);
-
- if(m_dsp2)
- {
- auto loader2 = virusLib::Device::bootDSP(*m_dsp2, m_rom, false);
- loader2.join();
- }
-
- return loader;
+ virusLib::Device::bootDSPs(m_dsp1.get(), m_dsp2, m_rom, _createDebugger);
}
dsp56k::IPeripherals& ConsoleApp::getYPeripherals() const
@@ -126,7 +121,7 @@ bool ConsoleApp::loadSingle(const std::string& _preset)
bool ConsoleApp::loadDemo(const std::string& _filename)
{
- m_demo.reset(new DemoPlayback(*uc));
+ m_demo.reset(new DemoPlayback(*m_uc));
if(m_demo->loadFile(_filename))
{
@@ -143,7 +138,7 @@ bool ConsoleApp::loadInternalDemo()
if(m_rom.getDemoData().empty())
return false;
- m_demo.reset(new DemoPlayback(*uc));
+ m_demo.reset(new DemoPlayback(*m_uc));
if(m_demo->loadBinData(m_rom.getDemoData()))
{
@@ -171,147 +166,53 @@ std::string ConsoleApp::getSingleNameAsFilename() const
return "virusEmu_" + audioFilename + ".wav";
}
-void ConsoleApp::audioCallback(uint32_t audioCallbackCount)
+void ConsoleApp::audioCallback(const uint32_t _audioCallbackCount)
{
- uc->process(1); // FIXME wrong value
+ m_uc->process();
- constexpr uint8_t baseChannel = 0;
-
- switch (audioCallbackCount)
+ switch (_audioCallbackCount)
{
case 1:
+ m_dsp1->drainESSI1();
LOG("Sending Init Control Commands");
- uc->sendInitControlCommands();
+ m_uc->sendInitControlCommands();
break;
case 256:
+ m_dsp1->drainESSI1();
+ m_dsp1->disableESSI1();
if(!m_demo)
{
LOG("Sending Preset");
-#if 0
- uc->writeSingle(BankNumber::EditBuffer, 0, m_preset); // cmdline
- v.getSingle(1, 6, m_preset); // Anubis MS
- uc->writeSingle(BankNumber::EditBuffer, 1, m_preset);
- v.getSingle(1, 10, m_preset); // Impact MS
- uc->writeSingle(BankNumber::EditBuffer, 2, m_preset);
- v.getSingle(1, 3, m_preset); // Impact MS
- uc->writeSingle(BankNumber::EditBuffer, 3, m_preset);
-#elif 0
- uc->writeSingle(BankNumber::EditBuffer, 0, m_preset); // cmdline
- uc->writeSingle(BankNumber::EditBuffer, 1, m_preset);
- v.getSingle(1, 6, m_preset); // Anubis MS
- uc->writeSingle(BankNumber::EditBuffer, 2, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 3, m_preset);
- v.getSingle(10, 56, m_preset); // Impact MS
- uc->writeSingle(BankNumber::EditBuffer, 4, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 5, m_preset);
-#else
- uc->writeSingle(BankNumber::EditBuffer, virusLib::SINGLE, m_preset);
-#endif
-/* uc->writeSingle(BankNumber::EditBuffer, 3, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 4, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 5, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 6, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 7, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 8, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 9, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 10, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 11, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 12, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 13, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 14, m_preset);
- uc->writeSingle(BankNumber::EditBuffer, 15, m_preset);
-*/ }
+ m_uc->writeSingle(BankNumber::EditBuffer, virusLib::SINGLE, m_preset);
+ }
break;
case 512:
if(!m_demo)
{
LOG("Sending Note On");
-// uc->sendMIDI(SMidiEvent(0x90 + baseChannel, 36, 0x5f)); // Note On
-// uc->sendMIDI(SMidiEvent(0x90 + baseChannel, 48, 0x5f)); // Note On
- for(uint8_t i=0; i<1; ++i)
- uc->sendMIDI(SMidiEvent(0x90 + i, 60, 0x5f)); // Note On
-// uc->sendMIDI(SMidiEvent(0x90 + baseChannel, 60, 0x5f)); // Note On
-// uc->sendMIDI(SMidiEvent(0x90 + baseChannel, 63, 0x5f)); // Note On
-// uc->sendMIDI(SMidiEvent(0x90 + baseChannel, 67, 0x5f)); // Note On
-// uc->sendMIDI(SMidiEvent(0x90 + baseChannel, 72, 0x5f)); // Note On
-// uc->sendMIDI(SMidiEvent(0x90 + baseChannel, 75, 0x5f)); // Note On
-// uc->sendMIDI(SMidiEvent(0x90 + baseChannel, 79, 0x5f)); // Note On
-// uc->sendMIDI(SMidiEvent(0xb0, 1, 0)); // Modwheel 0
- uc->sendPendingMidiEvents(std::numeric_limits<uint32_t>::max());
+ m_uc->sendMIDI(SMidiEvent(0x90, 60, 0x5f)); // Note On
+ m_uc->sendPendingMidiEvents(std::numeric_limits<uint32_t>::max());
}
break;
-/* case 8000:
- LOG("Sending 2nd Note On");
- uc->sendMIDI(SMidiEvent(0x90, 67, 0x7f)); // Note On
- uc->sendPendingMidiEvents(std::numeric_limits<uint32_t>::max());
- break;
- case 16000:
- LOG("Sending 3rd Note On");
- uc->sendMIDI(SMidiEvent(0x90, 63, 0x7f)); // Note On
- uc->sendPendingMidiEvents(std::numeric_limits<uint32_t>::max());
- break;
-*/
}
-#if 0
- static uint8_t cycle = 0;
-
- static uint8_t channel = 0;
- static int totalNoteCount = 1;
- if(audioCallbackCount >= 1024 && (audioCallbackCount & 2047) == 0)
- {
- static uint8_t note = 127;
- if(note >= 96)
- {
- note = 24;
-
- switch(cycle)
- {
- case 0: note += 0; break;
- case 1: note += 3; break;
- case 2: note += 7; break;
- case 3: note += 10; break;
- case 4: note += 5; break;
- case 5: note += 2; break;
- }
- ++cycle;
- if(cycle == 6)
- cycle = 0;
- }
- if(cycle < 7)
- {
- totalNoteCount++;
- LOG("Sending Note On for note " << static_cast<int>(note) << ", total notes " << totalNoteCount);
- uc->sendMIDI(SMidiEvent(0x90 + baseChannel + channel, note, 0x5f)); // Note On
- channel++;
- if(channel >= 6)
-// if(channel >= 16)
- channel = 0;
- uc->sendPendingMidiEvents(std::numeric_limits<uint32_t>::max());
-
-// if(totalNoteCount >= 40)
-// dsp.enableTrace(static_cast<dsp56k::DSP::TraceMode>(dsp56k::DSP::Ops | dsp56k::DSP::Regs | dsp56k::DSP::StackIndent));
- }
- note += 12;
- }
-#endif
- if(m_demo && audioCallbackCount >= 256)
+ if(m_demo && _audioCallbackCount >= 256)
m_demo->process(1);
}
-void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampleCount/* = 0*/, bool _createDebugger/* = false*/)
+void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampleCount/* = 0*/, bool _createDebugger/* = false*/, bool _dumpAssembler/* = false*/)
{
assert(!_audioOutputFilename.empty());
// dsp.enableTrace((DSP::TraceMode)(DSP::Ops | DSP::Regs | DSP::StackIndent));
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);
- auto& esai = m_dsp1->getPeriphX().getEsai();
+ auto& esai = m_dsp1->getAudio();
int32_t notifyTimeout = 0;
esai.setCallback([&](dsp56k::Audio*)
@@ -320,7 +221,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,21 +234,22 @@ 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();
+ bootDSP(_createDebugger);
- /*
- const std::string romFile = m_romName;
- auto& mem = m_dsp1->getMemory();
+ if(_dumpAssembler)
+ {
+ const std::string romFile = m_rom.getFilename();
+ auto& mem = m_dsp1->getMemory();
- mem.saveAsText((romFile + "_X.txt").c_str(), dsp56k::MemArea_X, 0, mem.size());
- mem.saveAsText((romFile + "_Y.txt").c_str(), dsp56k::MemArea_Y, 0, mem.size());
- mem.save((romFile + "_P.bin").c_str(), dsp56k::MemArea_P);
- mem.saveAssembly((romFile + "_P.asm").c_str(), 0, mem.size(), true, false, m_dsp1->getDSP().getPeriph(0), m_dsp1->getDSP().getPeriph(1));
- */
+ mem.saveAsText((romFile + "_X.txt").c_str(), dsp56k::MemArea_X, 0, mem.sizeXY());
+ mem.saveAsText((romFile + "_Y.txt").c_str(), dsp56k::MemArea_Y, 0, mem.sizeXY());
+ mem.save((romFile + "_P.bin").c_str(), dsp56k::MemArea_P);
+ mem.saveAssembly((romFile + "_P.asm").c_str(), 0, mem.sizeP(), true, false, m_dsp1->getDSP().getPeriph(0), m_dsp1->getDSP().getPeriph(1));
+ }
std::vector<synthLib::SMidiEvent> midiEvents;
@@ -356,7 +259,7 @@ void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampl
{
sem.wait();
proc.processBlock(blockSize);
- uc->readMidiOut(midiEvents);
+ m_uc->readMidiOut(midiEvents);
midiEvents.clear();
}
diff --git a/source/virusConsoleLib/consoleApp.h b/source/virusConsoleLib/consoleApp.h
@@ -1,11 +1,6 @@
#pragma once
#include <string>
-#include "esaiListener.h"
-#include "esaiListenerToCallback.h"
-#include "dsp56kEmu/memory.h"
-#include "dsp56kEmu/dsp.h"
-
#include "../virusLib/romfile.h"
#include "../virusLib/microcontroller.h"
#include "../virusLib/demoplayback.h"
@@ -30,21 +25,21 @@ public:
static void waitReturn();
- void run(const std::string& _audioOutputFilename, uint32_t _maxSampleCount = 0, bool _createDebugger = false);
+ void run(const std::string& _audioOutputFilename, uint32_t _maxSampleCount = 0, bool _createDebugger = false, bool _dumpAssembler = false);
const virusLib::ROMFile& getRom() const { return m_rom; }
private:
- std::thread bootDSP(bool _createDebugger) const;
+ void bootDSP(bool _createDebugger) const;
dsp56k::IPeripherals& getYPeripherals() const;
- void audioCallback(uint32_t audioCallbackCount);
+ void audioCallback(uint32_t _audioCallbackCount);
const std::string m_romName;
virusLib::ROMFile m_rom;
std::unique_ptr<virusLib::DspSingle> m_dsp1;
virusLib::DspSingle* m_dsp2 = nullptr;
- std::unique_ptr<virusLib::Microcontroller> uc;
+ std::unique_ptr<virusLib::Microcontroller> m_uc;
std::unique_ptr<virusLib::DemoPlayback> m_demo;
virusLib::Microcontroller::TPreset m_preset;
diff --git a/source/virusIntegrationTest/CMakeLists.txt b/source/virusIntegrationTest/CMakeLists.txt
@@ -20,3 +20,5 @@ add_test(NAME virusIntegrationTests COMMAND ${CMAKE_COMMAND}
-DROOT_DIR=${CMAKE_BINARY_DIR}
-P ${CMAKE_CURRENT_SOURCE_DIR}/runTest.cmake)
set_tests_properties(virusIntegrationTests PROPERTIES LABELS "IntegrationTest")
+
+set_property(TARGET virusIntegrationTest PROPERTY FOLDER "Virus")
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/CMakeLists.txt b/source/virusLib/CMakeLists.txt
@@ -6,12 +6,16 @@ add_library(virusLib STATIC)
set(SOURCES
demoplayback.cpp demoplayback.h
device.cpp device.h
+ deviceModel.cpp deviceModel.h
+ dspMemoryPatches.cpp dspMemoryPatches.h
dspSingle.cpp dspSingle.h
+ frontpanelState.cpp frontpanelState.h
hdi08List.cpp hdi08List.h
hdi08MidiQueue.cpp hdi08MidiQueue.h
hdi08TxParser.cpp hdi08TxParser.h
hdi08Queue.cpp hdi08Queue.h
romfile.cpp romfile.h
+ romloader.cpp romloader.h
microcontroller.cpp microcontroller.h
microcontrollerTypes.cpp microcontrollerTypes.h
midiFileToRomData.cpp midiFileToRomData.h
@@ -26,3 +30,5 @@ target_link_libraries(virusLib PUBLIC synthLib)
if(DSP56300_DEBUGGER)
target_link_libraries(virusLib PUBLIC dsp56kDebugger)
endif()
+
+set_property(TARGET virusLib PROPERTY FOLDER "Virus")
diff --git a/source/virusLib/device.cpp b/source/virusLib/device.cpp
@@ -7,43 +7,49 @@
#include "../synthLib/deviceException.h"
+#include <cstring>
+
+#include "dspMemoryPatches.h"
+
namespace virusLib
{
- Device::Device(const ROMFile& _rom, const bool _createDebugger/* = false*/)
- : synthLib::Device()
- , m_rom(_rom)
+ Device::Device(ROMFile _rom, const float _preferredDeviceSamplerate, const float _hostSamplerate, const bool _createDebugger/* = false*/)
+ : m_rom(std::move(_rom))
+ , m_samplerate(getDeviceSamplerate(_preferredDeviceSamplerate, _hostSamplerate))
{
if(!m_rom.isValid())
throw synthLib::DeviceException(synthLib::DeviceError::FirmwareMissing, "Either a ROM file (.bin) or an OS update file (.mid) is required, but neither was found.");
DspSingle* dsp1;
- createDspInstances(dsp1, m_dsp2, m_rom);
+ createDspInstances(dsp1, m_dsp2, m_rom, m_samplerate);
m_dsp.reset(dsp1);
- m_dsp->getPeriphX().getEsai().setCallback([this](dsp56k::Audio*)
+ m_dsp->getAudio().setCallback([this](dsp56k::Audio*)
{
onAudioWritten();
}, 0);
- m_mc.reset(new Microcontroller(*m_dsp, _rom, false));
+ m_mc.reset(new Microcontroller(*m_dsp, m_rom, false));
if(m_dsp2)
m_mc->addDSP(*m_dsp2, true);
- auto loader = bootDSP(*m_dsp, m_rom, _createDebugger);
-
- if(m_dsp2)
- {
- auto loader2 = bootDSP(*m_dsp2, m_rom, false);
- loader2.join();
- }
-
- loader.join();
+ bootDSPs(m_dsp.get(), m_dsp2, m_rom, _createDebugger);
// m_dsp->getMemory().saveAssembly("P.asm", 0, m_dsp->getMemory().sizeP(), true, false, m_dsp->getDSP().getPeriph(0), m_dsp->getDSP().getPeriph(1));
- while(!m_mc->dspHasBooted())
- dummyProcess(8);
+ if(m_rom.getModel() == DeviceModel::A)
+ {
+ // The A does not send any event to notify that it has finished booting
+ dummyProcess(32);
+
+ m_dsp->disableESSI1();
+ }
+ else
+ {
+ while(!m_mc->dspHasBooted())
+ dummyProcess(8);
+ }
m_mc->sendInitControlCommands();
@@ -54,14 +60,38 @@ namespace virusLib
Device::~Device()
{
- m_dsp->getPeriphX().getEsai().setCallback(nullptr,0);
+ m_dsp->getAudio().setCallback(nullptr,0);
m_mc.reset();
m_dsp.reset();
}
+ std::vector<float> Device::getSupportedSamplerates() const
+ {
+ switch (m_rom.getModel())
+ {
+ default:
+ case DeviceModel::A:
+ case DeviceModel::B:
+ case DeviceModel::C:
+ return {12000000.0f / 256.0f};
+ case DeviceModel::Snow:
+ case DeviceModel::TI:
+ case DeviceModel::TI2:
+ return {44100.0f, 48000.0f};
+ }
+ }
+
float Device::getSamplerate() const
{
- return 12000000.0f / 256.0f;
+ return m_samplerate;
+ }
+
+ bool Device::setSamplerate(float _samplerate)
+ {
+ if(!synthLib::Device::setSamplerate(_samplerate))
+ return false;
+ m_samplerate = _samplerate;
+ return true;
}
bool Device::isValid() const
@@ -71,11 +101,24 @@ namespace virusLib
void Device::process(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _size, const std::vector<synthLib::SMidiEvent>& _midiIn, std::vector<synthLib::SMidiEvent>& _midiOut)
{
+ m_frontpanelStateDSP.clear();
+
synthLib::Device::process(_inputs, _outputs, _size, _midiIn, _midiOut);
+ m_frontpanelStateDSP.updateLfoPhaseFromTimer(m_dsp->getDSP(), 0, 2); // TIMER 1 = ACI = LFO 1 LED
+ m_frontpanelStateDSP.updateLfoPhaseFromTimer(m_dsp->getDSP(), 1, 1); // TIMER 2 = ADO = LFO 2/3 LED
+
m_numSamplesProcessed += static_cast<uint32_t>(_size);
+
+ m_frontpanelStateGui.m_lfoPhases = m_frontpanelStateDSP.m_lfoPhases;
+ m_frontpanelStateGui.m_bpm = m_frontpanelStateDSP.m_bpm;
+ m_frontpanelStateGui.m_logo = m_frontpanelStateDSP.m_logo;
+
+ for(size_t i=0; i<m_frontpanelStateDSP.m_midiEventReceived.size(); ++i)
+ m_frontpanelStateGui.m_midiEventReceived[i] |= m_frontpanelStateDSP.m_midiEventReceived[i];
}
+#if !SYNTHLIB_DEMO_MODE
bool Device::getState(std::vector<uint8_t>& _state, const synthLib::StateType _type)
{
return m_mc->getState(_state, _type);
@@ -93,13 +136,17 @@ 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)
{
+ if(_data.size() < _4cc.size())
+ return false;
+
for(uint32_t i=0; i<_data.size() - _4cc.size(); ++i)
{
bool valid = true;
- for(size_t j=0; j<4; ++j)
+ for(size_t j=0; j<_4cc.size(); ++j)
{
if(static_cast<char>(_data[i + j]) == _4cc[j])
continue;
@@ -265,14 +312,14 @@ namespace virusLib
return 6;
}
- void Device::createDspInstances(DspSingle*& _dspA, DspSingle*& _dspB, const ROMFile& _rom)
+ void Device::createDspInstances(DspSingle*& _dspA, DspSingle*& _dspB, const ROMFile& _rom, const float _samplerate)
{
- _dspA = new DspSingle(0x040000, false);
+ _dspA = new DspSingle(0x040000, false, nullptr, _rom.getModel() == DeviceModel::A);
- configureDSP(*_dspA, _rom);
+ configureDSP(*_dspA, _rom, _samplerate);
if(_dspB)
- configureDSP(*_dspB, _rom);
+ configureDSP(*_dspB, _rom, _samplerate);
}
bool Device::sendMidi(const synthLib::SMidiEvent& _ev, std::vector<synthLib::SMidiEvent>& _response)
@@ -282,7 +329,7 @@ namespace virusLib
// LOG("MIDI: " << std::hex << (int)_ev.a << " " << (int)_ev.b << " " << (int)_ev.c);
auto ev = _ev;
ev.offset += m_numSamplesProcessed + getExtraLatencySamples();
- return m_mc->sendMIDI(ev);
+ return m_mc->sendMIDI(ev, &m_frontpanelStateDSP);
}
std::vector<synthLib::SMidiEvent> responses;
@@ -333,10 +380,10 @@ 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)
+ void Device::configureDSP(DspSingle& _dsp, const ROMFile& _rom, const float _samplerate)
{
auto& jit = _dsp.getJIT();
auto conf = jit.getConfig();
@@ -350,8 +397,57 @@ namespace virusLib
std::thread Device::bootDSP(DspSingle& _dsp, const ROMFile& _rom, const bool _createDebugger)
{
- auto res = _rom.bootDSP(_dsp.getDSP(), _dsp.getPeriphX());
+ auto res = _rom.bootDSP(_dsp.getDSP(), _dsp.getHDI08());
_dsp.startDSPThread(_createDebugger);
return res;
}
+
+ void Device::bootDSPs(DspSingle* _dspA, DspSingle* _dspB, const ROMFile& _rom, bool _createDebugger)
+ {
+ auto loader = bootDSP(*_dspA, _rom, _createDebugger);
+
+ if(_dspB)
+ {
+ auto loader2 = bootDSP(*_dspB, _rom, false);
+ loader2.join();
+ }
+
+ loader.join();
+
+// applyDspMemoryPatches(_dspA, _dspB, _rom);
+ }
+
+ bool Device::setDspClockPercent(const uint32_t _percent)
+ {
+ if(!m_dsp)
+ return false;
+
+ bool res = m_dsp->getEsxiClock().setSpeedPercent(_percent);
+
+ if(m_dsp2)
+ res &= m_dsp2->getEsxiClock().setSpeedPercent(_percent);
+
+ return res;
+ }
+
+ uint32_t Device::getDspClockPercent() const
+ {
+ return !m_dsp ? 0 : m_dsp->getEsxiClock().getSpeedPercent();
+ }
+
+ uint64_t Device::getDspClockHz() const
+ {
+ return !m_dsp ? 0 : m_dsp->getEsxiClock().getSpeedInHz();
+ }
+
+ void Device::applyDspMemoryPatches(const DspSingle* _dspA, const DspSingle* _dspB, const ROMFile& _rom)
+ {
+ DspMemoryPatches::apply(_dspA, _rom.getHash());
+ DspMemoryPatches::apply(_dspB, _rom.getHash());
+ }
+
+ void Device::applyDspMemoryPatches() const
+ {
+ applyDspMemoryPatches(m_dsp.get(), m_dsp2, m_rom);
+ }
}
diff --git a/source/virusLib/device.h b/source/virusLib/device.h
@@ -1,30 +1,41 @@
#pragma once
#include "dspSingle.h"
+#include "frontpanelState.h"
#include "../synthLib/midiTypes.h"
#include "../synthLib/device.h"
#include "romfile.h"
#include "microcontroller.h"
+namespace synthLib
+{
+ struct DspMemoryPatch;
+ struct DspMemoryPatches;
+}
+
namespace virusLib
{
class Device final : public synthLib::Device
{
public:
- Device(const ROMFile& _rom, bool _createDebugger = false);
+ Device(ROMFile _rom, float _preferredDeviceSamplerate, float _hostSamplerate, bool _createDebugger = false);
~Device() override;
+ std::vector<float> getSupportedSamplerates() const override;
float getSamplerate() const override;
+ bool setSamplerate(float _samplerate) override;
+
bool isValid() const override;
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);
@@ -34,22 +45,35 @@ namespace virusLib
uint32_t getChannelCountIn() override;
uint32_t getChannelCountOut() override;
- static void createDspInstances(DspSingle*& _dspA, DspSingle*& _dspB, const ROMFile& _rom);
+ static void createDspInstances(DspSingle*& _dspA, DspSingle*& _dspB, const ROMFile& _rom, float _samplerate);
static std::thread bootDSP(DspSingle& _dsp, const ROMFile& _rom, bool _createDebugger);
+ static void bootDSPs(DspSingle* _dspA, DspSingle* _dspB, const ROMFile& _rom, bool _createDebugger);
+
+ bool setDspClockPercent(uint32_t _percent) override;
+ uint32_t getDspClockPercent() const override;
+ uint64_t getDspClockHz() const override;
+
+ static void applyDspMemoryPatches(const DspSingle* _dspA, const DspSingle* _dspB, const ROMFile& _rom);
+ void applyDspMemoryPatches() const;
+
+ FrontpanelState& getFrontpanelState() { return m_frontpanelStateGui; }
private:
bool sendMidi(const synthLib::SMidiEvent& _ev, std::vector<synthLib::SMidiEvent>& _response) override;
void readMidiOut(std::vector<synthLib::SMidiEvent>& _midiOut) override;
void processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _samples) override;
void onAudioWritten();
- static void configureDSP(DspSingle& _dsp, const ROMFile& _rom);
+ static void configureDSP(DspSingle& _dsp, const ROMFile& _rom, float _samplerate);
- const ROMFile& m_rom;
+ const ROMFile m_rom;
std::unique_ptr<DspSingle> m_dsp;
DspSingle* m_dsp2 = nullptr;
std::unique_ptr<Microcontroller> m_mc;
uint32_t m_numSamplesProcessed = 0;
+ float m_samplerate;
+ FrontpanelState m_frontpanelStateDSP;
+ FrontpanelState m_frontpanelStateGui;
};
}
diff --git a/source/virusLib/deviceModel.cpp b/source/virusLib/deviceModel.cpp
@@ -0,0 +1,20 @@
+#include "deviceModel.h"
+
+namespace virusLib
+{
+ std::string getModelName(const DeviceModel _model)
+ {
+ switch (_model)
+ {
+ default:
+ case DeviceModel::Invalid: return "<invalid>";
+ case DeviceModel::A: return "A";
+ case DeviceModel::B: return "B";
+ case DeviceModel::C: return "C";
+// case DeviceModel::ABC: return "A/B/C";
+ case DeviceModel::Snow: return "Snow";
+ case DeviceModel::TI: return "TI";
+ case DeviceModel::TI2: return "TI2";
+ }
+ }
+}
diff --git a/source/virusLib/deviceModel.h b/source/virusLib/deviceModel.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <string>
+
+namespace virusLib
+{
+ enum class DeviceModel
+ {
+ Invalid = -1,
+ A = 0,
+ B = 1,
+ C = 2,
+ ABC = C,
+ Snow = 3,
+ TI = 4,
+ TI2 = 5
+ };
+
+ std::string getModelName(DeviceModel _model);
+
+ constexpr bool isTIFamily(const DeviceModel _model)
+ {
+ return _model == DeviceModel::Snow || _model == DeviceModel::TI || _model == DeviceModel::TI2;
+ }
+
+ constexpr bool isABCFamily(const DeviceModel _model)
+ {
+ return _model == DeviceModel::A || _model == DeviceModel::B || _model == DeviceModel::C;
+ }
+}
diff --git a/source/virusLib/dspMemoryPatches.cpp b/source/virusLib/dspMemoryPatches.cpp
@@ -0,0 +1,19 @@
+#include "dspMemoryPatches.h"
+
+#include "dspSingle.h"
+
+namespace virusLib
+{
+ static const std::initializer_list<synthLib::DspMemoryPatches> g_patches =
+ {
+ };
+
+ void DspMemoryPatches::apply(const DspSingle* _dsp, const synthLib::MD5& _romChecksum)
+ {
+ if(!_dsp)
+ return;
+
+ for (auto element : g_patches)
+ element.apply(_dsp->getDSP(), _romChecksum);
+ }
+}
diff --git a/source/virusLib/dspMemoryPatches.h b/source/virusLib/dspMemoryPatches.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "../synthLib/dspMemoryPatch.h"
+
+namespace virusLib
+{
+ class DspSingle;
+
+ class DspMemoryPatches
+ {
+ public:
+ static void apply(const DspSingle* _dsp, const synthLib::MD5& _romChecksum);
+ };
+}
diff --git a/source/virusLib/dspSingle.cpp b/source/virusLib/dspSingle.cpp
@@ -10,7 +10,12 @@ namespace virusLib
{
constexpr dsp56k::TWord g_externalMemStart = 0x020000;
- DspSingle::DspSingle(uint32_t _memorySize, bool _use56367Peripherals/* = false*/, const char* _name/* = nullptr*/) : m_name(_name ? _name : std::string()), m_periphX(_use56367Peripherals ? &m_periphY : nullptr)
+ DspSingle::DspSingle(uint32_t _memorySize, bool _use56367Peripherals/* = false*/, const char* _name/* = nullptr*/, bool _use56303Peripherals/* = false*/)
+ : m_name(_name ? _name : std::string())
+ , m_periphX362(_use56367Peripherals ? &m_periphY367 : nullptr)
+ , m_hdi08(_use56303Peripherals ? m_periphX303.getHI08() : m_periphX362.getHDI08())
+ , m_audio(_use56303Peripherals ? static_cast<dsp56k::Audio&>(m_periphX303.getEssi0()) : static_cast<dsp56k::Audio&>(m_periphX362.getEsai()))
+ , m_esxiClock(_use56303Peripherals ? m_periphX303.getEssiClock() : m_periphX362.getEsaiClock())
{
const size_t requiredMemSize =
dsp56k::alignedSize<dsp56k::DSP>() +
@@ -19,7 +24,7 @@ namespace virusLib
m_buffer.resize(dsp56k::alignedSize(requiredMemSize));
- auto* buf = &m_buffer[0];
+ auto* buf = m_buffer.data();
buf = dsp56k::alignedAddress(buf);
auto* bufDSPClass = buf;
@@ -28,12 +33,22 @@ namespace virusLib
m_memory = new (bufMemClass)dsp56k::Memory(m_memoryValidator, _memorySize, _memorySize, g_externalMemStart, reinterpret_cast<dsp56k::TWord*>(bufMemSpace));
+ dsp56k::IPeripherals* periphX = &m_periphX362;
dsp56k::IPeripherals* periphY = &m_periphNop;
+ if(_use56303Peripherals)
+ {
+ periphX = &m_periphX303;
+ m_periphX303.getEssiClock().setExternalClockFrequency(4'000'000); // 4 Mhz
+ m_periphX303.getEssiClock().setSamplerate(12000000/256);
+ drainESSI1();
+ }
+
if (_use56367Peripherals)
- periphY = &m_periphY;
+ periphY = &m_periphY367;
+
+ m_dsp = new (buf)dsp56k::DSP(*m_memory, periphX, periphY);
- m_dsp = new (buf)dsp56k::DSP(*m_memory, &m_periphX, periphY);
m_jit = &m_dsp->getJit();
}
@@ -62,20 +77,57 @@ namespace virusLib
m_dspThread.reset(new dsp56k::DSPThread(*m_dsp, m_name.empty() ? nullptr : m_name.c_str(), debugger));
}
- template<typename T> void processAudio(DspSingle& _dsp, const synthLib::TAudioInputsT<T>& _inputs, const synthLib::TAudioOutputsT<T>& _outputs, const size_t _samples, uint32_t _latency)
+ template<typename T> void processAudio(DspSingle& _dsp, const synthLib::TAudioInputsT<T>& _inputs, const synthLib::TAudioOutputsT<T>& _outputs, const size_t _samples, uint32_t _latency, std::vector<T>& _dummyIn, std::vector<T>& _dummyOut)
+ {
+ DspSingle::ensureSize(_dummyIn, _samples<<1);
+ DspSingle::ensureSize(_dummyOut, _samples<<1);
+
+ const T* dIn = _dummyIn.data();
+ T* dOut = _dummyOut.data();
+
+ const T* inputs[] = {_inputs[0] ? _inputs[0] : dIn, _inputs[1] ? _inputs[1] : dIn, dIn, dIn, dIn, dIn, dIn, dIn};
+ T* outputs[] =
+ { _outputs[0] ? _outputs[0] : dOut
+ , _outputs[1] ? _outputs[1] : dOut
+ , _outputs[2] ? _outputs[2] : dOut
+ , _outputs[3] ? _outputs[3] : dOut
+ , _outputs[4] ? _outputs[4] : dOut
+ , _outputs[5] ? _outputs[5] : dOut
+ , dOut, dOut, dOut, dOut, dOut, dOut};
+
+ _dsp.getAudio().processAudioInterleaved(inputs, outputs, static_cast<uint32_t>(_samples), _latency);
+ }
+ void DspSingle::processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, const size_t _samples, const uint32_t _latency)
{
- const T* inputs[] = {_inputs[0], _inputs[1], nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
- T* outputs[] = {_outputs[0], _outputs[1], _outputs[2], _outputs[3], _outputs[4], _outputs[5], nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
+ virusLib::processAudio(*this, _inputs, _outputs, _samples, _latency, m_dummyBufferInF, m_dummyBufferOutF);
+ }
- _dsp.getPeriphX().getEsai().processAudioInterleaved(inputs, outputs, static_cast<uint32_t>(_samples), _latency);
+ void DspSingle::processAudio(const synthLib::TAudioInputsInt& _inputs, const synthLib::TAudioOutputsInt& _outputs, const size_t _samples, const uint32_t _latency)
+ {
+ virusLib::processAudio(*this, _inputs, _outputs, _samples, _latency, m_dummyBufferInI, m_dummyBufferOutI);
}
- void DspSingle::processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, const size_t _samples, uint32_t _latency)
+
+ void DspSingle::disableESSI1()
{
- virusLib::processAudio(*this, _inputs, _outputs, _samples, _latency);
+ // Model A uses ESSI1 to send some initialization data to something.
+ // Disable it once it has done doing that because otherwise it keeps running
+ // and causes a performance hit that we can prevent by disabling it
+ auto& essi = m_periphX303.getEssi1();
+ auto crb = essi.readCRB();
+ crb &= ~((1<<dsp56k::Essi::CRB_RE) | dsp56k::Essi::CRB_TE);
+ essi.writeCRB(crb);
}
- void DspSingle::processAudio(const synthLib::TAudioInputsInt& _inputs, const synthLib::TAudioOutputsInt& _outputs, const size_t _samples, uint32_t _latency)
+ void DspSingle::drainESSI1()
{
- virusLib::processAudio(*this, _inputs, _outputs, _samples, _latency);
+ auto& essi = m_periphX303.getEssi1();
+ auto& ins = essi.getAudioInputs();
+ auto& outs = essi.getAudioOutputs();
+
+ while(!ins.full())
+ ins.push_back({});
+
+ while(!outs.empty())
+ outs.pop_front();
}
}
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"
@@ -12,15 +13,19 @@ namespace dsp56k
namespace virusLib
{
+ struct FrontpanelState;
+
class DspSingle
{
public:
- DspSingle(uint32_t _memorySize, bool _use56367Peripherals = false, const char* _name = nullptr);
+ DspSingle(uint32_t _memorySize, bool _use56367Peripherals = false, const char* _name = nullptr, bool _use56303Peripherals = false);
virtual ~DspSingle();
- dsp56k::HDI08& getHDI08() { return m_periphX.getHDI08(); }
- dsp56k::Peripherals56362& getPeriphX() { return m_periphX; }
- dsp56k::Peripherals56367& getPeriphY() { return m_periphY; }
+ dsp56k::HDI08& getHDI08() { return m_hdi08; }
+ dsp56k::Audio& getAudio() { return m_audio; }
+ dsp56k::EsxiClock& getEsxiClock() { return m_esxiClock; }
+ dsp56k::Peripherals56362& getPeriphX() { return m_periphX362; }
+ dsp56k::Peripherals56367& getPeriphY() { return m_periphY367; }
dsp56k::PeripheralsNop& getPeriphNop() { return m_periphNop; }
dsp56k::DSP& getDSP() const { return *m_dsp; }
dsp56k::Jit& getJIT() const { return *m_jit; }
@@ -31,15 +36,37 @@ namespace virusLib
virtual void processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _samples, uint32_t _latency);
virtual void processAudio(const synthLib::TAudioInputsInt& _inputs, const synthLib::TAudioOutputsInt& _outputs, size_t _samples, uint32_t _latency);
+ void disableESSI1();
+ void drainESSI1();
+
+ template<typename T> static void ensureSize(std::vector<T>& _buf, size_t _size)
+ {
+ if(_buf.size() >= _size)
+ return;
+
+ _buf.resize(_size, static_cast<T>(0));
+ }
+
+ protected:
+ std::vector<uint32_t> m_dummyBufferInI;
+ std::vector<uint32_t> m_dummyBufferOutI;
+ std::vector<float> m_dummyBufferInF;
+ std::vector<float> m_dummyBufferOutF;
+
private:
const std::string m_name;
std::vector<uint8_t> m_buffer;
dsp56k::DefaultMemoryValidator m_memoryValidator;
- dsp56k::Peripherals56367 m_periphY;
- dsp56k::Peripherals56362 m_periphX;
+ dsp56k::Peripherals56367 m_periphY367;
+ dsp56k::Peripherals56362 m_periphX362;
+ dsp56k::Peripherals56303 m_periphX303;
dsp56k::PeripheralsNop m_periphNop;
+ dsp56k::HDI08& m_hdi08;
+ dsp56k::Audio& m_audio;
+ dsp56k::EsxiClock& m_esxiClock;
+
dsp56k::Memory* m_memory = nullptr;
dsp56k::DSP* m_dsp = nullptr;
dsp56k::Jit* m_jit = nullptr;
diff --git a/source/virusLib/frontpanelState.cpp b/source/virusLib/frontpanelState.cpp
@@ -0,0 +1,44 @@
+#include "frontpanelState.h"
+
+#include "dsp56kEmu/dsp.h"
+#include "dsp56kEmu/peripherals.h"
+
+namespace virusLib
+{
+ void FrontpanelState::updateLfoPhaseFromTimer(dsp56k::DSP& _dsp, const uint32_t _lfo, const uint32_t _timer, const float _minimumValue/* = 0.0f*/, float _maximumValue/* = 1.0f*/)
+ {
+ updatePhaseFromTimer(m_lfoPhases[_lfo], _dsp, _timer, _minimumValue, _maximumValue);
+ }
+
+ void FrontpanelState::updatePhaseFromTimer(float& _target, dsp56k::DSP& _dsp, uint32_t _timer, float _minimumValue, float _maximumValue)
+ {
+ const auto* peripherals = _dsp.getPeriph(0);
+
+ if(const auto* p362 = dynamic_cast<const dsp56k::Peripherals56362*>(peripherals))
+ {
+ const auto& t = p362->getTimers();
+ updatePhaseFromTimer(_target, t, _timer, _minimumValue, _maximumValue);
+ }
+ else if(const auto* p303 = dynamic_cast<const dsp56k::Peripherals56303*>(peripherals))
+ {
+ const auto& t = p303->getTimers();
+ updatePhaseFromTimer(_target, t, _timer, _minimumValue, _maximumValue);
+ }
+ }
+
+ void FrontpanelState::updatePhaseFromTimer(float& _target, const dsp56k::Timers& _timers, uint32_t _timer, float _minimumValue, float _maximumValue)
+ {
+ const auto compare = _timers.readTCPR(static_cast<int>(_timer));
+ const auto load = _timers.readTLR(static_cast<int>(_timer));
+
+ const auto range = 0xffffff - load;
+
+ const auto normalized = static_cast<float>(compare - load) / static_cast<float>(range);
+
+ // the minimum PWM value is not always zero, we need to remap
+ const auto floatRange = _maximumValue - _minimumValue;
+ const auto floatRangeInv = 1.0f / floatRange;
+
+ _target = (normalized - _minimumValue) * floatRangeInv;
+ }
+}
diff --git a/source/virusLib/frontpanelState.h b/source/virusLib/frontpanelState.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <array>
+#include <cstdint>
+
+namespace dsp56k
+{
+ class Timers;
+ class DSP;
+}
+
+namespace virusLib
+{
+ struct FrontpanelState
+ {
+ FrontpanelState()
+ {
+ clear();
+ }
+
+ void clear()
+ {
+ m_midiEventReceived.fill(false);
+ m_lfoPhases.fill(0.0f);
+ m_bpm = 0;
+ m_logo = 0;
+ }
+
+ void updateLfoPhaseFromTimer(dsp56k::DSP& _dsp, uint32_t _lfo, uint32_t _timer, float _minimumValue = 0.0f, float _maximumValue = 1.0f);
+ static void updatePhaseFromTimer(float& _target, dsp56k::DSP& _dsp, uint32_t _timer, float _minimumValue = 0.0f, float _maximumValue = 1.0f);
+ static void updatePhaseFromTimer(float& _target, const dsp56k::Timers& _timers, uint32_t _timer, float _minimumValue = 0.0f, float _maximumValue = 1.0f);
+
+ std::array<bool, 16> m_midiEventReceived;
+ std::array<float, 3> m_lfoPhases;
+
+ float m_logo = 0.0f;
+ float m_bpm = 0.0f;
+ };
+}
diff --git a/source/virusLib/hdi08MidiQueue.cpp b/source/virusLib/hdi08MidiQueue.cpp
@@ -7,7 +7,7 @@
namespace virusLib
{
- Hdi08MidiQueue::Hdi08MidiQueue(DspSingle& _dsp, Hdi08Queue& _output, const bool _useEsaiBasedTiming) : m_output(_output), m_esai(_dsp.getPeriphX().getEsai()), m_useEsaiBasedTiming(_useEsaiBasedTiming)
+ Hdi08MidiQueue::Hdi08MidiQueue(DspSingle& _dsp, Hdi08Queue& _output, const bool _useEsaiBasedTiming) : m_output(_output), m_esai(_dsp.getAudio()), m_useEsaiBasedTiming(_useEsaiBasedTiming)
{
if(_useEsaiBasedTiming)
{
@@ -71,6 +71,6 @@ namespace virusLib
void Hdi08MidiQueue::onAudioWritten()
{
++m_numSamplesWritten;
- sendPendingMidiEvents(m_numSamplesWritten >> 1);
+ sendPendingMidiEvents(m_numSamplesWritten);
}
}
diff --git a/source/virusLib/hdi08MidiQueue.h b/source/virusLib/hdi08MidiQueue.h
@@ -6,7 +6,7 @@
namespace dsp56k
{
- class Esai;
+ class Audio;
}
namespace synthLib
@@ -40,7 +40,7 @@ namespace virusLib
void sendMidiToDSP(uint8_t _a, uint8_t _b, uint8_t _c) const;
Hdi08Queue& m_output;
- dsp56k::Esai& m_esai;
+ dsp56k::Audio& m_esai;
bool m_useEsaiBasedTiming;
dsp56k::RingBuffer<synthLib::SMidiEvent, 1024, false> m_pendingMidiEvents;
diff --git a/source/virusLib/hdi08TxParser.cpp b/source/virusLib/hdi08TxParser.cpp
@@ -34,7 +34,7 @@ namespace virusLib
else if(_data == 0xf50000)
{
m_state = State::StatusReport;
- m_remainingStatusBytes = m_mc.getROM().getModel() == ROMFile::Model::ABC ? 1 : 2;
+ m_remainingStatusBytes = isABCFamily(m_mc.getROM().getModel()) ? 1 : 2;
}
else if(_data == 0xf400f4)
{
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
@@ -6,11 +6,15 @@
#include "microcontroller.h"
#include "dspSingle.h"
+#include "frontpanelState.h"
#include "../synthLib/midiTypes.h"
using namespace dsp56k;
using namespace synthLib;
+namespace virusLib
+{
+
constexpr virusLib::PlayMode g_defaultPlayMode = virusLib::PlayModeSingle;
constexpr uint32_t g_sysexPresetHeaderSize = 9;
@@ -24,6 +28,7 @@ constexpr uint8_t g_pageA[] = {0x05, 0x0A, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0
0x3E, 0x3F, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5D, 0x5E, 0x61,
0x62, 0x63, 0x64, 0x65, 0x66, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x7B};
+
constexpr uint8_t g_pageB[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x11,
0x12, 0x13, 0x15, 0x19, 0x1A, 0x1B, 0x1C, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24,
0x26, 0x27, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x36, 0x37,
@@ -32,13 +37,9 @@ constexpr uint8_t g_pageB[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0
0x56, 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, 0x63,
0x64, 0x65, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x7B, 0x7C};
-constexpr uint8_t
- g_pageC_global[] = {45, 63, 64, 65, 66, 67, 68, 69, 70, 85, 86, 87, 90, 91,
- 92, 93, 94, 95, 96, 97, 98, 99, 105, 106, 110, 111, 112, 113,
- 114, 115, 116, 117, 118, 120, 121, 122, 123, 124, 125, 126, 127};
-
-namespace virusLib
-{
+constexpr uint8_t g_pageC_global[] = {45, 63, 64, 65, 66, 67, 68, 69, 70, 85, 86, 87, 90, 91,
+ 92, 93, 94, 95, 96, 97, 98, 99, 105, 106, 110, 111, 112, 113,
+ 114, 115, 116, 117, 118, 120, 121, 122, 123, 124, 125, 126, 127};
Microcontroller::Microcontroller(DspSingle& _dsp, const ROMFile& _romFile, bool _useEsaiBasedMidiTiming) : m_rom(_romFile)
{
@@ -75,7 +76,7 @@ Microcontroller::Microcontroller(DspSingle& _dsp, const ROMFile& _romFile, bool
if(ROMFile::getSingleName(single).size() != 10)
{
failed = true;
- break;
+ break;
}
singles.emplace_back(single);
@@ -110,7 +111,7 @@ void Microcontroller::sendInitControlCommands()
sendControlCommand(MIDI_CLOCK_RX, 0x1); // Enable MIDI clock receive
sendControlCommand(GLOBAL_CHANNEL, 0x0); // Set global midi channel to 0
sendControlCommand(MIDI_CONTROL_LOW_PAGE, 0x1); // Enable midi CC to edit parameters on page A
- sendControlCommand(MIDI_CONTROL_HIGH_PAGE, 0x1); // Enable poly pressure to edit parameters on page B
+ sendControlCommand(MIDI_CONTROL_HIGH_PAGE, 0x0); // Disable poly pressure to edit parameters on page B
sendControlCommand(MASTER_VOLUME, 127); // Set master volume to maximum
sendControlCommand(MASTER_TUNE, 64); // Set master tune to 0
sendControlCommand(DEVICE_ID, OMNI_DEVICE_ID); // Set device ID to Omni
@@ -215,6 +216,22 @@ void Microcontroller::sendControlCommand(const ControlCommand _command, const ui
send(globalSettingsPage(), 0x0, _command, _value);
}
+
+uint32_t Microcontroller::getPartCount() const
+{
+ return 16;
+}
+
+uint8_t Microcontroller::getPartMidiChannel(const uint8_t _part) const
+{
+ return m_multiEditBuffer[MD_PART_MIDI_CHANNEL + _part];
+}
+
+bool Microcontroller::isPolyPressureForPageBEnabled() const
+{
+ return m_globalSettings[MIDI_CONTROL_HIGH_PAGE] == 1;
+}
+
bool Microcontroller::send(const Page _page, const uint8_t _part, const uint8_t _param, const uint8_t _value)
{
std::lock_guard lock(m_mutex);
@@ -235,7 +252,7 @@ bool Microcontroller::send(const Page _page, const uint8_t _part, const uint8_t
return true;
}
-bool Microcontroller::sendMIDI(const SMidiEvent& _ev)
+bool Microcontroller::sendMIDI(const SMidiEvent& _ev, FrontpanelState* _fpState/* = nullptr*/)
{
const uint8_t channel = _ev.a & 0x0f;
const uint8_t status = _ev.a & 0xf0;
@@ -268,7 +285,8 @@ bool Microcontroller::sendMIDI(const SMidiEvent& _ev)
}
break;
case M_POLYPRESSURE:
- applyToSingleEditBuffer(PAGE_B, singleMode ? SINGLE : channel, _ev.b, _ev.c);
+ if(isPolyPressureForPageBEnabled())
+ applyToSingleEditBuffer(PAGE_B, singleMode ? SINGLE : channel, _ev.b, _ev.c);
break;
default:
break;
@@ -277,6 +295,15 @@ bool Microcontroller::sendMIDI(const SMidiEvent& _ev)
for (auto& midiQueue : m_midiQueues)
midiQueue.add(_ev);
+ if(status < 0xf0 && _fpState)
+ {
+ for(uint32_t p=0; p<getPartCount(); ++p)
+ {
+ if(channel == getPartMidiChannel(static_cast<uint8_t>(p)))
+ _fpState->m_midiEventReceived[p] = true;
+ }
+ }
+
return true;
}
@@ -294,13 +321,13 @@ bool Microcontroller::sendSysex(const std::vector<uint8_t>& _data, std::vector<S
if (deviceId != m_globalSettings[DEVICE_ID] && deviceId != OMNI_DEVICE_ID && m_globalSettings[DEVICE_ID] != OMNI_DEVICE_ID)
{
- // ignore messages intended for a different device, allow omni requests
+ // ignore messages intended for a different device but allow omni requests
return true;
}
- auto buildResponseHeader = [&](SMidiEvent& ev)
+ auto buildResponseHeader = [&](SMidiEvent& _ev)
{
- auto& response = ev.sysex;
+ auto& response = _ev.sysex;
response.reserve(1024);
@@ -453,36 +480,21 @@ bool Microcontroller::sendSysex(const std::vector<uint8_t>& _data, std::vector<S
buildSingleResponse(BankNumber::EditBuffer, SINGLE);
};
- auto buildControllerDumpResponse = [&](uint8_t _part)
+ auto buildControllerDumpResponse = [&](const uint8_t _part)
{
- TPreset _dump, _multi;
- const auto res = requestSingle(BankNumber::EditBuffer, _part, _dump);
- const auto resm = requestMulti(BankNumber::EditBuffer, 0, _multi);
- const uint8_t channel = _part == SINGLE ? static_cast<uint8_t>(m_globalSettings[GLOBAL_CHANNEL]) : _multi[static_cast<size_t>(MD_PART_MIDI_CHANNEL) + _part];
- for (const auto cc : g_pageA)
- {
- SMidiEvent ev;
- ev.source = _source;
- ev.a = M_CONTROLCHANGE + channel;
- ev.b = cc;
- ev.c = _dump[cc];
- _responses.emplace_back(std::move(ev));
- }
- for (const auto cc : g_pageB)
- {
- SMidiEvent ev;
- ev.source = _source;
- ev.a = M_POLYPRESSURE + channel;
- ev.b = cc;
- ev.c = _dump[static_cast<size_t>(cc)+128];
- _responses.emplace_back(std::move(ev));
- }
-
+ TPreset single;
+
+ requestSingle(BankNumber::EditBuffer, _part, single);
+
+ const uint8_t channel = _part == SINGLE ? 0 : _part;
+
+ for (const auto cc : g_pageA) _responses.emplace_back(M_CONTROLCHANGE + channel, cc, single[cc], 0, _source);
+ for (const auto cc : g_pageB) _responses.emplace_back(M_POLYPRESSURE, cc, single[cc + 128], 0, _source);
};
- auto enqueue = [&]()
+ auto enqueue = [&]
{
- m_pendingSysexInput.emplace_back(std::make_pair(_source, _data));
+ m_pendingSysexInput.emplace_back(_source, _data);
return false;
};
@@ -868,7 +880,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 +895,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 +973,7 @@ bool Microcontroller::setState(const std::vector<synthLib::SMidiEvent>& _events)
return true;
}
+#endif
void Microcontroller::addDSP(DspSingle& _dsp, bool _useEsaiBasedMidiTiming)
{
@@ -1158,6 +1172,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>
@@ -15,6 +16,7 @@
namespace virusLib
{
+ struct FrontpanelState;
class DspSingle;
class DemoPlayback;
@@ -26,7 +28,7 @@ public:
explicit Microcontroller(DspSingle& _dsp, const ROMFile& romFile, bool _useEsaiBasedMidiTiming);
- bool sendMIDI(const synthLib::SMidiEvent& _ev);
+ bool sendMIDI(const synthLib::SMidiEvent& _ev, FrontpanelState* _fpState = nullptr);
bool sendSysex(const std::vector<uint8_t>& _data, std::vector<synthLib::SMidiEvent>& _responses, synthLib::MidiEventSource _source);
bool writeSingle(BankNumber _bank, uint8_t _program, const TPreset& _data);
@@ -37,11 +39,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);
@@ -58,6 +62,11 @@ public:
const ROMFile& getROM() const { return m_rom; }
+ uint32_t getPartCount() const;
+
+ uint8_t getPartMidiChannel(uint8_t _part) const;
+ bool isPolyPressureForPageBEnabled() const;
+
private:
bool send(Page page, uint8_t part, uint8_t param, uint8_t value);
void sendControlCommand(ControlCommand command, uint8_t value);
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, bool _isMidiFileData/* = false*/)
+ {
+ std::vector<std::vector<uint8_t>> packets;
+
+ synthLib::MidiToSysex::splitMultipleSysex(packets, _fileData, _isMidiFileData);
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 _isMidiFileData = false);
bool add(const std::vector<Packet>& _packets);
bool add(const Packet& _packet);
diff --git a/source/virusLib/romfile.cpp b/source/virusLib/romfile.cpp
@@ -1,4 +1,3 @@
-#include <cassert>
#include <fstream>
#include <algorithm>
@@ -10,118 +9,29 @@
#include "../synthLib/os.h"
-#include "midiFileToRomData.h"
-
#include <cstring> // memcpy
-#ifdef _WIN32
-#define NOMINMAX
-#include <Windows.h>
-#endif
+#include "dsp56kEmu/memory.h"
namespace virusLib
{
-ROMFile::ROMFile(const std::string& _path, const Model _model/* = Model::ABC*/)
-{
- if(!_path.empty())
- {
- m_romFileName = _path;
-
- // Open file
- LOG("Loading ROM at " << _path);
-
- if(!synthLib::readFile(m_romFileData, _path))
- {
- LOG("Failed to load ROM at '" << _path << "'");
- #ifdef _WIN32
- const auto errorMessage = std::string("Failed to load ROM file. Make sure it is put next to the plugin and ends with .bin");
- ::MessageBoxA(nullptr, errorMessage.c_str(), "ROM not found", MB_OK);
- #endif
- return;
- }
- }
- else
- {
- const auto expectedSize = _model == Model::ABC ? getRomSizeModelABC() : 0;
-
- if(!loadROMData(m_romFileName, m_romFileData, expectedSize, expectedSize))
- return;
- }
- if(initialize())
- printf("ROM File: %s\n", m_romFileName.c_str());
-}
-
-ROMFile::ROMFile(std::vector<uint8_t> _data) : m_romFileData(std::move(_data))
+ROMFile::ROMFile(std::vector<uint8_t> _data, std::string _name, const DeviceModel _model/* = DeviceModel::ABC*/) : m_model(_model), m_romFileName(std::move(_name)), m_romFileData(std::move(_data))
{
- initialize();
+ if(initialize())
+ return;
+ m_romFileData.clear();
+ bootRom.size = 0;
}
-std::string ROMFile::findROM()
+ROMFile ROMFile::invalid()
{
- return synthLib::findROM(getRomSizeModelABC());
-}
-
-bool ROMFile::loadROMData(std::string& _loadedFile, std::vector<uint8_t>& _loadedData, const size_t _expectedSizeMin, const size_t _expectedSizeMax)
-{
- // try binary roms first
- const auto file = synthLib::findROM(_expectedSizeMin, _expectedSizeMax);
-
- if(!file.empty())
- {
- if(synthLib::readFile(_loadedData, file) && !_loadedData.empty())
- {
- _loadedFile = file;
- return true;
- }
- }
-
- // if that didn't work, load an OS update as rom
- auto loadMidiAsRom = [&](const std::string& _path)
- {
- _loadedFile.clear();
- _loadedData.clear();
-
- std::vector<std::string> files;
-
- synthLib::findFiles(files, _path, ".mid", 512 * 1024, 600 * 1024);
-
- bool gotSector0 = false;
- bool gotSector8 = false;
-
- for (const auto& f : files)
- {
- MidiFileToRomData loader;
- if(!loader.load(f) || loader.getData().size() != getRomSizeModelABC() / 2)
- continue;
- if(loader.getFirstSector() == 0)
- {
- if(gotSector0)
- continue;
- gotSector0 = true;
- _loadedFile = f;
- _loadedData.insert(_loadedData.begin(), loader.getData().begin(), loader.getData().end());
- }
- else if(loader.getFirstSector() == 8)
- {
- if(gotSector8)
- continue;
- gotSector8 = true;
- _loadedData.insert(_loadedData.end(), loader.getData().begin(), loader.getData().end());
- }
- }
- return gotSector0 && _loadedData.size() >= getRomSizeModelABC() / 2;
- };
-
- if(loadMidiAsRom(synthLib::getModulePath()))
- return true;
-
- return loadMidiAsRom(synthLib::getModulePath(false));
+ return ROMFile({}, {}, DeviceModel::Invalid);
}
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);
@@ -165,11 +75,11 @@ std::vector<ROMFile::Chunk> ROMFile::readChunks(std::istream& _file)
if (fileSize == getRomSizeModelABC() || fileSize == getRomSizeModelABC()/2) // the latter is a ROM without presets
{
// ABC
- m_model = Model::ABC;
+ m_model = DeviceModel::C;
}
else
{
- LOG("Invalid ROM, unexpected filesize")
+ LOG("Invalid ROM, unexpected filesize");
return {};
}
@@ -189,9 +99,13 @@ std::vector<ROMFile::Chunk> ROMFile::readChunks(std::istream& _file)
_file.read(reinterpret_cast<char*>(&chunk.size2), 1);
if(i == 0 && chunk.chunk_id == 3 && lastChunkId == 4) // Virus A has one chunk less
+ {
+ m_model = DeviceModel::A;
lastChunkId = 3;
+ }
- assert(chunk.chunk_id == lastChunkId - i);
+ if(chunk.chunk_id != lastChunkId - i)
+ return {};
// Format uses a special kind of size where the first byte should be decreased by 1
const uint16_t len = ((chunk.size1 - 1) << 8) | chunk.size2;
@@ -211,18 +125,22 @@ std::vector<ROMFile::Chunk> ROMFile::readChunks(std::istream& _file)
return chunks;
}
-std::thread ROMFile::bootDSP(dsp56k::DSP& dsp, dsp56k::Peripherals56362& periph) const
+std::thread ROMFile::bootDSP(dsp56k::DSP& dsp, dsp56k::HDI08& _hdi08) const
{
// 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);
// Attach command stream
std::thread feedCommandStream([&]()
{
- periph.getHDI08().writeRX(m_commandStream);
+ _hdi08.writeRX(m_commandStream);
});
// Initialize the DSP
@@ -230,6 +148,11 @@ std::thread ROMFile::bootDSP(dsp56k::DSP& dsp, dsp56k::Peripherals56362& periph)
return feedCommandStream;
}
+std::string ROMFile::getModelName() const
+{
+ return virusLib::getModelName(getModel());
+}
+
bool ROMFile::getSingle(const int _bank, const int _presetNumber, TPreset& _out) const
{
const uint32_t offset = 0x50000 + (_bank * 0x8000) + (_presetNumber * getSinglePresetSize());
diff --git a/source/virusLib/romfile.h b/source/virusLib/romfile.h
@@ -6,9 +6,13 @@
#include "dsp56kEmu/types.h"
+#include "../synthLib/md5.h"
+
+#include "deviceModel.h"
+
namespace dsp56k
{
- class Peripherals56362;
+ class HDI08;
class DSP;
}
@@ -32,18 +36,11 @@ public:
std::vector<uint32_t> data;
};
- enum class Model
- {
- Invalid = -1,
- ABC,
- Snow,
- TI
- };
-
using TPreset = std::array<uint8_t, 512>;
- explicit ROMFile(const std::string& _path, Model _model = Model::ABC);
- explicit ROMFile(std::vector<uint8_t> _data);
+ explicit ROMFile(std::vector<uint8_t> _data, std::string _name, DeviceModel _model = DeviceModel::ABC);
+
+ static ROMFile invalid();
bool getMulti(int _presetNumber, TPreset& _out) const;
bool getSingle(int _bank, int _presetNumber, TPreset& _out) const;
@@ -53,11 +50,15 @@ public:
static std::string getMultiName(const TPreset& _preset);
static std::string getPresetName(const TPreset& _preset, uint32_t _first, uint32_t _last);
- std::thread bootDSP(dsp56k::DSP& dsp, dsp56k::Peripherals56362& periph) const;
+ std::thread bootDSP(dsp56k::DSP& dsp, dsp56k::HDI08& _hdi08) const;
bool isValid() const { return bootRom.size > 0; }
- Model getModel() const { return m_model; }
+ DeviceModel getModel() const { return m_model; }
+
+ std::string getModelName() const;
+
+ bool isTIFamily() const { return virusLib::isTIFamily(m_model); }
uint32_t getSamplerate() const
{
@@ -89,14 +90,12 @@ public:
return 128;
}
- static std::string findROM();
-
- static bool loadROMData(std::string& _loadedFile, std::vector<uint8_t>& _loadedData, size_t _expectedSizeMin, size_t _expectedSizeMax);
-
const std::vector<uint8_t>& getDemoData() const { return m_demoData; }
std::string getFilename() const { return isValid() ? m_romFileName : std::string(); }
+ const auto& getHash() const { return m_romDataHash; }
+
private:
bool initialize();
std::vector<Chunk> readChunks(std::istream& _file);
@@ -104,7 +103,7 @@ private:
BootRom bootRom;
std::vector<uint32_t> m_commandStream;
- Model m_model = Model::Invalid;
+ DeviceModel m_model = DeviceModel::Invalid;
std::vector<TPreset> m_singles;
std::vector<TPreset> m_multis;
@@ -112,6 +111,7 @@ private:
std::string m_romFileName;
std::vector<uint8_t> m_romFileData;
+ synthLib::MD5 m_romDataHash;
};
}
diff --git a/source/virusLib/romloader.cpp b/source/virusLib/romloader.cpp
@@ -0,0 +1,218 @@
+#include "romloader.h"
+
+#include "midiFileToRomData.h"
+
+#include "../synthLib/os.h"
+
+namespace virusLib
+{
+ static constexpr uint32_t g_midiSizeMinABC = 512 * 1024;
+ static constexpr uint32_t g_midiSizeMaxABC = 600 * 1024;
+
+ static constexpr uint32_t g_binSizeTImin = 6 * 1024 * 1024;
+ static constexpr uint32_t g_binSizeTImax = 9 * 1024 * 1024;
+
+ std::vector<ROMFile> ROMLoader::findROMs(const DeviceModel _model/* = DeviceModel::ABC*/)
+ {
+ return findROMs(std::string(), _model);
+ }
+
+ std::vector<ROMFile> ROMLoader::findROMs(const DeviceModel _modelA, const DeviceModel _modelB)
+ {
+ auto roms = findROMs(_modelA);
+ const auto romsB = findROMs(_modelB);
+ if(!romsB.empty())
+ roms.insert(roms.end(), romsB.begin(), romsB.end());
+
+ return roms;
+ }
+
+ std::vector<ROMFile> ROMLoader::findROMs(const std::string& _path, const DeviceModel _model /*= DeviceModel::ABC*/)
+ {
+ std::vector<std::string> files;
+
+ if(isABCFamily(_model))
+ {
+ files = findFiles(_path, ".bin", ROMFile::getRomSizeModelABC(), ROMFile::getRomSizeModelABC());
+
+ const auto midiFileNames = findFiles(_path, ".mid", g_midiSizeMinABC, g_midiSizeMaxABC);
+
+ files.insert(files.end(), midiFileNames.begin(), midiFileNames.end());
+ }
+ else
+ {
+ files = findFiles(_path, ".bin", g_binSizeTImin, g_binSizeTImax);
+ }
+
+ return initializeRoms(files, _model);
+ }
+
+ ROMFile ROMLoader::findROM(const DeviceModel _model/* = DeviceModel::ABC*/)
+ {
+ std::vector<ROMFile> results = findROMs(_model);
+ return results.empty() ? ROMFile::invalid() : results.front();
+ }
+
+ ROMFile ROMLoader::findROM(const std::string& _filename, const DeviceModel _model/* = DeviceModel::ABC*/)
+ {
+ if(_filename.empty())
+ return findROM(_model);
+
+ const auto path = synthLib::getPath(_filename);
+
+ const std::vector<ROMFile> results = findROMs(path, _model);
+
+ if(results.empty())
+ return ROMFile::invalid();
+
+ const auto requestedName = synthLib::lowercase(_filename);
+
+ for (const auto& result : results)
+ {
+ const auto name = synthLib::lowercase(result.getFilename());
+ if(name == requestedName)
+ return result;
+ if(synthLib::getFilenameWithoutPath(name) == requestedName)
+ return result;
+ }
+ return ROMFile::invalid();
+ }
+
+ std::vector<std::string> ROMLoader::findFiles(const std::string& _extension, const size_t _minSize, const size_t _maxSize)
+ {
+ std::vector<std::string> results;
+
+ const auto path = synthLib::getModulePath();
+ synthLib::findFiles(results, path, _extension, _minSize, _maxSize);
+
+ const auto path2 = synthLib::getModulePath(false);
+ if(path2 != path)
+ synthLib::findFiles(results, path2, _extension, _minSize, _maxSize);
+
+ if(results.empty())
+ {
+ const auto path3 = synthLib::getCurrentDirectory();
+ if(path3 != path2 && path3 != path)
+ synthLib::findFiles(results, path, _extension, _minSize, _maxSize);
+ }
+
+ return results;
+ }
+
+ std::vector<std::string> ROMLoader::findFiles(const std::string& _path, const std::string& _extension, size_t _minSize, size_t _maxSize)
+ {
+ if(_path.empty())
+ return findFiles(_extension, _minSize, _maxSize);
+
+ std::vector<std::string> results;
+ synthLib::findFiles(results, _path, _extension, _minSize, _maxSize);
+ return results;
+ }
+
+ ROMLoader::FileData ROMLoader::loadFile(const std::string& _name)
+ {
+ FileData data;
+ data.filename = _name;
+
+ if(!synthLib::readFile(data.data, _name))
+ return {};
+
+ if(synthLib::hasExtension(_name, ".bin"))
+ {
+ data.type = BinaryRom;
+ return data;
+ }
+
+ if(!synthLib::hasExtension(_name, ".mid"))
+ return {};
+
+ MidiFileToRomData midiLoader;
+ if(!midiLoader.load(data.data, true))
+ return {};
+
+ data.data = midiLoader.getData();
+
+ if(data.data.size() != (ROMFile::getRomSizeModelABC()>>1))
+ return {};
+
+ if(midiLoader.getFirstSector() == 0)
+ data.type = MidiRom;
+ else if(midiLoader.getFirstSector() == 8)
+ data.type = MidiPresets;
+ else
+ return {};
+ return data;
+ }
+
+ std::vector<ROMFile> ROMLoader::initializeRoms(const std::vector<std::string>& _files, const DeviceModel _model)
+ {
+ if(_files.empty())
+ return {};
+
+ std::vector<FileData> fileDatas;
+ fileDatas.reserve(_files.size());
+
+ for (const auto& file : _files)
+ {
+ auto data = loadFile(file);
+ if(!data.isValid())
+ continue;
+
+ fileDatas.emplace_back(std::move(data));
+ }
+
+ if(fileDatas.empty())
+ return {};
+
+ std::vector<ROMFile> roms;
+ roms.reserve(fileDatas.size());
+
+ std::vector<const FileData*> presets;
+
+ for (const auto& fd : fileDatas)
+ {
+ if(fd.type == MidiPresets)
+ presets.push_back(&fd);
+ }
+
+ for (const auto& fd : fileDatas)
+ {
+ if(fd.type == MidiPresets)
+ continue;
+
+ if(fd.type == BinaryRom)
+ {
+ // load as-is
+ auto& rom = roms.emplace_back(fd.data, fd.filename, _model);
+ if(!rom.isValid())
+ roms.pop_back();
+ }
+ else if(fd.type == MidiRom)
+ {
+ // try to combine with presets
+ if(presets.empty())
+ {
+ // none available, use without presets
+ auto& rom = roms.emplace_back(fd.data, fd.filename, _model);
+ if(!rom.isValid())
+ roms.pop_back();
+ }
+ else
+ {
+ // build data with presets included and create rom
+ auto& p = *presets.front();
+ auto data = fd.data;
+ data.insert(data.end(), p.data.begin(), p.data.end());
+
+ auto& rom = roms.emplace_back(data, fd.filename, _model);
+ if(!rom.isValid())
+ roms.pop_back();
+ else if(presets.size() > 1)
+ presets.erase(presets.begin()); // do not use preset file more than once if we have multiple
+ }
+ }
+ }
+
+ return roms;
+ }
+}
diff --git a/source/virusLib/romloader.h b/source/virusLib/romloader.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include "romfile.h"
+
+namespace virusLib
+{
+ class ROMLoader
+ {
+ public:
+ enum FileType
+ {
+ Invalid,
+ BinaryRom,
+ MidiRom,
+ MidiPresets
+ };
+
+ struct FileData
+ {
+ std::string filename;
+ FileType type;
+ std::vector<uint8_t> data;
+
+ bool isValid() const { return type != Invalid && !data.empty(); }
+ };
+
+ static std::vector<ROMFile> findROMs(DeviceModel _model = DeviceModel::ABC);
+ static std::vector<ROMFile> findROMs(DeviceModel _modelA, DeviceModel _modelB);
+ static std::vector<ROMFile> findROMs(const std::string& _path, DeviceModel _model = DeviceModel::ABC);
+ static ROMFile findROM(DeviceModel _model = DeviceModel::ABC);
+ static ROMFile findROM(const std::string& _filename, DeviceModel _model = DeviceModel::ABC);
+
+ private:
+ static std::vector<std::string> findFiles(const std::string& _extension, size_t _minSize, size_t _maxSize);
+ static std::vector<std::string> findFiles(const std::string& _path, const std::string& _extension, size_t _minSize, size_t _maxSize);
+
+ static FileData loadFile(const std::string& _name);
+
+ static std::vector<ROMFile> initializeRoms(const std::vector<std::string>& _files, DeviceModel _model);
+ };
+}
diff --git a/source/virusTestConsole/CMakeLists.txt b/source/virusTestConsole/CMakeLists.txt
@@ -24,3 +24,4 @@ if(MSVC)
else()
install(DIRECTORY ${CMAKE_SOURCE_DIR}/deploy/linux/ DESTINATION . COMPONENT OsirusTestConsole)
endif()
+set_property(TARGET virusTestConsole PROPERTY FOLDER "Virus")
diff --git a/source/virusTestConsole/virusTestConsole.cpp b/source/virusTestConsole/virusTestConsole.cpp
@@ -7,6 +7,9 @@
#include "../synthLib/os.h"
+constexpr bool g_createDebugger = false;
+constexpr bool g_dumpAssembly = false;
+
using namespace dsp56k;
using namespace virusLib;
using namespace synthLib;
@@ -76,7 +79,7 @@ int main(int _argc, char* _argv[])
const std::string audioFilename = app->getSingleNameAsFilename();
- app->run(audioFilename);
+ app->run(audioFilename, 0, g_createDebugger, g_dumpAssembly);
std::cout << "Program ended. Press key to exit." << std::endl;
ConsoleApp::waitReturn();
diff --git a/source/wLib/CMakeLists.txt b/source/wLib/CMakeLists.txt
@@ -0,0 +1,25 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(wLib)
+
+add_library(wLib STATIC)
+
+set(SOURCES
+ am29f.cpp am29f.h
+ dspBootCode.h
+ lcd.cpp lcd.h
+ lcdfonts.cpp lcdfonts.h
+ wDevice.cpp wDevice.h
+ wDsp.cpp wDsp.h
+ wHardware.cpp wHardware.h
+ wMidi.cpp wMidi.h
+ wMidiTypes.h
+ wPlugin.cpp wPlugin.h
+ wRom.cpp wRom.h
+ wState.cpp wState.h
+)
+
+target_sources(wLib PRIVATE ${SOURCES})
+source_group("source" FILES ${SOURCES})
+
+target_link_libraries(wLib PUBLIC synthLib 68kEmu)
diff --git a/source/wLib/am29f.cpp b/source/wLib/am29f.cpp
@@ -0,0 +1,139 @@
+#include "am29f.h"
+
+#include <cassert>
+
+#include "../mc68k/logging.h"
+#include "../mc68k/mc68k.h"
+
+namespace wLib
+{
+ Am29f::Am29f(uint8_t* _buffer, const size_t _size, bool _useWriteEnable, bool _bitreversedCmdAddr): m_buffer(_buffer), m_size(_size), m_useWriteEnable(_useWriteEnable), m_bitreverseCmdAddr(_bitreversedCmdAddr)
+ {
+ auto br = [&](uint16_t x)
+ {
+ return m_bitreverseCmdAddr ? static_cast<uint16_t>(bitreverse(x) >> 4) : x;
+ };
+
+ // Chip Erase
+ m_commands.push_back({{{br(0x555),0xAA}, {br(0x2AA),0x55}, {br(0x555),0x80}, {br(0x555),0xAA}, {br(0x2AA),0x55}, {br(0x555),0x10}}});
+
+ // Sector Erase
+ m_commands.push_back({{{br(0x555),0xAA}, {br(0x2AA),0x55}, {br(0x555),0x80}, {br(0x555),0xAA}, {br(0x2AA),0x55}}});
+
+ // Program
+ m_commands.push_back({{{br(0x555),0xAA}, {br(0x2AA),0x55}, {br(0x555),0xA0}}});
+ }
+
+ void Am29f::write(const uint32_t _addr, const uint16_t _data)
+ {
+ const auto reset = [this]()
+ {
+ m_currentBusCycle = 0;
+ m_currentCommand = -1;
+ };
+
+ if(!writeEnabled())
+ {
+ reset();
+ return;
+ }
+
+ bool anyMatch = false;
+
+ const auto d = _data & 0xff;
+
+ for (size_t i=0; i<m_commands.size(); ++i)
+ {
+ auto& cycles = m_commands[i].cycles;
+
+ if(m_currentBusCycle < cycles.size())
+ {
+ const auto& c = cycles[m_currentBusCycle];
+
+ if(c.addr == _addr && c.data == d)
+ {
+ anyMatch = true;
+
+ if(m_currentBusCycle == cycles.size() - 1)
+ m_currentCommand = static_cast<int32_t>(i);
+ }
+ }
+ }
+
+ if(!anyMatch)
+ {
+ if(m_currentCommand >= 0)
+ {
+ const auto c = static_cast<CommandType>(m_currentCommand);
+
+ execCommand(c, _addr, _data);
+ }
+
+ reset();
+ }
+ else
+ {
+ ++m_currentBusCycle;
+ }
+ }
+
+ void Am29f::execCommand(const CommandType _command, uint32_t _addr, const uint16_t _data)
+ {
+ switch (_command)
+ {
+ case CommandType::ChipErase:
+ assert(false);
+ break;
+ case CommandType::SectorErase:
+ {
+ size_t sectorSizekB = 0;
+ switch (_addr)
+ {
+ case 0x00000: sectorSizekB = 16; break;
+ case 0x04000:
+ case 0x06000: sectorSizekB = 8; break;
+ case 0x08000: sectorSizekB = 32; break;
+ case 0x10000:
+ case 0x20000:
+ case 0x30000:
+ case 0x40000:
+ case 0x50000:
+ case 0x60000:
+ case 0x70000: sectorSizekB = 64; break;
+ case 0x78000:
+ case 0x7A000:
+ case 0x7C000:
+ // mq sends erase commands for a flash with top boot block even though a chip with bottom boot block is installed
+ _addr = 0x70000;
+ sectorSizekB = 64;
+ break;
+ default:
+ MCLOG("Unable to erase sector at " << MCHEX(_addr) << ", out of bounds!");
+ return;
+ }
+
+ MCLOG("Erasing Sector at " << MCHEX(_addr) << ", size " << MCHEX(1024 * sectorSizekB));
+ for(size_t i = _addr; i< _addr + sectorSizekB * 1024; ++i)
+ m_buffer[i] = 0xff;
+ }
+ break;
+ case CommandType::Program:
+ {
+ if(_addr >= m_size)
+ return;
+ MCLOG("Programming word at " << MCHEX(_addr) << ", value " << MCHEXN(_data, 4));
+ const auto old = mc68k::Mc68k::readW(m_buffer, _addr);
+ // "A bit cannot be programmed from a 0 back to a 1"
+ const auto v = _data & old;
+ mc68k::Mc68k::writeW(m_buffer, _addr, v);
+ // assert(v == _data);
+ break;
+ }
+ case CommandType::Invalid:
+ default:
+ assert(false);
+ break;
+ }
+ }
+
+}
+\ No newline at end of file
diff --git a/source/wLib/am29f.h b/source/wLib/am29f.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <cstdint>
+#include <cstddef>
+#include <vector>
+
+namespace wLib
+{
+ class Am29f
+ {
+ public:
+ struct BusCycle
+ {
+ uint16_t addr;
+ uint8_t data;
+ };
+
+ struct Command
+ {
+ std::vector<BusCycle> cycles;
+ };
+
+ enum class CommandType
+ {
+ Invalid = -1,
+ ChipErase,
+ SectorErase,
+ Program,
+ };
+
+ explicit Am29f(uint8_t* _buffer, size_t _size, bool _useWriteEnable, bool _bitreversedCmdAddr);
+
+ void writeEnable(bool _writeEnable)
+ {
+ m_writeEnable = _writeEnable;
+ }
+
+ void write(uint32_t _addr, uint16_t _data);
+
+ private:
+ bool writeEnabled() const
+ {
+ return !m_useWriteEnable || m_writeEnable;
+ }
+
+ static constexpr uint16_t bitreverse(uint16_t x)
+ {
+ x = ((x & 0xaaaau) >> 1) | static_cast<uint16_t>((x & 0x5555u) << 1);
+ x = ((x & 0xccccu) >> 2) | static_cast<uint16_t>((x & 0x3333u) << 2);
+ x = ((x & 0xf0f0u) >> 4) | static_cast<uint16_t>((x & 0x0f0fu) << 4);
+
+ return ((x & 0xff00) >> 8) | static_cast<uint16_t>((x & 0x00ff) << 8);
+ }
+
+ void execCommand(CommandType _command, uint32_t _addr, uint16_t _data);
+
+ uint8_t* m_buffer;
+ const size_t m_size;
+ const bool m_useWriteEnable;
+ const bool m_bitreverseCmdAddr;
+
+ std::vector<Command> m_commands;
+ bool m_writeEnable = false;
+ uint32_t m_currentBusCycle = 0;
+ int32_t m_currentCommand = -1;
+ };
+}
+\ No newline at end of file
diff --git a/source/wLib/dspBootCode.h b/source/wLib/dspBootCode.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include <cstdint>
+
+namespace wLib
+{
+ static constexpr uint32_t g_dspBootCode[] =
+ {
+ 0x350013, 0x0afa23, 0xff0035, 0x0afa22,
+ 0xff000e, 0x0afa01, 0xff0022, 0x0afa20,
+ 0xff005e, 0x61f400, 0xff1000, 0x050c8f,
+ 0x0afa00, 0xff0021, 0x31a900, 0x0afa01,
+ 0xff0012, 0x0ad161, 0x04d191, 0x019191,
+ 0xff0013, 0x044894, 0x019191, 0xff0016,
+ 0x045094, 0x221100, 0x06c800, 0xff001f,
+ 0x019191, 0xff001c, 0x009814, 0x000000,
+ 0x050c5a, 0x050c5d, 0x62f400, 0xd00000,
+ 0x08f4b8, 0xd00409, 0x060680, 0xff0029,
+ 0x07da8a, 0x0c1c10, 0x219000, 0x219100,
+ 0x06c800, 0xff0033, 0x060380, 0xff0031,
+ 0x07da8a, 0x0c1c10, 0x07588c, 0x000000,
+ 0x050c46, 0x0afa02, 0xff005c, 0x0afa01,
+ 0xff003e, 0x0afa00, 0xff0046, 0x08f484,
+ 0x000038, 0x050c0b, 0x0afa20, 0xff0043,
+ 0x08f484, 0x005018, 0x050c06, 0x08f484,
+ 0x000218, 0x050c03, 0x08f484, 0x001c1e,
+ 0x0a8426, 0x0a8380, 0xff0049, 0x084806,
+ 0x0a8380, 0xff004c, 0x085006, 0x221100,
+ 0x06c800, 0xff0059, 0x0a83a0, 0xff0058,
+ 0x0a8383, 0xff0052, 0x00008c, 0x050c03,
+ 0x085846, 0x000000, 0x0000b9, 0x0ae180,
+ 0x0afa01, 0xff005f, 0x050c00, 0x66f41b,
+ 0xff0090, 0x0503a6, 0x04cfdd, 0x013f03,
+ 0x013e23, 0x045517, 0x060980, 0xff008b,
+ 0x07de85, 0x07de84, 0x07de86, 0x300013,
+ 0x70f400, 0x001600, 0x06d820, 0x4258a2,
+ 0x320013, 0x72f400, 0x000c00, 0x06da20,
+ 0x075a86, 0x300013, 0x06d800, 0xff007d,
+ 0x54e000, 0x200063, 0x200018, 0x5cd800,
+ 0x200043, 0x200018, 0x320013, 0x06da00,
+ 0xff0083, 0x07da8c, 0x200053, 0x200018,
+ 0x022d07, 0x08d73c, 0x0d104a, 0x000005,
+ 0x013d03, 0x00008c, 0x050c02, 0x017d03,
+ 0x000200, 0x000086
+ };
+
+ static constexpr uint32_t g_dspBootCode56303[] =
+ {
+ 0x240a13, 0x0afa02, 0xff0028, 0x0afa01,
+ 0xff0009, 0x0afa00, 0xff0011, 0x0af080,
+ 0xff0014, 0x0afa20, 0xff000e, 0x08f484,
+ 0x005018, 0x050c09, 0x08f484, 0x000218,
+ 0x050c06, 0x08f484, 0x001c1e, 0x050c03,
+ 0x08f484, 0x000038, 0x0a8426, 0x0a8380,
+ 0xff0017, 0x084806, 0x0a8380, 0xff001a,
+ 0x085006, 0x221100, 0x06c800, 0xff0026,
+ 0x0a83a0, 0xff0026, 0x0a8383, 0xff0020,
+ 0x00008c, 0x050c02, 0x085846, 0x050c52,
+ 0x0afa01, 0xff0048, 0x07f41c, 0x000302,
+ 0x07f41b, 0x00c000, 0x07f41f, 0x000007,
+ 0x060680, 0xff0038, 0x019382, 0xff0032,
+ 0x044a98, 0x019381, 0xff0035, 0x04ca95,
+ 0x0c1c10, 0x219000, 0x219100, 0x06c800,
+ 0xff0046, 0x060380, 0xff0045, 0x019382,
+ 0xff003f, 0x044a98, 0x019381, 0xff0042,
+ 0x04ca95, 0x0c1c10, 0x07588c, 0x050c12,
+ 0x62f400, 0xd00000, 0x08f4b8, 0xd00409,
+ 0x060680, 0xff004f, 0x07da8a, 0x0c1c10,
+ 0x219000, 0x219100, 0x06c800, 0xff0058,
+ 0x060380, 0xff0057, 0x07da8a, 0x0c1c10,
+ 0x060380, 0xff0057, 0x07da8a, 0x0c1c10,
+ 0x07588c, 0x0000b9, 0x0ae180
+ };
+}
diff --git a/source/wLib/lcd.cpp b/source/wLib/lcd.cpp
@@ -0,0 +1,219 @@
+#include "lcd.h"
+
+#include <cassert>
+
+#include "../mc68k/port.h"
+#include "../mc68k/logging.h"
+
+#define LOG MCLOG
+
+namespace wLib
+{
+ LCD::LCD() = default;
+
+ std::optional<uint8_t> LCD::exec(bool registerSelect, bool read, uint8_t g)
+ {
+ bool changed = false;
+ bool cgRamChanged = false;
+
+ std::optional<uint8_t> result;
+
+ if(!read)
+ {
+ if(!registerSelect)
+ {
+ if(g == 0x01)
+ {
+ LOG("LCD Clear Display");
+ m_dramData.fill(' ');
+ changed = true;
+ }
+ else if(g == 0x02)
+ {
+ LOG("LCD Return Home");
+ m_dramAddr = 0;
+ m_cursorPos = 0;
+ }
+ else if((g & 0xfc) == 0x04)
+ {
+ const int increment = (g >> 1) & 1;
+ const int shift = g & 1;
+ LOG("LCD Entry Mode Set, inc=" << increment << ", shift=" << shift);
+
+ m_addrIncrement = increment ? 1 : -1;
+ }
+ else if((g & 0xf8) == 0x08)
+ {
+ const int displayOnOff = (g >> 2) & 1;
+ const int cursorOnOff = (g >> 1) & 1;
+ const int cursorBlinking = g & 1;
+
+ LOG("LCD Display ON/OFF, display=" << displayOnOff << ", cursor=" << cursorOnOff << ", blinking=" << cursorBlinking);
+
+ m_displayOn = displayOnOff != 0;
+ m_cursorOn = cursorOnOff != 0;
+ m_cursorBlinking = cursorBlinking != 0;
+ }
+ else if((g & 0xf3) == 0x10)
+ {
+ const int scrl = (g >> 2) & 3;
+
+ LOG("LCD Cursor/Display Shift, scrl=" << scrl);
+ m_cursorShift = static_cast<CursorShiftMode>(scrl);
+ }
+ else if((g & 0xec) == 0x28)
+ {
+ const int dl = (g >> 4) & 1;
+ const int ft = g & 3;
+
+ LOG("LCD Function Set, dl=" << dl << ", ft=" << ft);
+ m_dataLength = static_cast<DataLength>(dl);
+ m_fontTable = static_cast<FontTable>(ft);
+ }
+ else if(g & (1<<7))
+ {
+ const int addr = g & 0x7f;
+// LOG("LCD Set DDRAM address, addr=" << addr);
+ m_dramAddr = addr;
+ m_addressMode = AddressMode::DDRam;
+ }
+ else if(g & (1<<6))
+ {
+ const int acg = g & 0x3f;
+
+// LOG("LCD Set CGRAM address, acg=" << acg);
+ m_cgramAddr = acg;
+ m_addressMode = AddressMode::CGRam;
+ }
+ else
+ {
+ LOG("LCD unknown command");
+ assert(false);
+ }
+ }
+ else
+ {
+ if(m_addressMode == AddressMode::CGRam)
+ {
+// changed = true;
+// LOG("LCD write data to CGRAM addr " << m_cgramAddr << ", data=" << static_cast<int>(g));
+
+ if (m_cgramData[m_cgramAddr] != g)
+ {
+ m_cgramData[m_cgramAddr] = g;
+ cgRamChanged = true;
+ }
+
+ m_cgramAddr += m_addrIncrement;
+
+ /*
+ if((m_cgramAddr & 0x7) == 0)
+ {
+ std::stringstream ss;
+ ss << "CG RAM character " << (m_cgramAddr/8 - 1) << ':' << '\n';
+ ss << "##################" << '\n';
+ for(auto i = m_cgramAddr - 8; i < m_cgramAddr - 1; ++i)
+ {
+ ss << '#';
+ for(int x=7; x >= 0; --x)
+ {
+ if(m_cgramData[i] & (1<<x))
+ ss << "[]";
+ else
+ ss << " ";
+ }
+ ss << '#' << '\n';
+ }
+ ss << "##################" << '\n';
+ const auto s(ss.str());
+ LOG(s);
+ }
+ */
+ }
+ else
+ {
+// LOG("LCD write data to DDRAM addr " << m_dramAddr << ", data=" << static_cast<int>(g) << ", char=" << static_cast<char>(g));
+
+ const auto old = m_dramData;
+
+ if(m_dramAddr >= 20 && m_dramAddr < 0x40)
+ {
+ for(size_t i=1; i<=20; ++i)
+ m_dramData[i-1] = m_dramData[i];
+ m_dramData[19] = static_cast<char>(g);
+ }
+ else if(m_dramAddr > 0x53)
+ {
+ for(size_t i=21; i<=40; ++i)
+ m_dramData[i-1] = m_dramData[i];
+
+ m_dramData[39] = static_cast<char>(g);
+ }
+ else
+ {
+ if(m_dramAddr < 20)
+ m_dramData[m_dramAddr] = static_cast<char>(g);
+ else
+ m_dramData[m_dramAddr - 0x40 + 20] = static_cast<char>(g);
+ }
+
+ if(m_dramAddr != 20 && m_dramAddr != 0x54)
+ m_dramAddr += m_addrIncrement;
+
+ if(m_dramData != old)
+ changed = true;
+ }
+ }
+ }
+ else
+ {
+ if(registerSelect)
+ {
+ LOG("LCD read data from CGRAM or DDRAM");
+ if(m_addressMode == AddressMode::CGRam)
+ result = m_cgramData[m_cgramAddr];
+ else
+ result = m_dramData[m_dramAddr];
+ }
+ else
+ {
+ LOG("LCD read busy flag & address");
+ if(m_addressMode == AddressMode::CGRam)
+ {
+ result = static_cast<uint8_t>(m_cgramAddr);
+ }
+ else
+ {
+ auto a = m_dramAddr;
+ if(a > 0x53)
+ a = 0x53;
+ if(a == 20)
+ a = 19;
+ result = static_cast<uint8_t>(m_dramAddr);
+ }
+ }
+ }
+
+ if(changed && m_changeCallback)
+ m_changeCallback();
+
+ if(cgRamChanged && m_cgRamChangeCallback)
+ m_cgRamChangeCallback();
+
+ return result;
+ }
+
+ bool LCD::getCgData(std::array<uint8_t, 8>& _data, uint32_t _charIndex) const
+ {
+ const auto idx = _charIndex * 8;
+ if(idx + 8 >= getCgRam().size())
+ return false;
+
+ uint32_t j = 0;
+
+ for(auto i = idx; i<idx+8; ++i)
+ _data[j++] = getCgRam()[i];
+
+ return true;
+ }
+}
diff --git a/source/wLib/lcd.h b/source/wLib/lcd.h
@@ -0,0 +1,88 @@
+#pragma once
+
+#include <array>
+#include <cstdint>
+#include <functional>
+#include <optional>
+
+namespace wLib
+{
+ class LCD
+ {
+ public:
+ using ChangeCallback = std::function<void()>;
+
+ LCD();
+ std::optional<uint8_t> exec(bool registerSelect, bool read, uint8_t g);
+
+ const std::array<char, 40>& getDdRam() const { return m_dramData; }
+ const auto& getCgRam() const { return m_cgramData; }
+ bool getCgData(std::array<uint8_t, 8>& _data, uint32_t _charIndex) const;
+
+ void setChangeCallback(const ChangeCallback& _callback)
+ {
+ m_changeCallback = _callback;
+ }
+
+ void setCgRamChangeCallback(const ChangeCallback& _callback)
+ {
+ m_cgRamChangeCallback = _callback;
+ }
+ private:
+ enum class CursorShiftMode
+ {
+ CursorLeft,
+ CursorRight,
+ DisplayLeft,
+ DisplayRight
+ };
+ enum class DisplayShiftMode
+ {
+ Right,
+ Left
+ };
+ enum class FontTable
+ {
+ EnglishJapanese,
+ WesternEuropean1,
+ EnglishRussian,
+ WesternEuropean2,
+ };
+ enum class DataLength
+ {
+ Bit8,
+ Bit4
+ };
+
+ enum class AddressMode
+ {
+ DDRam,
+ CGRam,
+ };
+
+ uint32_t m_lastWriteCounter = 0xffffffff;
+
+ uint32_t m_cursorPos = 0;
+ uint32_t m_dramAddr = 0;
+ uint32_t m_cgramAddr = 0;
+
+ CursorShiftMode m_cursorShift = CursorShiftMode::CursorLeft;
+ DisplayShiftMode m_displayShift = DisplayShiftMode::Left;
+ FontTable m_fontTable = FontTable::EnglishJapanese;
+ DataLength m_dataLength = DataLength::Bit8;
+ AddressMode m_addressMode = AddressMode::DDRam;
+
+ bool m_displayOn = true;
+ bool m_cursorOn = false;
+ bool m_cursorBlinking = false;
+
+ int m_addrIncrement = 1;
+
+ std::array<uint8_t, 0x40> m_cgramData{};
+ std::array<char, 40> m_dramData{};
+ uint32_t m_lastOpState = 0;
+
+ ChangeCallback m_changeCallback;
+ ChangeCallback m_cgRamChangeCallback;
+ };
+}
diff --git a/source/wLib/lcdfonts.cpp b/source/wLib/lcdfonts.cpp
@@ -0,0 +1,2675 @@
+#include <cstdint>
+#include <cstddef>
+
+#include <array>
+
+namespace wLib
+{
+ constexpr uint8_t g_fontTable0[] =
+ {
+ // CGRam Data, initially empty
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 0
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 1
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 2
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 3
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 4
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 5
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 6
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 7
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 8
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 9
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 a
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 b
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 c
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 d
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 e
+ 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, // 0 f
+
+ 0b00000, // 1 0
+ 0b10001,
+ 0b01001,
+ 0b01110,
+ 0b10010,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 1
+ 0b11110,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 2
+ 0b00110,
+ 0b00010,
+ 0b00110,
+ 0b01010,
+ 0b01010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 3
+ 0b11111,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 4
+ 0b11111,
+ 0b00001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 5
+ 0b00110,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 6
+ 0b01110,
+ 0b00100,
+ 0b01000,
+ 0b00100,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 7
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 8
+ 0b10111,
+ 0b10101,
+ 0b10101,
+ 0b10001,
+ 0b01111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 9
+ 0b00110,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 a
+ 0b01111,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 b
+ 0b11110,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10000, // 1 c
+ 0b11111,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 d
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 e
+ 0b10110,
+ 0b01001,
+ 0b10001,
+ 0b10001,
+ 0b10111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 1 f
+ 0b00110,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 2 0
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 2 1 !
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01010, // 2 2 "
+ 0b01010,
+ 0b01010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01010, // 2 3 #
+ 0b01010,
+ 0b11111,
+ 0b01010,
+ 0b11111,
+ 0b01010,
+ 0b01010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 2 4 $
+ 0b01111,
+ 0b10100,
+ 0b01110,
+ 0b00101,
+ 0b11110,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11000, // 2 5 %
+ 0b11001,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b10011,
+ 0b00011,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01100, // 2 6 &
+ 0b10010,
+ 0b10100,
+ 0b01000,
+ 0b10101,
+ 0b10010,
+ 0b01101,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01100, // 2 7 '
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // 2 8 (
+ 0b00100,
+ 0b01000,
+ 0b01000,
+ 0b01000,
+ 0b00100,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // 2 9 )
+ 0b00100,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 2 a *
+ 0b00100,
+ 0b10101,
+ 0b01110,
+ 0b10101,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 2 b +
+ 0b00100,
+ 0b00100,
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 2 c ,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b01100,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 2 d -
+ 0b00000,
+ 0b00000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 2 e .
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b01100,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 2 f /
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 3 0 0
+ 0b10001,
+ 0b10011,
+ 0b10101,
+ 0b11001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 3 1 1
+ 0b01100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 3 2 2
+ 0b10001,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // 3 3 3
+ 0b00010,
+ 0b00100,
+ 0b00010,
+ 0b00001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // 3 4 4
+ 0b00110,
+ 0b01010,
+ 0b10010,
+ 0b11111,
+ 0b00010,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // 3 5 5
+ 0b10000,
+ 0b11110,
+ 0b00001,
+ 0b00001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00110, // 3 6 6
+ 0b01000,
+ 0b10000,
+ 0b11110,
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // 3 7 7
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b01000,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 3 8 8
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 3 9 9
+ 0b10001,
+ 0b10001,
+ 0b01111,
+ 0b00001,
+ 0b00010,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 3 a :
+ 0b01100,
+ 0b01100,
+ 0b00000,
+ 0b01100,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 3 b ;
+ 0b01100,
+ 0b01100,
+ 0b00000,
+ 0b01100,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // 3 c <
+ 0b00100,
+ 0b01000,
+ 0b10000,
+ 0b01000,
+ 0b00100,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 3 d =
+ 0b00000,
+ 0b11111,
+ 0b00000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // 3 e >
+ 0b00100,
+ 0b00010,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 3 f ?
+ 0b10001,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b00000,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 4 0 @
+ 0b10001,
+ 0b00001,
+ 0b01101,
+ 0b10101,
+ 0b10101,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 4 1 A
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11110, // 4 2 B
+ 0b10001,
+ 0b10001,
+ 0b11110,
+ 0b10001,
+ 0b10001,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 4 3 C
+ 0b10001,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11100, // 4 4 D
+ 0b10010,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10010,
+ 0b11100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // 4 5 E
+ 0b10000,
+ 0b10000,
+ 0b11110,
+ 0b10000,
+ 0b10000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // 4 6 F
+ 0b10000,
+ 0b10000,
+ 0b11110,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 4 7 G
+ 0b10001,
+ 0b10000,
+ 0b10111,
+ 0b10001,
+ 0b10001,
+ 0b01111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 4 8 H
+ 0b10001,
+ 0b10001,
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 4 9 I
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00111, // 4 a J
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b10010,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 4 b K
+ 0b10010,
+ 0b10100,
+ 0b11000,
+ 0b10100,
+ 0b10010,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10000, // 4 c L
+ 0b10000,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 4 d M
+ 0b11011,
+ 0b10101,
+ 0b10101,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 4 e N
+ 0b10001,
+ 0b11001,
+ 0b10101,
+ 0b10011,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 4 f O
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11110, // 5 0 P
+ 0b10001,
+ 0b10001,
+ 0b11110,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 5 1 Q
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10101,
+ 0b10010,
+ 0b01101,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11110, // 5 2 R
+ 0b10001,
+ 0b10001,
+ 0b11110,
+ 0b10100,
+ 0b10010,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01111, // 5 3 S
+ 0b10000,
+ 0b10000,
+ 0b01110,
+ 0b00001,
+ 0b00001,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // 5 4 T
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 5 5 U
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 5 6 V
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10101, // 5 7 W
+ 0b10101,
+ 0b10101,
+ 0b10101,
+ 0b10101,
+ 0b10101,
+ 0b01010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 5 8 X
+ 0b10001,
+ 0b01010,
+ 0b00100,
+ 0b01010,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 5 9 Y
+ 0b10001,
+ 0b10001,
+ 0b01010,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // 5 a Z
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b10000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 5 b [
+ 0b01000,
+ 0b01000,
+ 0b01000,
+ 0b01000,
+ 0b01000,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10001, // 5 c
+ 0b01010,
+ 0b11111,
+ 0b00100,
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // 5 d ]
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 5 e ^
+ 0b01010,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 5 f _
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // 6 0 `
+ 0b00100,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 6 1 a
+ 0b00000,
+ 0b01110,
+ 0b00001,
+ 0b01111,
+ 0b10001,
+ 0b01111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10000, // 6 2 b
+ 0b10000,
+ 0b10110,
+ 0b11001,
+ 0b10001,
+ 0b10001,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 6 3 c
+ 0b00000,
+ 0b01110,
+ 0b10000,
+ 0b10000,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00001, // 6 4 d
+ 0b00001,
+ 0b01101,
+ 0b10011,
+ 0b10001,
+ 0b10001,
+ 0b01111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 6 5 e
+ 0b00000,
+ 0b01110,
+ 0b10001,
+ 0b11111,
+ 0b10000,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00110, // 6 6 f
+ 0b01001,
+ 0b01000,
+ 0b11100,
+ 0b01000,
+ 0b01000,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 6 7 g
+ 0b01111,
+ 0b10001,
+ 0b10001,
+ 0b01111,
+ 0b00001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10000, // 6 8 h
+ 0b10000,
+ 0b10110,
+ 0b11001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 6 9 i
+ 0b00000,
+ 0b01100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // 6 a j
+ 0b00000,
+ 0b00110,
+ 0b00010,
+ 0b00010,
+ 0b10010,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10000, // 6 b k
+ 0b10000,
+ 0b10010,
+ 0b10100,
+ 0b11000,
+ 0b10100,
+ 0b10010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01100, // 6 c l
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 6 d m
+ 0b00000,
+ 0b11010,
+ 0b10101,
+ 0b10101,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 6 e n
+ 0b00000,
+ 0b10110,
+ 0b11001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 6 f o
+ 0b00000,
+ 0b01110,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 0 p
+ 0b00000,
+ 0b11110,
+ 0b10001,
+ 0b11110,
+ 0b10000,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 1 q
+ 0b00000,
+ 0b01101,
+ 0b10011,
+ 0b01111,
+ 0b00001,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 2 r
+ 0b00000,
+ 0b10110,
+ 0b11001,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 3 s
+ 0b00000,
+ 0b01110,
+ 0b10000,
+ 0b01110,
+ 0b00001,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // 7 4 t
+ 0b01000,
+ 0b11100,
+ 0b01000,
+ 0b01000,
+ 0b01001,
+ 0b00110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 5 u
+ 0b00000,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10011,
+ 0b01101,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 6 v
+ 0b00000,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 7 w
+ 0b00000,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10101,
+ 0b01010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 8 x
+ 0b00000,
+ 0b10001,
+ 0b01010,
+ 0b00100,
+ 0b01010,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 9 y
+ 0b00000,
+ 0b10001,
+ 0b10001,
+ 0b01111,
+ 0b00001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 a z
+ 0b00000,
+ 0b11111,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // 7 b {
+ 0b00100,
+ 0b00100,
+ 0b01000,
+ 0b00100,
+ 0b00100,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 7 c |
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // 7 d }
+ 0b00100,
+ 0b00100,
+ 0b00010,
+ 0b00100,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 e ->
+ 0b00100,
+ 0b00010,
+ 0b11111,
+ 0b00010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 7 f <-
+ 0b00100,
+ 0b01000,
+ 0b11111,
+ 0b01000,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 0
+ 0b00110,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 1
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 2
+ 0b10001,
+ 0b10001,
+ 0b01001,
+ 0b00101,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 3
+ 0b01111,
+ 0b01001,
+ 0b01101,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 4
+ 0b11111,
+ 0b01001,
+ 0b01101,
+ 0b00001,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 5
+ 0b01001,
+ 0b00110,
+ 0b00010,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 6
+ 0b10001,
+ 0b01010,
+ 0b00100,
+ 0b00010,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 7
+ 0b11111,
+ 0b00001,
+ 0b10001,
+ 0b10110,
+ 0b10000,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 8
+ 0b11111,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 9
+ 0b10101,
+ 0b10101,
+ 0b10101,
+ 0b10101,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 a
+ 0b01111,
+ 0b01001,
+ 0b01001,
+ 0b01001,
+ 0b11001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 8 b
+ 0b01001,
+ 0b11101,
+ 0b00001,
+ 0b10001,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 c
+ 0b00010,
+ 0b00000,
+ 0b00010,
+ 0b00101,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 d
+ 0b00000,
+ 0b00000,
+ 0b00010,
+ 0b00101,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // 8 e
+ 0b00100,
+ 0b01110,
+ 0b00000,
+ 0b00010,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 8 f
+ 0b00100,
+ 0b00000,
+ 0b00110,
+ 0b01000,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 9 0
+ 0b00101,
+ 0b11100,
+ 0b00000,
+ 0b00100,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 1
+ 0b00000,
+ 0b00100,
+ 0b01010,
+ 0b00001,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 2
+ 0b00010,
+ 0b00000,
+ 0b10011,
+ 0b10011,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 3
+ 0b00000,
+ 0b00000,
+ 0b00111,
+ 0b00101,
+ 0b11011,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 4
+ 0b01100,
+ 0b00010,
+ 0b01001,
+ 0b10101,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 9 5
+ 0b00000,
+ 0b01111,
+ 0b01001,
+ 0b00110,
+ 0b11001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 9 6
+ 0b01001,
+ 0b11101,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 7
+ 0b00101,
+ 0b00000,
+ 0b00010,
+ 0b00101,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 8
+ 0b00000,
+ 0b00000,
+ 0b01100,
+ 0b01010,
+ 0b01101,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 9
+ 0b01010,
+ 0b00000,
+ 0b01100,
+ 0b01010,
+ 0b01101,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 a
+ 0b00000,
+ 0b01111,
+ 0b01001,
+ 0b00110,
+ 0b11001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // 9 b
+ 0b00000,
+ 0b00100,
+ 0b01010,
+ 0b00001,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 c
+ 0b00101,
+ 0b00000,
+ 0b10011,
+ 0b10011,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 d
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // 9 e
+ 0b00000,
+ 0b00000,
+ 0b00100,
+ 0b01010,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00110, // 9 f
+ 0b11101,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 0
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 1
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b11100,
+ 0b10100,
+ 0b11100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00111, // a 2
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 3
+ 0b00000,
+ 0b00000,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b11100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 4
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b10000,
+ 0b01000,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 5
+ 0b00000,
+ 0b00000,
+ 0b01100,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 6
+ 0b11111,
+ 0b00001,
+ 0b11111,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 7
+ 0b00000,
+ 0b11111,
+ 0b00001,
+ 0b00110,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 8
+ 0b00000,
+ 0b00010,
+ 0b00100,
+ 0b01100,
+ 0b10100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a 9
+ 0b00000,
+ 0b00100,
+ 0b11111,
+ 0b10001,
+ 0b00001,
+ 0b00110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a a
+ 0b00000,
+ 0b00000,
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a b
+ 0b00000,
+ 0b00010,
+ 0b11111,
+ 0b00110,
+ 0b01010,
+ 0b10010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a c
+ 0b00000,
+ 0b01000,
+ 0b11111,
+ 0b01001,
+ 0b01010,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a d
+ 0b00000,
+ 0b00000,
+ 0b01110,
+ 0b00010,
+ 0b00010,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a e
+ 0b00000,
+ 0b00000,
+ 0b11110,
+ 0b00010,
+ 0b11110,
+ 0b00010,
+ 0b11110,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // a f
+ 0b00000,
+ 0b00000,
+ 0b10101,
+ 0b10101,
+ 0b00001,
+ 0b00110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // b 0
+ 0b00000,
+ 0b00000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // b 1
+ 0b00001,
+ 0b00101,
+ 0b00110,
+ 0b00100,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00001, // b 2
+ 0b00010,
+ 0b00100,
+ 0b01100,
+ 0b10100,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // b 3
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // b 4
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // b 5
+ 0b11111,
+ 0b00010,
+ 0b00110,
+ 0b01010,
+ 0b10010,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // b 6
+ 0b11111,
+ 0b01001,
+ 0b01001,
+ 0b01001,
+ 0b01001,
+ 0b10010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // b 7
+ 0b11111,
+ 0b00100,
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // b 8
+ 0b01111,
+ 0b01001,
+ 0b10001,
+ 0b00001,
+ 0b00010,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // b 9
+ 0b01111,
+ 0b10010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // b a
+ 0b11111,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01010, // b b
+ 0b11111,
+ 0b01010,
+ 0b01010,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // b c
+ 0b11000,
+ 0b00001,
+ 0b11001,
+ 0b00001,
+ 0b00010,
+ 0b11100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // b d
+ 0b11111,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b01010,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // b e
+ 0b11111,
+ 0b01001,
+ 0b01010,
+ 0b01000,
+ 0b01000,
+ 0b00111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // b f
+ 0b10001,
+ 0b10001,
+ 0b01001,
+ 0b00001,
+ 0b00010,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // c 0
+ 0b01111,
+ 0b01001,
+ 0b10101,
+ 0b00011,
+ 0b00010,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // c 1
+ 0b11100,
+ 0b00100,
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // c 2
+ 0b10101,
+ 0b10101,
+ 0b10101,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // c 3
+ 0b00000,
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // c 4
+ 0b01000,
+ 0b01000,
+ 0b01100,
+ 0b01010,
+ 0b01000,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // c 5
+ 0b00100,
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b01000,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // c 6
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // c 7
+ 0b11111,
+ 0b00001,
+ 0b01010,
+ 0b00100,
+ 0b01010,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // c 8
+ 0b11111,
+ 0b00010,
+ 0b00100,
+ 0b01110,
+ 0b10101,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // c 9
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // c a
+ 0b00100,
+ 0b00010,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10000, // c b
+ 0b10000,
+ 0b11111,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+ 0b01111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // c c
+ 0b11111,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+ 0b00010,
+ 0b01100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // c d
+ 0b01000,
+ 0b10100,
+ 0b00010,
+ 0b00001,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // c e
+ 0b11111,
+ 0b00100,
+ 0b10101,
+ 0b10101,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // c f
+ 0b11111,
+ 0b00001,
+ 0b00001,
+ 0b01010,
+ 0b00100,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d 0
+ 0b01110,
+ 0b00000,
+ 0b01110,
+ 0b00000,
+ 0b01110,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d 1
+ 0b00100,
+ 0b01000,
+ 0b10000,
+ 0b10001,
+ 0b11111,
+ 0b00001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d 2
+ 0b00001,
+ 0b00001,
+ 0b01010,
+ 0b00100,
+ 0b01010,
+ 0b10000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d 3
+ 0b11111,
+ 0b01000,
+ 0b11111,
+ 0b01000,
+ 0b01000,
+ 0b00111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // d 4
+ 0b01000,
+ 0b11111,
+ 0b01001,
+ 0b01010,
+ 0b01000,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d 5
+ 0b01110,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d 6
+ 0b11111,
+ 0b00001,
+ 0b11111,
+ 0b00001,
+ 0b00001,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // d 7
+ 0b00000,
+ 0b11111,
+ 0b00001,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b10010, // d 8
+ 0b10010,
+ 0b10010,
+ 0b10010,
+ 0b00010,
+ 0b00100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d 9
+ 0b00100,
+ 0b10100,
+ 0b10100,
+ 0b10100,
+ 0b10101,
+ 0b10110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d a
+ 0b10000,
+ 0b10000,
+ 0b10001,
+ 0b10010,
+ 0b10100,
+ 0b11000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d b
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d c
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b00001,
+ 0b00010,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // d d
+ 0b11000,
+ 0b00000,
+ 0b00001,
+ 0b00001,
+ 0b00010,
+ 0b11100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00100, // d e
+ 0b10010,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11100, // d f
+ 0b10100,
+ 0b11100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // e 0
+ 0b01001,
+ 0b10101,
+ 0b10010,
+ 0b10010,
+ 0b01101,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01010, // e 1
+ 0b00000,
+ 0b01110,
+ 0b00001,
+ 0b01111,
+ 0b10001,
+ 0b01111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // e 2
+ 0b00000,
+ 0b01110,
+ 0b10001,
+ 0b11110,
+ 0b10001,
+ 0b11110,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+
+ 0b00000, // e 3
+ 0b00000,
+ 0b01110,
+ 0b10000,
+ 0b01100,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // e 4
+ 0b00000,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10011,
+ 0b11101,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+
+ 0b00000, // e 5
+ 0b00000,
+ 0b00000,
+ 0b01111,
+ 0b10100,
+ 0b10010,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // e 6
+ 0b00000,
+ 0b00110,
+ 0b01001,
+ 0b10001,
+ 0b10001,
+ 0b11110,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+
+ 0b00000, // e 7
+ 0b00000,
+ 0b01111,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01111,
+ 0b00001,
+ 0b00001,
+ 0b01110,
+
+ 0b00000, // e 8
+ 0b00000,
+ 0b00111,
+ 0b00100,
+ 0b00100,
+ 0b10100,
+ 0b01000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // e 9
+ 0b00010,
+ 0b11010,
+ 0b00010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00010, // e a
+ 0b00000,
+ 0b00110,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b00010,
+ 0b10010,
+ 0b01100,
+
+ 0b00000, // e b
+ 0b10100,
+ 0b01000,
+ 0b10100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // e c
+ 0b00100,
+ 0b01110,
+ 0b10100,
+ 0b10101,
+ 0b01110,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01000, // e d
+ 0b01000,
+ 0b11100,
+ 0b01000,
+ 0b11100,
+ 0b01000,
+ 0b01111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01110, // e e
+ 0b00000,
+ 0b10110,
+ 0b11001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01010, // e f
+ 0b00000,
+ 0b01110,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f 0
+ 0b00000,
+ 0b10110,
+ 0b11001,
+ 0b10001,
+ 0b10001,
+ 0b11110,
+ 0b10000,
+ 0b10000,
+ 0b10000,
+
+ 0b00000, // f 1
+ 0b00000,
+ 0b01101,
+ 0b10011,
+ 0b10001,
+ 0b10001,
+ 0b01111,
+ 0b00001,
+ 0b00001,
+ 0b00001,
+
+ 0b00000, // f 2
+ 0b01110,
+ 0b10001,
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b01110,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f 3
+ 0b00000,
+ 0b00000,
+ 0b01011,
+ 0b10101,
+ 0b11010,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f 4
+ 0b00000,
+ 0b01110,
+ 0b10001,
+ 0b10001,
+ 0b01010,
+ 0b11011,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b01010, // f 5
+ 0b00000,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10011,
+ 0b01101,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // f 6
+ 0b10000,
+ 0b01000,
+ 0b00100,
+ 0b01000,
+ 0b10000,
+ 0b11111,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f 7
+ 0b00000,
+ 0b11111,
+ 0b01010,
+ 0b01010,
+ 0b01010,
+ 0b10011,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // f 8
+ 0b00000,
+ 0b10001,
+ 0b01010,
+ 0b00100,
+ 0b01010,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f 9
+ 0b00000,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b10001,
+ 0b01111,
+ 0b00001,
+ 0b00001,
+ 0b01110,
+
+ 0b00000, // f a
+ 0b00001,
+ 0b11110,
+ 0b00100,
+ 0b11111,
+ 0b00100,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f b
+ 0b00000,
+ 0b11111,
+ 0b01000,
+ 0b01111,
+ 0b01001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f c
+ 0b00000,
+ 0b11111,
+ 0b10101,
+ 0b11111,
+ 0b10001,
+ 0b10001,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f d
+ 0b00000,
+ 0b00100,
+ 0b00000,
+ 0b11111,
+ 0b00000,
+ 0b00100,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b00000, // f e
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+ 0b00000,
+
+ 0b11111, // f f
+ 0b11111,
+ 0b11111,
+ 0b11111,
+ 0b11111,
+ 0b11111,
+ 0b11111,
+ 0b11111,
+ 0b11111,
+ 0b11111,
+ };
+
+ static_assert(std::size(g_fontTable0) == static_cast<size_t>(256 * 10));
+
+ const uint8_t* getCharacterData(const uint8_t _character)
+ {
+ return &g_fontTable0[static_cast<size_t>(_character) * 10];
+ }
+}
diff --git a/source/wLib/lcdfonts.h b/source/wLib/lcdfonts.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <cstdint>
+
+namespace wLib
+{
+ const uint8_t* getCharacterData(uint8_t _character);
+}
diff --git a/source/wLib/wDevice.cpp b/source/wLib/wDevice.cpp
@@ -0,0 +1,36 @@
+#include "wDevice.h"
+
+#include "dsp56kEmu/esaiclock.h"
+
+namespace wLib
+{
+ bool Device::setDspClockPercent(uint32_t _percent)
+ {
+ auto* c = getDspEsxiClock();
+ if(!c)
+ return false;
+ return c->setSpeedPercent(_percent);
+ }
+
+ uint32_t Device::getDspClockPercent() const
+ {
+ const auto* c = getDspEsxiClock();
+ if(!c)
+ return 0;
+ return c->getSpeedPercent();
+ }
+
+ uint64_t Device::getDspClockHz() const
+ {
+ const auto* c = getDspEsxiClock();
+ if(!c)
+ return false;
+ return c->getSpeedInHz();
+ }
+
+ void Device::process(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, const size_t _size, const std::vector<synthLib::SMidiEvent>& _midiIn, std::vector<synthLib::SMidiEvent>& _midiOut)
+ {
+ synthLib::Device::process(_inputs, _outputs, _size, _midiIn, _midiOut);
+ m_numSamplesProcessed += static_cast<uint32_t>(_size);
+ }
+}
diff --git a/source/wLib/wDevice.h b/source/wLib/wDevice.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include "../synthLib/device.h"
+#include "../synthLib/midiBufferParser.h"
+
+namespace dsp56k
+{
+ class EsxiClock;
+}
+
+namespace wLib
+{
+ class Device : public synthLib::Device
+ {
+ bool setDspClockPercent(uint32_t _percent) override;
+ uint32_t getDspClockPercent() const override;
+ uint64_t getDspClockHz() const override;
+
+ protected:
+ virtual dsp56k::EsxiClock* getDspEsxiClock() const = 0;
+ 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;
+
+ std::vector<uint8_t> m_midiOutBuffer;
+ synthLib::MidiBufferParser m_midiOutParser;
+ std::vector<synthLib::SMidiEvent> m_customSysexOut;
+ uint32_t m_numSamplesProcessed = 0;
+ };
+}
diff --git a/source/wLib/wDsp.cpp b/source/wLib/wDsp.cpp
diff --git a/source/wLib/wDsp.h b/source/wLib/wDsp.h
@@ -0,0 +1,8 @@
+#pragma once
+
+namespace wLib
+{
+ class Dsp
+ {
+ };
+}
diff --git a/source/wLib/wHardware.cpp b/source/wLib/wHardware.cpp
@@ -0,0 +1,5 @@
+#include "wHardware.h"
+
+namespace wLib
+{
+}
diff --git a/source/wLib/wHardware.h b/source/wLib/wHardware.h
@@ -0,0 +1,8 @@
+#pragma once
+
+namespace wLib
+{
+ class Hardware
+ {
+ };
+}
diff --git a/source/wLib/wMidi.cpp b/source/wLib/wMidi.cpp
@@ -0,0 +1,88 @@
+#include "wMidi.h"
+
+#include <deque>
+
+#include "../mc68k/qsm.h"
+
+namespace wLib
+{
+ static constexpr float g_sysexSendDelaySeconds = 0.2f;
+ static constexpr uint32_t g_sysexSendDelaySamples = static_cast<uint32_t>(44100.0f * g_sysexSendDelaySeconds);
+
+ Midi::Midi(mc68k::Qsm& _qsm) : m_qsm(_qsm)
+ {
+ }
+
+ void Midi::process(const uint32_t _numSamples)
+ {
+ std::unique_lock lock(m_mutex);
+
+ if(m_remainingSysexDelay)
+ m_remainingSysexDelay -= std::min(m_remainingSysexDelay, _numSamples);
+
+ while(m_remainingSysexDelay == 0 && !m_transmittingSysex && !m_pendingSysexBuffers.empty())
+ {
+ const auto& msg = m_pendingSysexBuffers.front();
+
+ for (const auto b : msg)
+ m_qsm.writeSciRX(b);
+
+ if(msg.size() > 0xf)
+ m_remainingSysexDelay = g_sysexSendDelaySamples;
+
+ m_pendingSysexBuffers.pop_front();
+ }
+ }
+
+ void Midi::writeMidi(const uint8_t _byte)
+ {
+ std::unique_lock lock(m_mutex);
+
+ if(_byte == 0xf0)
+ {
+ m_receivingSysex = true;
+ }
+
+ if(m_receivingSysex)
+ {
+ m_pendingSysexMessage.push_back(_byte);
+ }
+ else
+ {
+ m_qsm.writeSciRX(_byte);
+ }
+
+ if (_byte == 0xf7)
+ {
+ m_receivingSysex = false;
+
+ if (!m_pendingSysexMessage.empty())
+ m_pendingSysexBuffers.push_back(std::move(m_pendingSysexMessage));
+
+ m_pendingSysexMessage.clear();
+ }
+ }
+
+ void Midi::readTransmitBuffer(std::vector<uint8_t>& _result)
+ {
+ std::deque<uint16_t> midiData;
+ m_qsm.readSciTX(midiData);
+ if (midiData.empty())
+ return;
+
+ _result.clear();
+ _result.reserve(midiData.size());
+
+ for (const auto data : midiData)
+ {
+ const uint8_t d = data & 0xff;
+
+ if(d == 0xf0)
+ m_transmittingSysex = true;
+ else if(d == 0xf7)
+ m_transmittingSysex = false;
+
+ _result.push_back(d);
+ }
+ }
+}
diff --git a/source/wLib/wMidi.h b/source/wLib/wMidi.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <deque>
+#include <vector>
+#include <cstdint>
+#include <mutex>
+
+namespace mc68k
+{
+ class Qsm;
+}
+
+namespace wLib
+{
+ class Midi
+ {
+ public:
+ explicit Midi(mc68k::Qsm& _qsm);
+
+ void process(uint32_t _numSamples);
+
+ void writeMidi(uint8_t _byte);
+ void writeMidi(const std::initializer_list<uint8_t>& _bytes)
+ {
+ for (const uint8_t byte : _bytes)
+ writeMidi(byte);
+ }
+ void writeMidi(const std::vector<uint8_t>& _bytes)
+ {
+ for (const uint8_t byte : _bytes)
+ writeMidi(byte);
+ }
+ void readTransmitBuffer(std::vector<uint8_t>& _result);
+
+ private:
+ mc68k::Qsm& m_qsm;
+
+ bool m_transmittingSysex = false;
+ bool m_receivingSysex = false;
+ uint32_t m_remainingSysexDelay = 0;
+
+ std::deque< std::vector<uint8_t> > m_pendingSysexBuffers;
+ std::vector<uint8_t> m_pendingSysexMessage;
+ std::mutex m_mutex;
+ };
+}
diff --git a/source/wLib/wMidiTypes.h b/source/wLib/wMidiTypes.h
@@ -0,0 +1,23 @@
+#pragma once
+
+namespace wLib
+{
+ enum MidiHeaderByte : uint8_t
+ {
+ IdWaldorf = 0x3e,
+ IdDeviceOmni = 0x7f
+ };
+
+ enum SysexIndex
+ {
+ IdxSysexBegin = 0,
+ IdxIdWaldorf = 1,
+ IdxIdMachine = 2,
+ IdxDeviceId = 3,
+ IdxCommand = 4,
+
+ // dumps / dump requests
+ IdxBuffer = 5,
+ IdxLocation = 6,
+ };
+}
diff --git a/source/wLib/wPlugin.cpp b/source/wLib/wPlugin.cpp
@@ -0,0 +1,5 @@
+#include "wPlugin.h"
+
+namespace wLib
+{
+}
diff --git a/source/wLib/wPlugin.h b/source/wLib/wPlugin.h
@@ -0,0 +1,9 @@
+#pragma once
+
+namespace wLib
+{
+ class Plugin
+ {
+
+ };
+}
diff --git a/source/wLib/wRom.cpp b/source/wLib/wRom.cpp
@@ -0,0 +1,112 @@
+#include "wRom.h"
+
+#include <cstdint>
+
+#include "../synthLib/os.h"
+#include "../synthLib/midiToSysex.h"
+
+namespace wLib
+{
+ constexpr uint8_t IdWaldorf = 0x3e;
+
+ bool ROM::loadFromFile(const std::string& _filename, const uint32_t _expectedSize)
+ {
+ FILE* hFile = fopen(_filename.c_str(), "rb");
+ if(!hFile)
+ return false;
+
+ (void)fseek(hFile, 0, SEEK_END);
+ const auto size = ftell(hFile);
+ (void)fseek(hFile, 0, SEEK_SET);
+
+ m_buffer.resize(size);
+ const auto numRead = fread(m_buffer.data(), 1, size, hFile);
+ (void)fclose(hFile);
+
+ if(numRead != static_cast<size_t>(size))
+ {
+ m_buffer.clear();
+ return false;
+ }
+
+ if(numRead != _expectedSize)
+ {
+ loadFromMidi(m_buffer, _filename);
+
+ if (!m_buffer.empty() && m_buffer.size() < _expectedSize)
+ m_buffer.resize(_expectedSize, 0xff);
+ }
+
+ if(!m_buffer.empty())
+ {
+ m_data = m_buffer.data();
+ return true;
+ }
+ return false;
+ }
+
+ bool ROM::loadFromMidi(std::vector<unsigned char>& _buffer, const std::string& _filename)
+ {
+ _buffer.clear();
+
+ std::vector<uint8_t> data;
+ if(!synthLib::MidiToSysex::readFile(data, _filename.c_str()) || data.empty())
+ return false;
+
+ return loadFromSysExBuffer(_buffer, data);
+ }
+
+ bool ROM::loadFromSysExFile(std::vector<uint8_t>& _buffer, const std::string& _filename)
+ {
+ _buffer.clear();
+
+ std::vector<uint8_t> buf;
+ if (!synthLib::readFile(buf, _filename))
+ return false;
+ return loadFromSysExBuffer(_buffer, buf);
+ }
+
+ bool ROM::loadFromSysExBuffer(std::vector<unsigned char>& _buffer, const std::vector<uint8_t>& _sysex)
+ {
+ _buffer.reserve(_sysex.size());
+
+ std::vector<std::vector<uint8_t>> messages;
+ synthLib::MidiToSysex::splitMultipleSysex(messages, _sysex);
+
+ uint16_t expectedCounter = 1;
+
+ for (const auto& message : messages)
+ {
+ if(message.size() < 0xfc)
+ continue;
+
+ if(message[1] != IdWaldorf)
+ continue;
+
+ if(message[3] != 0x7f)
+ continue;
+
+ if(message[4] != 0x71 && message[4] != 0x72 && message[4] != 0x73) // MW2, Q, mQ
+ continue;
+
+ const auto counter = (message[6] << 7) | message[7];
+ if(expectedCounter != counter && counter != 1)
+ return false;
+ expectedCounter = static_cast<uint16_t>(counter);
+ ++expectedCounter;
+
+ size_t i = 10;
+ while(i + 5 < message.size())
+ {
+ const auto lsbs = message[i];
+ _buffer.push_back((message[i+1] << 1) | ((lsbs >> 0) & 1));
+ _buffer.push_back((message[i+2] << 1) | ((lsbs >> 1) & 1));
+ _buffer.push_back((message[i+3] << 1) | ((lsbs >> 2) & 1));
+ _buffer.push_back((message[i+4] << 1) | ((lsbs >> 3) & 1));
+ i += 5;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/source/wLib/wRom.h b/source/wLib/wRom.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace wLib
+{
+ class ROM
+ {
+ public:
+ explicit ROM(const std::string& _filename, const uint32_t _expectedSize, const uint8_t* _data = nullptr) : m_data(_data)
+ {
+ if (!_filename.empty())
+ loadFromFile(_filename, _expectedSize);
+ }
+ virtual ~ROM() = default;
+
+ const uint8_t* getData() const { return m_data; }
+ bool isValid() const { return !m_buffer.empty(); }
+ virtual uint32_t getSize() const = 0;
+
+ static bool loadFromMidi(std::vector<uint8_t>& _buffer, const std::string& _filename);
+ static bool loadFromSysExFile(std::vector<uint8_t>& _buffer, const std::string& _filename);
+ static bool loadFromSysExBuffer(std::vector<uint8_t> &_buffer, const std::vector<uint8_t> &_sysex);
+
+ private:
+ bool loadFromFile(const std::string& _filename, uint32_t _expectedSize);
+
+ const uint8_t* m_data;
+ std::vector<uint8_t> m_buffer;
+ };
+}
diff --git a/source/wLib/wState.cpp b/source/wLib/wState.cpp
@@ -0,0 +1,5 @@
+#include "wState.h"
+
+namespace wLib
+{
+}
diff --git a/source/wLib/wState.h b/source/wLib/wState.h
@@ -0,0 +1,41 @@
+#pragma once
+
+#include <cstdint>
+#include <cstddef>
+#include <vector>
+#include <array>
+
+namespace wLib
+{
+ using SysEx = std::vector<uint8_t>;
+ using Responses = std::vector<SysEx>;
+
+ class State
+ {
+ protected:
+ template<size_t Size> static bool convertTo(std::array<uint8_t, Size>& _dst, const SysEx& _data)
+ {
+ if(_data.size() != Size)
+ return false;
+ std::copy(_data.begin(), _data.end(), _dst.begin());
+ return true;
+ }
+
+ template<size_t Size> static SysEx convertTo(const std::array<uint8_t, Size>& _src)
+ {
+ SysEx dst;
+ dst.insert(dst.begin(), _src.begin(), _src.end());
+ return dst;
+ }
+
+ template<size_t Size> static void updateChecksum(std::array<uint8_t, Size>& _src, uint32_t _startIndex)
+ {
+ uint8_t& c = _src[_src.size() - 2];
+ c = 0;
+ for(size_t i = _startIndex; i<_src.size()-2; ++i)
+ c += _src[i];
+ c &= 0x7f;
+ }
+
+ };
+}
diff --git a/source/xtJucePlugin/.gitignore b/source/xtJucePlugin/.gitignore
@@ -0,0 +1 @@
+/version.h
diff --git a/source/xtJucePlugin/CMakeLists.txt b/source/xtJucePlugin/CMakeLists.txt
@@ -0,0 +1,28 @@
+cmake_minimum_required(VERSION 3.15)
+project(xtJucePlugin VERSION ${CMAKE_PROJECT_VERSION})
+
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_SOURCE_DIR}/version.h)
+
+set(SOURCES
+ parameterDescriptions_xt.json
+ PluginEditor.cpp PluginEditor.h
+ PluginEditorState.cpp PluginEditorState.h
+ PluginProcessor.cpp PluginProcessor.h
+ xtController.cpp xtController.h
+ xtEditor.cpp xtEditor.h
+ xtFrontPanel.cpp xtFrontPanel.h
+ xtLcd.cpp xtLcd.h
+ xtPatchManager.cpp xtPatchManager.h
+ version.h
+)
+
+# https://forum.juce.com/t/help-needed-using-binarydata-with-cmake-juce-6/40486
+# "This might be because the BinaryData files are generated during the build, so the IDE may not be able to find them until the build has been run once (and even then, some IDEs might need a bit of a nudge to re-index the binary directory…)"
+SET(ASSETS "parameterDescriptions_xt.json")
+
+#include(skins/xtFrontPanel/assets.cmake)
+include(skins/xtDefault/assets.cmake)
+
+juce_add_binary_data(xtJucePlugin_BinaryData SOURCES ${ASSETS} ${ASSETS_xtDefault})
+
+createJucePluginWithFX(xtJucePlugin "Xenia" "Txts" "Txtf" xtJucePlugin_BinaryData xtLib)
diff --git a/source/xtJucePlugin/PluginEditor.cpp b/source/xtJucePlugin/PluginEditor.cpp
@@ -0,0 +1,83 @@
+#include "PluginEditor.h"
+
+#include "PluginEditorState.h"
+#include "PluginProcessor.h"
+
+//==============================================================================
+AudioPluginAudioProcessorEditor::AudioPluginAudioProcessorEditor(AudioPluginAudioProcessor &p, PluginEditorState& s) :
+ AudioProcessorEditor(&p), processorRef(p), m_state(s)
+{
+ addMouseListener(this, true);
+
+ m_state.evSkinLoaded = [&](juce::Component* _component)
+ {
+ setUiRoot(_component);
+ };
+
+ m_state.evSetGuiScale = [&](const int _scale)
+ {
+ if(getNumChildComponents() > 0)
+ setGuiScale(getChildComponent(0), _scale);
+ };
+
+ m_state.enableBindings();
+
+ setUiRoot(m_state.getUiRoot());
+}
+
+AudioPluginAudioProcessorEditor::~AudioPluginAudioProcessorEditor()
+{
+ m_state.evSetGuiScale = [&](int){};
+ m_state.evSkinLoaded = [&](juce::Component*){};
+
+ m_state.disableBindings();
+
+ setUiRoot(nullptr);
+}
+
+void AudioPluginAudioProcessorEditor::setGuiScale(juce::Component* _comp, int percent)
+{
+ if(!_comp)
+ return;
+
+ const auto s = static_cast<float>(percent)/100.0f * m_state.getRootScale();
+ _comp->setTransform(juce::AffineTransform::scale(s,s));
+
+ setSize(static_cast<int>(m_state.getWidth() * s), static_cast<int>(m_state.getHeight() * s));
+
+// auto* config = processorRef.getController().getConfig();
+// config->setValue("scale", percent);
+// config->saveIfNeeded();
+}
+
+void AudioPluginAudioProcessorEditor::setUiRoot(juce::Component* _component)
+{
+ removeAllChildren();
+
+ if(!_component)
+ return;
+
+// const auto& config = processorRef.getController().getConfig();
+// const auto scale = config->getIntValue("scale", 100);
+
+// setGuiScale(_component, scale);
+ addAndMakeVisible(_component);
+}
+
+void AudioPluginAudioProcessorEditor::mouseDown(const juce::MouseEvent& event)
+{
+ if(!event.mods.isPopupMenu())
+ {
+ AudioProcessorEditor::mouseDown(event);
+ return;
+ }
+
+ // file browsers have their own menu, do not display two menus at once
+ if(event.eventComponent && event.eventComponent->findParentComponentOfClass<juce::FileBrowserComponent>())
+ return;
+
+ if(dynamic_cast<juce::TextEditor*>(event.eventComponent))
+ return;
+
+ m_state.openMenu();
+}
diff --git a/source/xtJucePlugin/PluginEditor.h b/source/xtJucePlugin/PluginEditor.h
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <juce_audio_processors/juce_audio_processors.h>
+
+class AudioPluginAudioProcessor;
+class PluginEditorState;
+
+//==============================================================================
+class AudioPluginAudioProcessorEditor : public juce::AudioProcessorEditor
+{
+public:
+ explicit AudioPluginAudioProcessorEditor (AudioPluginAudioProcessor&, PluginEditorState&);
+ ~AudioPluginAudioProcessorEditor() override;
+
+ void mouseDown(const juce::MouseEvent& event) override;
+
+ void paint(juce::Graphics& g) override {}
+
+private:
+ void setGuiScale(juce::Component* _component, int percent);
+ void setUiRoot(juce::Component* _component);
+
+ // This reference is provided as a quick way for your editor to
+ // access the processor object that created it.
+ AudioPluginAudioProcessor& processorRef;
+
+ PluginEditorState& m_state;
+
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessorEditor)
+};
diff --git a/source/xtJucePlugin/PluginEditorState.cpp b/source/xtJucePlugin/PluginEditorState.cpp
@@ -0,0 +1,70 @@
+#include "PluginEditorState.h"
+
+#include "xtEditor.h"
+#include "PluginProcessor.h"
+
+#include "../synthLib/os.h"
+
+const std::vector<PluginEditorState::Skin> g_includedSkins =
+{
+ {"XT", "xtDefault.json", ""},
+};
+
+PluginEditorState::PluginEditorState(AudioPluginAudioProcessor& _processor) : jucePluginEditorLib::PluginEditorState(_processor, _processor.getController(), g_includedSkins)
+{
+ loadDefaultSkin();
+}
+
+void PluginEditorState::initContextMenu(juce::PopupMenu& _menu)
+{
+ jucePluginEditorLib::PluginEditorState::initContextMenu(_menu);
+
+ auto& p = m_processor;
+
+ const auto gain = static_cast<int>(std::roundf(p.getOutputGain()));
+
+ juce::PopupMenu gainMenu;
+
+ 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;
+}
+
+
+genericUI::Editor* PluginEditorState::createEditor(const Skin& _skin, std::function<void()> _openMenuCallback)
+{
+ return new xtJucePlugin::Editor(m_processor, m_parameterBinding, _skin.folder, _skin.jsonFilename);
+}
diff --git a/source/xtJucePlugin/PluginEditorState.h b/source/xtJucePlugin/PluginEditorState.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <functional>
+
+#include "../jucePluginEditorLib/pluginEditorState.h"
+
+namespace juce
+{
+ class Component;
+}
+
+class AudioPluginAudioProcessor;
+
+class PluginEditorState : public jucePluginEditorLib::PluginEditorState
+{
+public:
+ explicit PluginEditorState(AudioPluginAudioProcessor& _processor);
+ void initContextMenu(juce::PopupMenu& _menu) override;
+ bool initAdvancedContextMenu(juce::PopupMenu& _menu, bool _enabled) override;
+private:
+ genericUI::Editor* createEditor(const Skin& _skin, std::function<void()> _openMenuCallback) override;
+};
diff --git a/source/xtJucePlugin/PluginProcessor.cpp b/source/xtJucePlugin/PluginProcessor.cpp
@@ -0,0 +1,67 @@
+#include "PluginProcessor.h"
+
+#include <juce_audio_processors/juce_audio_processors.h>
+#include <juce_audio_devices/juce_audio_devices.h>
+
+#include "PluginEditorState.h"
+#include "xtController.h"
+
+#include "../jucePluginLib/processor.h"
+#include "../xtLib/xtDevice.h"
+
+class Controller;
+
+namespace
+{
+ juce::PropertiesFile::Options getOptions()
+ {
+ juce::PropertiesFile::Options opts;
+ opts.applicationName = "DSP56300EmulatorXenia";
+ opts.filenameSuffix = ".settings";
+ opts.folderName = "DSP56300EmulatorXenia";
+ opts.osxLibrarySubFolder = "Application Support/DSP56300EmulatorXenia";
+ return opts;
+ }
+}
+
+//==============================================================================
+AudioPluginAudioProcessor::AudioPluginAudioProcessor() :
+ Processor(BusesProperties()
+ .withInput("Input", juce::AudioChannelSet::stereo(), true)
+ .withOutput("Output", juce::AudioChannelSet::stereo(), true)
+#if JucePlugin_IsSynth
+ .withOutput("Out 2", juce::AudioChannelSet::stereo(), true)
+#endif
+ , getOptions(), pluginLib::Processor::Properties{JucePlugin_Name, JucePlugin_IsSynth, JucePlugin_WantsMidiInput, JucePlugin_ProducesMidiOutput, JucePlugin_IsMidiEffect})
+{
+ getController();
+ const auto latencyBlocks = getConfig().getIntValue("latencyBlocks", static_cast<int>(getPlugin().getLatencyBlocks()));
+ Processor::setLatencyBlocks(latencyBlocks);
+}
+
+AudioPluginAudioProcessor::~AudioPluginAudioProcessor()
+{
+ destroyEditorState();
+}
+
+jucePluginEditorLib::PluginEditorState* AudioPluginAudioProcessor::createEditorState()
+{
+ return new PluginEditorState(*this);
+}
+
+synthLib::Device* AudioPluginAudioProcessor::createDevice()
+{
+ return new xt::Device();
+}
+
+pluginLib::Controller* AudioPluginAudioProcessor::createController()
+{
+ return new Controller(*this);
+}
+
+//==============================================================================
+// This creates new instances of the plugin..
+juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
+{
+ return new AudioPluginAudioProcessor();
+}
diff --git a/source/xtJucePlugin/PluginProcessor.h b/source/xtJucePlugin/PluginProcessor.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "../jucePluginEditorLib/pluginProcessor.h"
+
+//==============================================================================
+class AudioPluginAudioProcessor : public jucePluginEditorLib::Processor
+{
+public:
+ AudioPluginAudioProcessor();
+ ~AudioPluginAudioProcessor() override;
+
+ jucePluginEditorLib::PluginEditorState* createEditorState() override;
+ // _____________
+ //
+ synthLib::Device* createDevice() override;
+
+ pluginLib::Controller* createController() override;
+
+private:
+
+ //==============================================================================
+ JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioPluginAudioProcessor)
+};
diff --git a/source/xtJucePlugin/parameterDescriptions_xt.json b/source/xtJucePlugin/parameterDescriptions_xt.json
@@ -0,0 +1,1106 @@
+{
+ "parameterdescriptiondefaults":
+ {
+ "isPublic":true,
+ "isBipolar":false,
+ "toText":"unsignedZero",
+ "name":"",
+ "class":"",
+ "min":0,
+ "max":127,
+ "isBool":false,
+ "isDiscrete":false,
+ "page":0,
+ "step":0
+ },
+ "parameterdescriptions":
+ [
+ {"page":0, "index":0, "name":"Version", "min":0, "max":1, "isPublic":false, "isDiscrete":true},
+
+ // Osc 1
+ {"index":1 , "name":"O1Octave" , "min":16, "max":112, "step":12},
+ {"index":2 , "name":"O1Semi" , "min":52, "max":76, "isBipolar":true, "toText":"signed"},
+ {"index":3 , "name":"O1Detune" , "isBipolar":true},
+
+ {"index":5 , "name":"O1BendRange" , "min":0, "max":122, "toText":"pbrange"},
+ {"index":6 , "name":"O1KeyTrack" , "min":0, "max":76, "default":64, "toText":"keytrack76"},
+ {"index":7 , "name":"O1FmAmount"},
+
+ // Osc 2
+ {"index":12 , "name":"O2Octave" , "min":16, "max":112, "step":12},
+ {"index":13 , "name":"O2Semi" , "min":52, "max":76, "isBipolar":true, "toText":"signed"},
+ {"index":14 , "name":"O2Detune" , "isBipolar":true},
+
+ {"index":16 , "name":"O2BendRange" , "min":0, "max":122, "toText":"pbrange"},
+ {"index":17 , "name":"O2KeyTrack" , "min":0, "max":76, "default":64, "toText":"keytrack76"},
+ {"index":18 , "name":"O2FmAmount"},
+ {"index":19 , "name":"O2Link" , "min":0, "max":1, "toText":"offOn"},
+
+ // Wavetable
+ {"index":25, "name":"Wave", "isDiscrete":true, "toText":"waveType"},
+
+ // Wave 1
+ {"index":26, "name":"W1StartW", "min":0 , "max":63, "toText":"waveStartWave"},
+ {"index":27, "name":"W1StartP", "min":0 , "max":127, "toText":"waveStartPhase"},
+ {"index":28, "name":"W1EnvAmount", "min":0 , "max":127, "isBipolar":true, "toText":"signed"},
+ {"index":29, "name":"W1EnvVelAmount", "min":0 , "max":127, "isBipolar":true, "toText":"signed"},
+ {"index":30, "name":"W1Keytrack", "min":0 , "max":127, "toText":"keytrack127"},
+ {"index":31, "name":"W1Limit", "min":0 , "max":1, "toText":"offOn"},
+
+ // Wave 2
+ {"index":36, "name":"W2StartW", "min":0 , "max":63, "toText":"waveStartWave"},
+ {"index":37, "name":"W2StartP", "min":0 , "max":127, "toText":"waveStartPhase"},
+ {"index":38, "name":"W2EnvAmount", "min":0 , "max":127, "isBipolar":true, "toText":"signed"},
+ {"index":39, "name":"W2EnvVelAmount", "min":0 , "max":127, "isBipolar":true, "toText":"signed"},
+ {"index":40, "name":"W2Keytrack", "min":0 , "max":127, "toText":"keytrack127"},
+ {"index":41, "name":"W2Limit", "min":0 , "max":1, "toText":"offOn"},
+ {"index":42, "name":"W2Link", "min":0 , "max":1, "toText":"offOn"},
+
+ // Mixer
+ {"index":47, "name":"MixW1"},
+ {"index":48, "name":"MixW2"},
+ {"index":49, "name":"MixRingMod"},
+ {"index":50, "name":"MixNoise"},
+ {"index":51, "name":"MixExternal"},
+
+ {"index":53, "name":"Aliasing", "min":0, "max":5, "toText":"aliasing"},
+ {"index":54, "name":"TimeQuant", "min":0, "max":5, "toText":"timeQuant"},
+ {"index":55, "name":"Clipping", "min":0, "max":1, "toText":"clipping"},
+
+ {"index":57, "name":"Accuracy", "min":0, "max":1, "toText":"accuracy"},
+
+ {"index":58, "name":"PlayParamA", "min":0, "max":82, "isDiscrete":true, "toText":"playParameter"},
+ {"index":59, "name":"PlayParamB", "min":0, "max":82, "isDiscrete":true, "toText":"playParameter"},
+ {"index":60, "name":"PlayParamC", "min":0, "max":82, "isDiscrete":true, "toText":"playParameter"},
+ {"index":61, "name":"PlayParamD", "min":0, "max":82, "isDiscrete":true, "toText":"playParameter"},
+
+ // Filters
+ {"index":62, "name":"F1Cutoff"},
+ {"index":63, "name":"F1Resonance"},
+ {"index":64, "name":"F1Type", "min":0, "max":9, "isDiscrete":true, "toText":"filter1Type"},
+ {"index":65, "name":"F1KeyTrack", "toText":"keytrack127"},
+ {"index":66, "name":"F1EnvAmount", "toText":"signed", "default":64, "isBipolar":true},
+ {"index":67, "name":"F1EnvVelAmount", "toText":"signed", "default":64, "isBipolar":true},
+
+ {"index":70, "name":"F1ContextSensitive"},
+
+ {"index":73, "name":"F2Cutoff"},
+ {"index":74, "name":"F2Type", "min":0, "max":1, "isDiscrete":true, "toText":"filter2Type"},
+ {"index":75, "name":"F2KeyTrack", "toText":"keytrack127"},
+
+ {"index":76, "name":"EffectType", "min":0, "max":34, "toText":"effectType", "isDiscrete":true},
+
+ // Amp
+ {"index":77, "name":"AmpVolume"},
+
+ {"index":79, "name":"AmpVelocity", "isBipolar":true, "toText":"signed"},
+ {"index":80, "name":"AmpKeytrack", "isBipolar":true, "toText":"keytrack127"},
+
+ {"index":81, "name":"EffectParamA"},
+
+ {"index":82, "name":"ChorusEnabled", "min":0, "max":1, "isBool":true, "toText":"offOn"},
+
+ {"index":83, "name":"EffectParamB"},
+
+ {"index":84, "name":"Pan", "isBipolar":true, "toText":"pan"},
+ {"index":85, "name":"PanKeytrack", "isBipolar":true, "toText":"keytrack127"},
+ {"index":86, "name":"EffectParamC"},
+
+ // Glide
+ {"index":87, "name":"GlideEnabled", "min":0, "max":1, "isBool":true, "toText":"offOn"},
+ {"index":88, "name":"GlideType", "min":0, "max":3, "toText":"glideType", "isDiscrete":true},
+ {"index":89, "name":"GlideMode", "min":0, "max":1, "toText":"glideMode", "isDiscrete":true},
+ {"index":90, "name":"GlideTime"},
+
+ // Arp
+ {"index":92, "name":"ArpMode", "min":0, "max":2, "toText":"arpMode", "isDiscrete":true},
+ {"index":93, "name":"ArpTempo", "min":1, "max":127, "toText":"arpTempo"},
+ {"index":94, "name":"ArpClock", "min":0, "max":15, "toText":"arpClock", "isDiscrete":true},
+ {"index":95, "name":"ArpRange", "min":1, "max":10, "isDiscrete":true},
+ {"index":96, "name":"ArpPattern", "min":0, "max":16, "toText":"arpPattern", "isDiscrete":true},
+ {"index":97, "name":"ArpDirection", "min":0, "max":3, "toText":"arpDirection", "isDiscrete":true},
+ {"index":98, "name":"ArpNoteOrder", "min":0, "max":3, "toText":"arpNoteOrder", "isDiscrete":true},
+ {"index":99, "name":"ArpVelocity", "min":0, "max":1, "toText":"arpVelocity", "isDiscrete":true},
+ {"index":100, "name":"ArpReset", "min":0, "max":1, "toText":"offOn", "isDiscrete":true},
+ {"index":101, "name":"ArpUserPatternLength", "min":0, "max":15, "isDiscrete":true, "toText":"unsignedOne"},
+
+ {"index":102, "name":"ArpUserPattern1" , "min":0, "max":1, "isBool":true},
+ {"index":102, "name":"ArpUserPattern2" , "min":0, "max":1, "isBool":true},
+ {"index":102, "name":"ArpUserPattern3" , "min":0, "max":1, "isBool":true},
+ {"index":102, "name":"ArpUserPattern4" , "min":0, "max":1, "isBool":true},
+ {"index":103, "name":"ArpUserPattern5" , "min":0, "max":1, "isBool":true},
+ {"index":103, "name":"ArpUserPattern6" , "min":0, "max":1, "isBool":true},
+ {"index":103, "name":"ArpUserPattern7" , "min":0, "max":1, "isBool":true},
+ {"index":103, "name":"ArpUserPattern8" , "min":0, "max":1, "isBool":true},
+ {"index":104, "name":"ArpUserPattern9" , "min":0, "max":1, "isBool":true},
+ {"index":104, "name":"ArpUserPattern10", "min":0, "max":1, "isBool":true},
+ {"index":104, "name":"ArpUserPattern11", "min":0, "max":1, "isBool":true},
+ {"index":104, "name":"ArpUserPattern12", "min":0, "max":1, "isBool":true},
+ {"index":105, "name":"ArpUserPattern13", "min":0, "max":1, "isBool":true},
+ {"index":105, "name":"ArpUserPattern14", "min":0, "max":1, "isBool":true},
+ {"index":105, "name":"ArpUserPattern15", "min":0, "max":1, "isBool":true},
+ {"index":105, "name":"ArpUserPattern16", "min":0, "max":1, "isBool":true},
+
+ {"index":108, "name":"AllocationMode", "min":0, "max":1, "toText":"allocationMode", "isDiscrete":true},
+ {"index":109, "name":"Assignment", "min":0, "max":2, "toText":"assignment", "isDiscrete":true},
+ {"index":110, "name":"Detune"},
+
+ {"index":112, "name":"DePan"},
+
+ {"index":113, "name":"F1EnvAttack"},
+ {"index":114, "name":"F1EnvDecay"},
+ {"index":115, "name":"F1EnvSustain"},
+ {"index":116, "name":"F1EnvRelease"},
+ {"index":117, "name":"F1EnvTrigger", "min":0, "max":2, "isDiscrete":true, "toText":"envTrigger"},
+
+ {"index":119, "name":"AmpEnvAttack"},
+ {"index":120, "name":"AmpEnvDecay"},
+ {"index":121, "name":"AmpEnvSustain"},
+ {"index":122, "name":"AmpEnvRelease"},
+ {"index":123, "name":"AmpEnvTrigger", "min":0, "max":2, "isDiscrete":true, "toText":"envTrigger"},
+
+ {"index":125, "name":"WaveEnvTime1"}, {"index":126, "name":"WaveEnvLevel1"},
+ {"index":127, "name":"WaveEnvTime2"}, {"index":128, "name":"WaveEnvLevel2"},
+ {"index":129, "name":"WaveEnvTime3"}, {"index":130, "name":"WaveEnvLevel3"},
+ {"index":131, "name":"WaveEnvTime4"}, {"index":132, "name":"WaveEnvLevel4"},
+ {"index":133, "name":"WaveEnvTime5"}, {"index":134, "name":"WaveEnvLevel5"},
+ {"index":135, "name":"WaveEnvTime6"}, {"index":136, "name":"WaveEnvLevel6"},
+ {"index":137, "name":"WaveEnvTime7"}, {"index":138, "name":"WaveEnvLevel7"},
+ {"index":139, "name":"WaveEnvTime8"}, {"index":140, "name":"WaveEnvLevel8"},
+
+ {"index":141, "name":"WaveEnvTrigger", "min":0, "max":2, "isDiscrete":true, "toText":"envTrigger"},
+
+ {"index":142, "name":"WaveKeyOnLoop", "min":0, "max":1, "toText":"offOn", "isBool":true},
+ {"index":143, "name":"WaveKeyOnLoopStart", "min":0, "max":7, "isDiscrete":true},
+ {"index":144, "name":"WaveKeyOnLoopEnd", "min":0, "max":7, "isDiscrete":true},
+
+ {"index":145, "name":"WaveKeyOffLoop", "min":0, "max":1, "toText":"offOn", "isBool":true},
+ {"index":146, "name":"WaveKeyOffLoopStart", "min":0, "max":7, "isDiscrete":true},
+ {"index":147, "name":"WaveKeyOffLoopEnd", "min":0, "max":7, "isDiscrete":true},
+
+ {"index":149, "name":"FreeEnvTime1"}, {"index":150, "name":"FreeEnvLevel1", "isBipolar":true, "toText":"signed", "default":64},
+ {"index":151, "name":"FreeEnvTime2"}, {"index":152, "name":"FreeEnvLevel2", "isBipolar":true, "toText":"signed", "default":64},
+ {"index":153, "name":"FreeEnvTime3"}, {"index":154, "name":"FreeEnvLevel3", "isBipolar":true, "toText":"signed", "default":64},
+ {"index":155, "name":"FreeEnvReleaseTime"}, {"index":156, "name":"FreeEnvReleaseLevel", "isBipolar":true, "toText":"signed", "default":64},
+ {"index":157, "name":"FreeEnvTrigger", "min":0, "max":2, "isDiscrete":true, "toText":"envTrigger"},
+
+ // LFO 1
+ {"index":159, "name":"Lfo1Rate"},
+ {"index":160, "name":"Lfo1Shape", "min":0, "max":5, "isDiscrete":true, "toText":"lfoShape"},
+ {"index":161, "name":"Lfo1Delay"},
+ {"index":162, "name":"Lfo1Sync", "min":0 , "max":3, "isDiscrete":true, "toText":"lfoSync"},
+ {"index":163, "name":"Lfo1Symmetry", "isBipolar":true, "toText":"signed"},
+ {"index":164, "name":"Lfo1Humanize"},
+
+ {"index":166, "name":"Lfo2Rate"},
+ {"index":167, "name":"Lfo2Shape", "min":0, "max":5, "isDiscrete":true, "toText":"lfoShape"},
+ {"index":168, "name":"Lfo2Delay"},
+ {"index":169, "name":"Lfo2Sync", "min":0 , "max":3, "isDiscrete":true, "toText":"lfoSync"},
+ {"index":170, "name":"Lfo2Symmetry", "isBipolar":true, "toText":"signed"},
+ {"index":171, "name":"Lfo2Humanize"},
+ {"index":172, "name":"Lfo2Phase", "toText":"lfoPhase"},
+
+ // Modifiers
+ {"index":174, "name":"ModDelaySource", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":175, "name":"ModDelayTime"},
+
+ {"index":176, "name":"Mod1Source1", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":177, "name":"Mod1Source2", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":178, "name":"Mod1Type", "min":0, "max":15, "isDiscrete":true, "toText":"modType" },
+ {"index":179, "name":"Mod1Parameter" },
+ {"index":180, "name":"Mod2Source1", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":181, "name":"Mod2Source2", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":182, "name":"Mod2Type", "min":0, "max":15, "isDiscrete":true, "toText":"modType" },
+ {"index":183, "name":"Mod2Parameter" },
+ {"index":184, "name":"Mod3Source1", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":185, "name":"Mod3Source2", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":186, "name":"Mod3Type", "min":0, "max":15, "isDiscrete":true, "toText":"modType" },
+ {"index":187, "name":"Mod3Parameter" },
+ {"index":188, "name":"Mod4Source1", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":189, "name":"Mod4Source2", "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":190, "name":"Mod4Type", "min":0, "max":15, "isDiscrete":true, "toText":"modType" },
+ {"index":191, "name":"Mod4Parameter" },
+
+ // Mod Matrix
+ {"index":192, "name":"Slot1Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":193, "name":"Slot1Amount" , "default":64, "isBipolar":true },
+ {"index":194, "name":"Slot1Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":195, "name":"Slot2Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":196, "name":"Slot2Amount" , "default":64, "isBipolar":true },
+ {"index":197, "name":"Slot2Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":198, "name":"Slot3Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":199, "name":"Slot3Amount" , "default":64, "isBipolar":true },
+ {"index":200, "name":"Slot3Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":201, "name":"Slot4Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":202, "name":"Slot4Amount" , "default":64, "isBipolar":true },
+ {"index":203, "name":"Slot4Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":204, "name":"Slot5Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":205, "name":"Slot5Amount" , "default":64, "isBipolar":true },
+ {"index":206, "name":"Slot5Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":207, "name":"Slot6Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":208, "name":"Slot6Amount" , "default":64, "isBipolar":true },
+ {"index":209, "name":"Slot6Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":210, "name":"Slot7Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":211, "name":"Slot7Amount" , "default":64, "isBipolar":true },
+ {"index":212, "name":"Slot7Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":213, "name":"Slot8Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":214, "name":"Slot8Amount" , "default":64, "isBipolar":true },
+ {"index":215, "name":"Slot8Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":216, "name":"Slot9Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":217, "name":"Slot9Amount" , "default":64, "isBipolar":true },
+ {"index":218, "name":"Slot9Destination" , "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":219, "name":"Slot10Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":220, "name":"Slot10Amount" , "default":64, "isBipolar":true },
+ {"index":221, "name":"Slot10Destination", "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":222, "name":"Slot11Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":223, "name":"Slot11Amount" , "default":64, "isBipolar":true },
+ {"index":224, "name":"Slot11Destination", "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":225, "name":"Slot12Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":226, "name":"Slot12Amount" , "default":64, "isBipolar":true },
+ {"index":227, "name":"Slot12Destination", "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":228, "name":"Slot13Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":229, "name":"Slot13Amount" , "default":64, "isBipolar":true },
+ {"index":230, "name":"Slot13Destination", "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":231, "name":"Slot14Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":232, "name":"Slot14Amount" , "default":64, "isBipolar":true },
+ {"index":233, "name":"Slot14Destination", "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":234, "name":"Slot15Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":235, "name":"Slot15Amount" , "default":64, "isBipolar":true },
+ {"index":236, "name":"Slot15Destination", "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+ {"index":237, "name":"Slot16Source" , "min":0, "max":31, "isDiscrete":true, "toText":"modSource"},
+ {"index":238, "name":"Slot16Amount" , "default":64, "isBipolar":true },
+ {"index":239, "name":"Slot16Destination", "min":0, "max":30, "isDiscrete":true, "toText":"modDest" },
+
+ // Sound Name
+ {"index":240, "name":"Name00", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":241, "name":"Name01", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":242, "name":"Name02", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":243, "name":"Name03", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":244, "name":"Name04", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":245, "name":"Name05", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":246, "name":"Name06", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":247, "name":"Name07", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":248, "name":"Name08", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":249, "name":"Name09", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":250, "name":"Name10", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":251, "name":"Name11", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":252, "name":"Name12", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":253, "name":"Name13", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":254, "name":"Name14", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+ {"index":255, "name":"Name15", "min":32, "max":127, "isDiscrete":true, "toText":"ascii"},
+
+ // GLOBAL
+ {"index":1000, "class":"Global", "name":"MainVolume"}
+ ],
+ "valuelists":
+ {
+ "unsignedZero":
+ [
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
+ "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
+ "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
+ "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
+ "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
+ "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114", "115", "116", "117", "118", "119",
+ "120", "121", "122", "123", "124", "125", "126", "127"
+ ],
+ "unsignedOne":
+ [
+ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
+ "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
+ "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
+ "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
+ "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
+ "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114", "115", "116", "117", "118", "119",
+ "120", "121", "122", "123", "124", "125", "126", "127", "128"
+ ],
+ "signed":
+ [
+ "-64", "-63", "-62", "-61",
+ "-60", "-59", "-58", "-57", "-56", "-55", "-54", "-53", "-52", "-51", "-50", "-49", "-48", "-47", "-46", "-45", "-44", "-43", "-42", "-41",
+ "-40", "-39", "-38", "-37", "-36", "-35", "-34", "-33", "-32", "-31", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21",
+ "-20", "-19", "-18", "-17", "-16", "-15", "-14", "-13", "-12", "-11", "-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1",
+ "0", "+1", "+2", "+3", "+4", "+5", "+6", "+7", "+8", "+9", "+10", "+11", "+12", "+13", "+14", "+15", "+16", "+17", "+18", "+19",
+ "+20", "+21", "+22", "+23", "+24", "+25", "+26", "+27", "+28", "+29", "+30", "+31", "+32", "+33", "+34", "+35", "+36", "+37", "+38", "+39",
+ "+40", "+41", "+42", "+43", "+44", "+45", "+46", "+47", "+48", "+49", "+50", "+51", "+52", "+53", "+54", "+55", "+56", "+57", "+58", "+59",
+ "+60", "+61", "+62", "+63"
+ ],
+ "ascii":
+ [
+ "NUL","SOH","STX","ETX","EOT","ENQ","ACK","BEL","BS","HT","LF","VT","FF","CR","SO","SI","DLE","DC1","DC2","DC3","DC4","NAK","SYN","ETB","CAN","EM","SUB","ESC","FS","GS","RS","US",
+ ", ","!","\"","#","$","%","&","'","(",")","*","+",",","-",".","/",
+ "0","1","2","3","4","5","6","7","8","9",
+ ":",";","<","=",">","?","@",
+ "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
+ "[","\\","]","^","_","`",
+ "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z",
+ "{","|","}","~","DEL"
+ ],
+ "pbrange":
+ [
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
+ "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
+ "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
+ "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
+ "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
+ "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114", "115", "116", "117", "118", "119",
+ "120", "harmonic", "global"
+ ],
+ "offOn":
+ [
+ "Off", "On"
+ ],
+ "waveType":
+ [
+ "Resonant", "Resonant 2", "Mallet Syn", "Square-Sweep", "Bellish", "Pulse-Sweep", "Saw-Sweep", "Mellow Saw",
+ "Feedback", "Add Harm", "Reso 3 HP", "Wind Syn", "High Harm", "Clipper", "Organ Syn", "Square Saw",
+ "Formant 1", "Polated", "Transient", "E Piano", "Robotic", "Strong Harm", "Perc Organ", "Clip Sweep",
+ "Reso Harms", "2 Echoes", "Formant 2", "Formant Vocal", "Micro Sync", "Micro PWM", "Glassy", "Square HP",
+ "Saw Sync 1", "Saw Sync 2", "Saw Sync 3", "Pul Sync 1", "Pul Sync 2", "Pul Sync 3", "Sin Sync 1", "Sin Sync 2",
+ "Sin Sync 3", "PWM Pulse", "PWM Saw", "Fuzz Wave", "Distorted", "Heavy Fuzz", "Fuzz Sync", "K+Strong 1",
+ "K+Strong 2", "K+Strong 3", "1-2-3-4-5", "19/twenty", "Wavetrip 1", "Wavetrip 2", "Wavetrip 3", "Wavetrip 4",
+ "Male Voice", "Low Piano", "Reso Sweep", "Xmas Bell", "FM Piano", "Fat Organ", "Vibes", "Chorus 2",
+ "True PWM",
+ "", "", "", "", "", "", "", "",
+ "", "", "", "", "", "", "", "",
+ "", "", "", "", "", "", "", "",
+ "", "", "", "", "", "", "",
+ "User 1", "User 2", "User 3", "User 4", "User 5", "User 6", "User 7", "User 8",
+ "User 9", "User 10", "User 11", "User 12", "User 13", "User 14", "User 15", "User 16",
+ "User 17", "User 18", "User 19", "User 20", "User 21", "User 22", "User 23", "User 24",
+ "User 25", "User 26", "User 27", "User 28", "User 29", "User 30", "User 31", "User 32"
+ ],
+ "waveStartWave":
+ [
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
+ "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
+ "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
+ "60", "Tri", "Sqr", "Saw"
+ ],
+ // TODO
+ "waveStartPhase":
+ [
+ "free", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
+ "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
+ "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
+ "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
+ "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
+ "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114", "115", "116", "117", "118", "119",
+ "120", "121", "122", "123", "124", "125", "126", "127"
+ ],
+ "aliasing":
+ [
+ "off", "1", "2", "3", "4", "5"
+ ],
+ "timeQuant":
+ [
+ "off", "1", "2", "3", "4", "5"
+ ],
+ "clipping":
+ [
+ "Saturate", "Overflow"
+ ],
+ "accuracy":
+ [
+ "Off", "On"
+ ],
+ "playParameter":
+ [
+ "Osc 1 Octave", "Osc 1 Semitone", "Osc 1 Detune", "Osc 1 Pitchbend", "Osc 1 Keytrack",
+ "Osc 2 Octave", "Osc 2 Semitone", "Osc 2 Detune", "Osc 2 Pitchbend", "Osc 2 Keytrack",
+ "Wavetable",
+ "Wave 1 Startwave", "Wave 1 Phase", "Wave 1 Env Amount", "Wave 1 Velo Amount", "Wave 1 Keytrack",
+ "Wave 2 Startwave", "Wave 2 Phase", "Wave 2 Env Amount", "Wave 2 Velo Amount", "Wave 2 Keytrack",
+ "Mix Wave 1", "Mix Wave 2", "Mix Ringmod", "Mix Noise",
+ "Aliasing", "Quantize", "Clipping",
+ "Filter 1 Cutoff", "Filter 1 Resonance", "Filter 1 Type", "Filter 1 Keytrack", "Filter 1 Env Amount", "Filter 1 Velo Amount",
+ "Filter 2 Cutoff", "Filter 2 Type", "Filter 2 Keytrack",
+ "Sound Volume", "Amp Envelope Velo Amount", "Amplifier Keytrack",
+ "Chorus",
+ "Panning",
+ "Pan Keytrack",
+ "Glide on/off", "Glide Type",
+ "Arpeggiator on/off/hold", "Arp Tempo", "Arp Clock", "Arp Range", "Arp Pattern", "Arp Direction", "Arp Note Order", "Arp Velocity",
+ "Allocation", "Assignment",
+ "Filter Env Attack", "Filter Env Decay", "Filter Env Sustain", "Filter Env Release",
+ "Amlifier Env Attack", "Amlifier Env Decay", "Amplifier Env Sustain", "Amplifier Env Release",
+ "LFO1 Rate", "LFO1 Shape", "LFO1 Delay", "LFO1 Sync", "LFO1 Symmetry", "LFO1 Humanize",
+ "LFO2 Rate", "LFO2 Shape", "LFO2 Delay", "LFO2 Sync", "LFO2 Symmetry", "LFO2 Humanize", "LFO2 Phase",
+ "Osc 1 FM Amount", "Filter 1 Special", "Glide Time",
+ "Control W", "Control X", "Control Y", "Control Z"
+ ],
+ "filter1Type":
+ [
+ "24 dB LP", "12 dB LP", "24 dB BP", "12 dB BP",
+ "12 dB HP", "Sine WS => 12 dB LP", "12 dB LP => WS", "12 dB LP/BP",
+ "12 dB LP FM", "12 dB LP S&H"
+ ],
+ "filter2Type":
+ [
+ "6 dB LP", "6 dB HP"
+ ],
+ "effectType":
+ [
+ "Off", "Chorus", "Flanger 1", "Flanger 2", "AutoWah LP", "AutoWah BP", "Overdrive", "Amp. Mod", "", "", "",
+ "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "Delay", "Pan Delay", "Mod Delay"
+ ],
+ "pan":
+ [
+ "Left", "-63", "-62", "-61",
+ "-60", "-59", "-58", "-57", "-56", "-55", "-54", "-53", "-52", "-51", "-50", "-49", "-48", "-47", "-46", "-45", "-44", "-43", "-42", "-41",
+ "-40", "-39", "-38", "-37", "-36", "-35", "-34", "-33", "-32", "-31", "-30", "-29", "-28", "-27", "-26", "-25", "-24", "-23", "-22", "-21",
+ "-20", "-19", "-18", "-17", "-16", "-15", "-14", "-13", "-12", "-11", "-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1",
+ "Center","+1", "+2", "+3", "+4", "+5", "+6", "+7", "+8", "+9", "+10", "+11", "+12", "+13", "+14", "+15", "+16", "+17", "+18", "+19",
+ "+20", "+21", "+22", "+23", "+24", "+25", "+26", "+27", "+28", "+29", "+30", "+31", "+32", "+33", "+34", "+35", "+36", "+37", "+38", "+39",
+ "+40", "+41", "+42", "+43", "+44", "+45", "+46", "+47", "+48", "+49", "+50", "+51", "+52", "+53", "+54", "+55", "+56", "+57", "+58", "+59",
+ "+60", "+61", "+62", "Right"
+ ],
+ "glideType":
+ [
+ "Portamento", "Glissando", "Fingered Porta", "Glissando", "Fingered Gliss."
+ ],
+ "glideMode":
+ [
+ "Exp", "Linear"
+ ],
+ "arpMode":
+ [
+ "Off", "On", "Hold"
+ ],
+ // TODO
+ "arpTempo":
+ [
+ "40", "42", "44", "46", "48",
+ "50", "52", "54", "56", "58",
+ "60", "62", "64", "66", "68",
+ "70", "72", "74", "76", "78",
+ "80", "82", "84", "86", "88",
+ "90",
+ "91", "92", "93", "94", "95", "96", "97", "98", "99",
+ "100", "101", "102", "103", "104", "105", "106", "107", "108", "109",
+ "110", "111", "112", "113", "114", "115", "116", "117", "118", "119",
+ "120", "121", "122", "123", "124", "125", "126", "127", "128", "129",
+ "130", "131", "132", "133", "134", "135", "136", "137", "138", "139",
+ "140", "141", "142", "143", "144", "145", "146", "147", "148", "149",
+ "150", "151", "152", "153", "154", "155", "156", "157", "158", "159",
+ "160", "161", "162", "163", "164",
+ "165", "170", "175", "180", "185", "190", "195",
+ "200", "205", "210", "215", "220", "225", "230", "235", "240", "245",
+ "250", "255", "260", "265", "270", "275", "280", "285", "290", "295",
+ "300"
+ ],
+ "arpClock":
+ [
+ "1/1", "1/2.", "1/2T", "1/2",
+ "1/4.", "1/4T", "1/4", "1/8.",
+ "1/8T", "1/8", "1/16.", "1/16T",
+ "1/16", "1/32.", "1/32T", "1/32"
+ ],
+ "arpPattern":
+ [
+ "Off", "User", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"
+ ],
+ "arpDirection":
+ [
+ "Up", "Down", "Alt", "Random"
+ ],
+ "arpNoteOrder":
+ [
+ "Note", "N. Reversed", "As Played", "P. Reversed"
+ ],
+ "arpVelocity":
+ [
+ "Root Note", "Last Note"
+ ],
+ "arpUserPatternBits":
+ [
+ "---", "---*", "--*-", "--**",
+ "-*-", "-*-*", "-**-", "-***",
+ "*--", "*--*", "*-*-", "*-**",
+ "**-", "**-*", "***-", "****"
+ ],
+ "allocationMode":
+ [
+ "Poly", "Mono"
+ ],
+ "assignment":
+ [
+ "Normal", "Dual", "Unisono"
+ ],
+ "envTrigger":
+ [
+ "Normal", "Single", "Retriger"
+ ],
+ "lfoShape":
+ [
+ "Sine", "Triangle", "Square", "Saw", "Random", "S&H"
+ ],
+ "lfoSync":
+ [
+ "Off", "On", "On", "Clock"
+ ],
+ // TODO
+ "lfoPhase":
+ [
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
+ "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
+ "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
+ "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
+ "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
+ "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", "110", "111", "112", "113", "114", "115", "116", "117", "118", "119",
+ "120", "121", "122", "123", "124", "125", "126", "127"
+ ],
+ "modSource":
+ [
+ "Off", "LFO1", "LFO1 * MW", "LFO1 * AT", "LFO2", "Filter Env", "Amp Env", "Wave Env", "Free Env", "Key Follow", "Keytrack", "Velocity", "Release Vel", "Aftertouch", "Poly Pressure", "Pitch Bend",
+ "Modwheel", "Sustain Control", "Foot Control", "Breath Control", "Control W", "Control X", "Control Y", "Control Z", "Control Delay", "Modifier #1", "Modifier #2", "Modifier #3", "Modifier #4",
+ "MIDI Clock", "min", "MAX"
+ ],
+ "modType":
+ [
+ "+", "-", "*", "/", "XOR", "OR", "AND", "S&H", "Ramp", "Switch", "Abs", "min", "MAX", "Lag", "Control Proc.", "Diff"
+ ],
+ "modDest":
+ [
+ "Pitch", "Osc 1 Pitch", "Osc 2 Pitch", "Wave 1 Pos", "Wave 2 Pos", "Mix Wave 1", "Mix Wave 2", "Mix Ring Mod", "Mix Noise", "Filter 1 Cutoff", "Filter 1 Resonance", "Filter 2 Cutoff",
+ "Volume", "Pan", "Filter Env Attack", "Filter Env Decay", "Filter Env Sustain", "Filter Env Release", "Amp Env Attack", "Amp Env Decay", "Amp Env Sustain", "Amp Env Release",
+ "Wave Env Times", "Wave Env Levels", "Free Env Times", "Free Env Levels", "LFO1 Rate", "LFO1 Level", "LFO2 Rate", "LFO2 Level", "Mod #1 Amount", "Mod #2 Amount", "Mod #3 Amount",
+ "Mod #4 Amount", "FM Amount", "F1 Extra"
+ ],
+ "midiChannel":
+ [
+ "Global", "Omni", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16"
+ ],
+ "output":
+ [
+ "Main", "Sub"
+ ],
+ "midiNote":
+ [
+ "C-2","C#-2","D-2","D#-2","E-2","F-2","F#-2","G-2","G#-2","A-2","A#-2","B-2",
+ "C-1","C#-1","D-1","D#-1","E-1","F-1","F#-1","G-1","G#-1","A-1","A#-1","B-1",
+ "C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
+ "C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
+ "C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
+ "C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
+ "C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
+ "C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
+ "C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
+ "C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
+ "C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8"
+ ],
+
+ // TODO
+ "keytrack76":
+ [
+ "-200%", "-197%", "-194%", "-191%", "-188%", "-185%", "-182%", "-179%",
+ "-175%", "-172%", "-169%", "-166%", "-163%", "-160%", "-157%", "-154%",
+ "-150%", "-147%", "-144%", "-141%", "-138%", "-135%", "-132%", "-129%",
+ "-125%", "-122%", "-119%", "-116%", "-113%", "-110%", "-107%", "-104%",
+ "-100%", "-97%", "-94%", "-91%", "-88%", "-85%", "-82%", "-79%",
+ "-75%", "-72%", "-69%", "-66%", "-63%", "-60%", "-57%", "-54%",
+ "-50%", "-47%", "-44%", "-41%", "-38%", "-35%", "-32%", "-29%",
+ "-25%", "-22%", "-19%", "-16%", "-13%", "-10%", "-7%", "-4%",
+ "0%", "+3%", "+6%", "+9%", "+12%", "+15%", "+18%", "+21%",
+ "+25%", "+28%", "+31%", "+34%", "+37%", "+40%", "+43%", "+46%",
+ "+50%", "+53%", "+56%", "+59%", "+62%", "+65%", "+68%", "+71%",
+ "+75%", "+78%", "+81%", "+84%", "+87%", "+90%", "+93%", "+96%",
+ "+100%", "+103%", "+106%", "+109%", "+112%", "+115%", "+118%", "+121%",
+ "+125%", "+128%", "+131%", "+134%", "+137%", "+140%", "+143%", "+146%",
+ "+150%", "+153%", "+156%", "+159%", "+162%", "+165%", "+168%", "+171%",
+ "+175%", "+178%", "+181%", "+184%", "+187%", "+190%", "+193%", "+197%"
+ ],
+ "keytrack127":
+ [
+ "-200%", "-197%", "-194%", "-191%", "-188%", "-185%", "-182%", "-179%",
+ "-175%", "-172%", "-169%", "-166%", "-163%", "-160%", "-157%", "-154%",
+ "-150%", "-147%", "-144%", "-141%", "-138%", "-135%", "-132%", "-129%",
+ "-125%", "-122%", "-119%", "-116%", "-113%", "-110%", "-107%", "-104%",
+ "-100%", "-97%", "-94%", "-91%", "-88%", "-85%", "-82%", "-79%",
+ "-75%", "-72%", "-69%", "-66%", "-63%", "-60%", "-57%", "-54%",
+ "-50%", "-47%", "-44%", "-41%", "-38%", "-35%", "-32%", "-29%",
+ "-25%", "-22%", "-19%", "-16%", "-13%", "-10%", "-7%", "-4%",
+ "0%", "+3%", "+6%", "+9%", "+12%", "+15%", "+18%", "+21%",
+ "+25%", "+28%", "+31%", "+34%", "+37%", "+40%", "+43%", "+46%",
+ "+50%", "+53%", "+56%", "+59%", "+62%", "+65%", "+68%", "+71%",
+ "+75%", "+78%", "+81%", "+84%", "+87%", "+90%", "+93%", "+96%",
+ "+100%", "+103%", "+106%", "+109%", "+112%", "+115%", "+118%", "+121%",
+ "+125%", "+128%", "+131%", "+134%", "+137%", "+140%", "+143%", "+146%",
+ "+150%", "+153%", "+156%", "+159%", "+162%", "+165%", "+168%", "+171%",
+ "+175%", "+178%", "+181%", "+184%", "+187%", "+190%", "+193%", "+197%"
+ ]
+ },
+ "midipackets":
+ {
+ "requestsingle": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "00"},
+ {"type": "bank"},
+ {"type": "program"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "requestmulti": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "01"},
+ {"type": "bank"},
+ {"type": "program"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "requestsinglebank": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "00"},
+ {"type": "byte", "value": "10"},
+ {"type": "bank"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "requestmultibank": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "01"},
+ {"type": "byte", "value": "10"},
+ {"type": "bank"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "requestglobal": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "04"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "requestmode": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "07"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "requestallsingles": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "00"},
+ {"type": "byte", "value": "10"},
+ {"type": "byte", "value": "00"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "singleparameterchange": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "20"},
+ {"type": "part"},
+ {"type": "page"},
+ {"type": "paramindex"},
+ {"type": "paramvalue"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "multiparameterchange": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "21"},
+ {"type": "page"},
+ {"type": "paramindex"},
+ {"type": "paramvalue"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "globalparameterchange": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "24"},
+ {"type": "page"},
+ {"type": "paramindex"},
+ {"type": "paramvalue"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "singledump": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "10"},
+ {"type": "bank"},
+ {"type": "program"},
+
+ {"type": "param", "name": "Version"}, // 0
+ {"type": "param", "name": "O1Octave"},
+ {"type": "param", "name": "O1Semi"},
+ {"type": "param", "name": "O1Detune"},
+ {"type": "null"},
+ {"type": "param", "name": "O1BendRange"},
+ {"type": "param", "name": "O1KeyTrack"},
+ {"type": "param", "name": "O1FmAmount"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "param", "name": "O2Octave"},
+ {"type": "param", "name": "O2Semi"},
+ {"type": "param", "name": "O2Detune"},
+ {"type": "null"},
+ {"type": "param", "name": "O2BendRange"},
+ {"type": "param", "name": "O2KeyTrack"},
+ {"type": "param", "name": "O2FmAmount"},
+ {"type": "param", "name": "O2Link"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "param", "name": "Wave"},
+ {"type": "param", "name": "W1StartW"},
+ {"type": "param", "name": "W1StartP"},
+ {"type": "param", "name": "W1EnvAmount"},
+ {"type": "param", "name": "W1EnvVelAmount"},
+ {"type": "param", "name": "W1Keytrack"},
+ {"type": "param", "name": "W1Limit"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "param", "name": "W2StartW"},
+ {"type": "param", "name": "W2StartP"},
+ {"type": "param", "name": "W2EnvAmount"},
+ {"type": "param", "name": "W2EnvVelAmount"},
+ {"type": "param", "name": "W2Keytrack"},
+ {"type": "param", "name": "W2Limit"},
+ {"type": "param", "name": "W2Link"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "param", "name": "MixW1"},
+ {"type": "param", "name": "MixW2"},
+ {"type": "param", "name": "MixRingMod"},
+ {"type": "param", "name": "MixNoise"},
+ {"type": "param", "name": "MixExternal"},
+ {"type": "null"},
+ {"type": "param", "name": "Aliasing"},
+ {"type": "param", "name": "TimeQuant"},
+ {"type": "param", "name": "Clipping"},
+ {"type": "null"},
+ {"type": "param", "name": "Accuracy"},
+ {"type": "param", "name": "PlayParamA"},
+ {"type": "param", "name": "PlayParamB"},
+ {"type": "param", "name": "PlayParamC"},
+ {"type": "param", "name": "PlayParamD"},
+ {"type": "param", "name": "F1Cutoff"},
+ {"type": "param", "name": "F1Resonance"},
+ {"type": "param", "name": "F1Type"},
+ {"type": "param", "name": "F1KeyTrack"},
+ {"type": "param", "name": "F1EnvAmount"},
+ {"type": "param", "name": "F1EnvVelAmount"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "param", "name": "F1ContextSensitive"},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "param", "name": "F2Cutoff"},
+ {"type": "param", "name": "F2Type"},
+ {"type": "param", "name": "F2KeyTrack"},
+ {"type": "param", "name": "EffectType"},
+ {"type": "param", "name": "AmpVolume"},
+ {"type": "null"},
+ {"type": "param", "name": "AmpVelocity"},
+ {"type": "param", "name": "AmpKeytrack"},
+ {"type": "param", "name": "EffectParamA"},
+ {"type": "param", "name": "ChorusEnabled"},
+ {"type": "param", "name": "EffectParamB"},
+ {"type": "param", "name": "Pan"},
+ {"type": "param", "name": "PanKeytrack"},
+ {"type": "param", "name": "EffectParamC"},
+ {"type": "param", "name": "GlideEnabled"},
+ {"type": "param", "name": "GlideType"},
+ {"type": "param", "name": "GlideMode"},
+ {"type": "param", "name": "GlideTime"},
+ {"type": "null"},
+ {"type": "param", "name": "ArpMode"},
+ {"type": "param", "name": "ArpTempo"},
+ {"type": "param", "name": "ArpClock"},
+ {"type": "param", "name": "ArpRange"},
+ {"type": "param", "name": "ArpPattern"},
+ {"type": "param", "name": "ArpDirection"},
+ {"type": "param", "name": "ArpNoteOrder"},
+ {"type": "param", "name": "ArpVelocity"},
+ {"type": "param", "name": "ArpReset"},
+ {"type": "param", "name": "ArpUserPatternLength"},
+ {"type": "param", "name": "ArpUserPattern1" , "mask":1, "shift":3},
+ {"type": "param", "name": "ArpUserPattern2" , "mask":1, "shift":2},
+ {"type": "param", "name": "ArpUserPattern3" , "mask":1, "shift":1},
+ {"type": "param", "name": "ArpUserPattern4" , "mask":1, "shift":0},
+ {"type": "param", "name": "ArpUserPattern5" , "mask":1, "shift":3},
+ {"type": "param", "name": "ArpUserPattern6" , "mask":1, "shift":2},
+ {"type": "param", "name": "ArpUserPattern7" , "mask":1, "shift":1},
+ {"type": "param", "name": "ArpUserPattern8" , "mask":1, "shift":0},
+ {"type": "param", "name": "ArpUserPattern9" , "mask":1, "shift":3},
+ {"type": "param", "name": "ArpUserPattern10", "mask":1, "shift":2},
+ {"type": "param", "name": "ArpUserPattern11", "mask":1, "shift":1},
+ {"type": "param", "name": "ArpUserPattern12", "mask":1, "shift":0},
+ {"type": "param", "name": "ArpUserPattern13", "mask":1, "shift":3},
+ {"type": "param", "name": "ArpUserPattern14", "mask":1, "shift":2},
+ {"type": "param", "name": "ArpUserPattern15", "mask":1, "shift":1},
+ {"type": "param", "name": "ArpUserPattern16", "mask":1, "shift":0},
+ {"type": "null"},
+ {"type": "null"},
+ {"type": "param", "name": "AllocationMode"},
+ {"type": "param", "name": "Assignment"},
+ {"type": "param", "name": "Detune"},
+ {"type": "null"},
+ {"type": "param", "name": "DePan"},
+ {"type": "param", "name": "F1EnvAttack"},
+ {"type": "param", "name": "F1EnvDecay"},
+ {"type": "param", "name": "F1EnvSustain"},
+ {"type": "param", "name": "F1EnvRelease"},
+ {"type": "param", "name": "F1EnvTrigger"},
+ {"type": "null"},
+ {"type": "param", "name": "AmpEnvAttack"},
+ {"type": "param", "name": "AmpEnvDecay"},
+ {"type": "param", "name": "AmpEnvSustain"},
+ {"type": "param", "name": "AmpEnvRelease"},
+ {"type": "param", "name": "AmpEnvTrigger"},
+ {"type": "null"},
+ {"type": "param", "name": "WaveEnvTime1"},
+ {"type": "param", "name": "WaveEnvLevel1"},
+ {"type": "param", "name": "WaveEnvTime2"},
+ {"type": "param", "name": "WaveEnvLevel2"},
+ {"type": "param", "name": "WaveEnvTime3"},
+ {"type": "param", "name": "WaveEnvLevel3"},
+ {"type": "param", "name": "WaveEnvTime4"},
+ {"type": "param", "name": "WaveEnvLevel4"},
+ {"type": "param", "name": "WaveEnvTime5"},
+ {"type": "param", "name": "WaveEnvLevel5"},
+ {"type": "param", "name": "WaveEnvTime6"},
+ {"type": "param", "name": "WaveEnvLevel6"},
+ {"type": "param", "name": "WaveEnvTime7"},
+ {"type": "param", "name": "WaveEnvLevel7"},
+ {"type": "param", "name": "WaveEnvTime8"},
+ {"type": "param", "name": "WaveEnvLevel8"},
+ {"type": "param", "name": "WaveEnvTrigger"},
+ {"type": "param", "name": "WaveKeyOnLoop"},
+ {"type": "param", "name": "WaveKeyOnLoopStart"},
+ {"type": "param", "name": "WaveKeyOnLoopEnd"},
+ {"type": "param", "name": "WaveKeyOffLoop"},
+ {"type": "param", "name": "WaveKeyOffLoopStart"},
+ {"type": "param", "name": "WaveKeyOffLoopEnd"},
+ {"type": "null"},
+ {"type": "param", "name": "FreeEnvTime1"},
+ {"type": "param", "name": "FreeEnvLevel1"},
+ {"type": "param", "name": "FreeEnvTime2"},
+ {"type": "param", "name": "FreeEnvLevel2"},
+ {"type": "param", "name": "FreeEnvTime3"},
+ {"type": "param", "name": "FreeEnvLevel3"},
+ {"type": "param", "name": "FreeEnvReleaseTime"},
+ {"type": "param", "name": "FreeEnvReleaseLevel"},
+ {"type": "param", "name": "FreeEnvTrigger"},
+ {"type": "null"},
+ {"type": "param", "name": "Lfo1Rate"},
+ {"type": "param", "name": "Lfo1Shape"},
+ {"type": "param", "name": "Lfo1Delay"},
+ {"type": "param", "name": "Lfo1Sync"},
+ {"type": "param", "name": "Lfo1Symmetry"},
+ {"type": "param", "name": "Lfo1Humanize"},
+ {"type": "null"},
+ {"type": "param", "name": "Lfo2Rate"},
+ {"type": "param", "name": "Lfo2Shape"},
+ {"type": "param", "name": "Lfo2Delay"},
+ {"type": "param", "name": "Lfo2Sync"},
+ {"type": "param", "name": "Lfo2Symmetry"},
+ {"type": "param", "name": "Lfo2Humanize"},
+ {"type": "param", "name": "Lfo2Phase"},
+ {"type": "null"},
+ {"type": "param", "name": "ModDelaySource"},
+ {"type": "param", "name": "ModDelayTime"},
+ {"type": "param", "name": "Mod1Source1"},
+ {"type": "param", "name": "Mod1Source2"},
+ {"type": "param", "name": "Mod1Type"},
+ {"type": "param", "name": "Mod1Parameter"},
+ {"type": "param", "name": "Mod2Source1"},
+ {"type": "param", "name": "Mod2Source2"},
+ {"type": "param", "name": "Mod2Type"},
+ {"type": "param", "name": "Mod2Parameter"},
+ {"type": "param", "name": "Mod3Source1"},
+ {"type": "param", "name": "Mod3Source2"},
+ {"type": "param", "name": "Mod3Type"},
+ {"type": "param", "name": "Mod3Parameter"},
+ {"type": "param", "name": "Mod4Source1"},
+ {"type": "param", "name": "Mod4Source2"},
+ {"type": "param", "name": "Mod4Type"},
+ {"type": "param", "name": "Mod4Parameter"},
+ {"type": "param", "name": "Slot1Source" },
+ {"type": "param", "name": "Slot1Amount" },
+ {"type": "param", "name": "Slot1Destination" },
+ {"type": "param", "name": "Slot2Source" },
+ {"type": "param", "name": "Slot2Amount" },
+ {"type": "param", "name": "Slot2Destination" },
+ {"type": "param", "name": "Slot3Source" },
+ {"type": "param", "name": "Slot3Amount" },
+ {"type": "param", "name": "Slot3Destination" },
+ {"type": "param", "name": "Slot4Source" },
+ {"type": "param", "name": "Slot4Amount" },
+ {"type": "param", "name": "Slot4Destination" },
+ {"type": "param", "name": "Slot5Source" },
+ {"type": "param", "name": "Slot5Amount" },
+ {"type": "param", "name": "Slot5Destination" },
+ {"type": "param", "name": "Slot6Source" },
+ {"type": "param", "name": "Slot6Amount" },
+ {"type": "param", "name": "Slot6Destination" },
+ {"type": "param", "name": "Slot7Source" },
+ {"type": "param", "name": "Slot7Amount" },
+ {"type": "param", "name": "Slot7Destination" },
+ {"type": "param", "name": "Slot8Source" },
+ {"type": "param", "name": "Slot8Amount" },
+ {"type": "param", "name": "Slot8Destination" },
+ {"type": "param", "name": "Slot9Source" },
+ {"type": "param", "name": "Slot9Amount" },
+ {"type": "param", "name": "Slot9Destination" },
+ {"type": "param", "name": "Slot10Source" },
+ {"type": "param", "name": "Slot10Amount" },
+ {"type": "param", "name": "Slot10Destination"},
+ {"type": "param", "name": "Slot11Source" },
+ {"type": "param", "name": "Slot11Amount" },
+ {"type": "param", "name": "Slot11Destination"},
+ {"type": "param", "name": "Slot12Source" },
+ {"type": "param", "name": "Slot12Amount" },
+ {"type": "param", "name": "Slot12Destination"},
+ {"type": "param", "name": "Slot13Source" },
+ {"type": "param", "name": "Slot13Amount" },
+ {"type": "param", "name": "Slot13Destination"},
+ {"type": "param", "name": "Slot14Source" },
+ {"type": "param", "name": "Slot14Amount" },
+ {"type": "param", "name": "Slot14Destination"},
+ {"type": "param", "name": "Slot15Source" },
+ {"type": "param", "name": "Slot15Amount" },
+ {"type": "param", "name": "Slot15Destination"},
+ {"type": "param", "name": "Slot16Source" },
+ {"type": "param", "name": "Slot16Amount" },
+ {"type": "param", "name": "Slot16Destination"},
+ {"type": "param", "name": "Name00"},
+ {"type": "param", "name": "Name01"},
+ {"type": "param", "name": "Name02"},
+ {"type": "param", "name": "Name03"},
+ {"type": "param", "name": "Name04"},
+ {"type": "param", "name": "Name05"},
+ {"type": "param", "name": "Name06"},
+ {"type": "param", "name": "Name07"},
+ {"type": "param", "name": "Name08"},
+ {"type": "param", "name": "Name09"},
+ {"type": "param", "name": "Name10"},
+ {"type": "param", "name": "Name11"},
+ {"type": "param", "name": "Name12"},
+ {"type": "param", "name": "Name13"},
+ {"type": "param", "name": "Name14"},
+ {"type": "param", "name": "Name15"},
+ {"type": "checksum", "first": 7, "last": 262},
+ {"type": "byte", "value": "f7"}
+ ],
+
+ "globaldump": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "14"},
+
+ {"type": "null"}, // Reserved0,
+ {"type": "null"}, // Version,
+ {"type": "null"}, // StartupSoundbank,
+ {"type": "null"}, // StartupSoundNum,
+ {"type": "null"}, // MidiChannel,
+ {"type": "null"}, // ProgramChangeMode,
+ {"type": "null"}, // DeviceId,
+ {"type": "null"}, // BendRange,
+ {"type": "null"}, // ControllerW,
+ {"type": "null"}, // ControllerX,
+ {"type": "null"}, // ControllerY,
+ {"type": "null"}, // ControllerZ,
+ {"type": "param", "name": "MainVolume"}, // MainVolume,
+ {"type": "null"}, // Reserved13,
+ {"type": "null"}, // Reserved14,
+ {"type": "null"}, // Transpose,
+ {"type": "null"}, // MasterTune,
+ {"type": "null"}, // DisplayTimeout,
+ {"type": "null"}, // LcdContrast,
+ {"type": "null"}, // Reserved19,
+ {"type": "null"}, // Reserved20,
+ {"type": "null"}, // Reserved21,
+ {"type": "null"}, // Reserved22,
+ {"type": "null"}, // StartupMultiNumber,
+ {"type": "null"}, // ArpNoteOutChannel,
+ {"type": "null"}, // MidiClockOutput,
+ {"type": "null"}, // ParameterSend,
+ {"type": "null"}, // ParameterReceive,
+ {"type": "null"}, // InputGain,
+ {"type": "null"}, // Reserved29,
+ {"type": "null"}, // Reserved30,
+ {"type": "null"}, // Reserved31
+
+ {"type": "checksum", "first": 5, "last": 37},
+ {"type": "byte", "value": "f7"}
+ ],
+
+ "modedump": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "17"},
+ {"type": "null"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "emuRequestLcd": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "60"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "emuRequestLeds": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "61"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "emuSendButton": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "62"},
+ {"type": "paramindex"},
+ {"type": "paramvalue"},
+ {"type": "byte", "value": "f7"}
+ ],
+ "emuSendRotary": [
+ {"type": "byte", "value": "f0"},
+ {"type": "byte", "value": "3e"},
+ {"type": "byte", "value": "0e"},
+ {"type": "deviceid"},
+ {"type": "byte", "value": "63"},
+ {"type": "paramindex"},
+ {"type": "paramvalue"},
+ {"type": "byte", "value": "f7"}
+ ]
+ }
+}
diff --git a/source/xtJucePlugin/skins/xtDefault/Digital.ttf b/source/xtJucePlugin/skins/xtDefault/Digital.ttf
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/alphadial.png b/source/xtJucePlugin/skins/xtDefault/alphadial.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/assets.cmake b/source/xtJucePlugin/skins/xtDefault/assets.cmake
@@ -0,0 +1,23 @@
+set(ASSETS_xtDefault
+ ${CMAKE_CURRENT_LIST_DIR}/fxAmpMod.png
+ ${CMAKE_CURRENT_LIST_DIR}/fxAutoWahLP.png
+ ${CMAKE_CURRENT_LIST_DIR}/fxChorus.png
+ ${CMAKE_CURRENT_LIST_DIR}/fxDelay.png
+ ${CMAKE_CURRENT_LIST_DIR}/fxFlanger2.png
+ ${CMAKE_CURRENT_LIST_DIR}/fxModDelay.png
+ ${CMAKE_CURRENT_LIST_DIR}/fxOff.png
+ ${CMAKE_CURRENT_LIST_DIR}/fxOverdrive.png
+ ${CMAKE_CURRENT_LIST_DIR}/led.png
+ ${CMAKE_CURRENT_LIST_DIR}/xt_encoder_ranged.png
+ ${CMAKE_CURRENT_LIST_DIR}/xt_encoder_ranged_red.png
+ ${CMAKE_CURRENT_LIST_DIR}/xtDefaultBG.png
+ ${CMAKE_CURRENT_LIST_DIR}/xtknob.png
+ ${CMAKE_CURRENT_LIST_DIR}/xtknob_small.png
+ ${CMAKE_CURRENT_LIST_DIR}/xtPageArp.png
+ ${CMAKE_CURRENT_LIST_DIR}/xtPageMod.png
+ ${CMAKE_CURRENT_LIST_DIR}/xtPageOsc.png
+ ${CMAKE_CURRENT_LIST_DIR}/xtPagePatchManager.png
+ ${CMAKE_CURRENT_LIST_DIR}/xtPageWave.png
+
+ ${CMAKE_CURRENT_LIST_DIR}/xtDefault.json
+)
diff --git a/source/xtJucePlugin/skins/xtDefault/fxAmpMod.png b/source/xtJucePlugin/skins/xtDefault/fxAmpMod.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/fxAutoWahLP.png b/source/xtJucePlugin/skins/xtDefault/fxAutoWahLP.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/fxChorus.png b/source/xtJucePlugin/skins/xtDefault/fxChorus.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/fxDelay.png b/source/xtJucePlugin/skins/xtDefault/fxDelay.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/fxFlanger2.png b/source/xtJucePlugin/skins/xtDefault/fxFlanger2.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/fxModDelay.png b/source/xtJucePlugin/skins/xtDefault/fxModDelay.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/fxOff.png b/source/xtJucePlugin/skins/xtDefault/fxOff.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/fxOverdrive.png b/source/xtJucePlugin/skins/xtDefault/fxOverdrive.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/led.png b/source/xtJucePlugin/skins/xtDefault/led.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xtDefault.json b/source/xtJucePlugin/skins/xtDefault/xtDefault.json
@@ -0,0 +1,6053 @@
+{
+ "name" : "Root",
+ "root" : {
+ "x" : "0",
+ "y" : "0",
+ "width" : "3400",
+ "height" : "2000",
+ "scale" : "0.5"
+ },
+ "tabgroup" : {
+ "name" : "Root",
+ "buttons" : [
+ "pageButtonMain",
+ "pageButtonPatchManager",
+ "pageButtonMod",
+ "pageButtonArp",
+ "pageButtonWave"
+ ],
+ "pages" : [
+ "pageOsc",
+ "pagePatchManager",
+ "pageMod",
+ "pageArp",
+ "pageWave"
+ ]
+ },
+ "children" : [
+ {
+ "name" : "bakedBG",
+ "image" : {
+ "x" : "0",
+ "y" : "0",
+ "width" : "3400",
+ "height" : "2000",
+ "texture" : "xtDefaultBG"
+ }
+ },
+ {
+ "name" : "lcdArea",
+ "component" : {
+ "x" : "196.0613",
+ "y" : "168.1559",
+ "width" : "1273.877",
+ "height" : "98.43307"
+ },
+ "componentProperties" : {
+ "lcdBackgroundColor" : "248727",
+ "lcdTextColor" : "000000"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "310.85",
+ "y" : "285.7459",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1Semi"
+ }
+ },
+ {
+ "name" : "xtKnob (1)",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "614.95",
+ "y" : "303.7759",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1Semi"
+ }
+ },
+ {
+ "name" : "xtKnob (2)",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "919.05",
+ "y" : "303.7759",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1Semi"
+ }
+ },
+ {
+ "name" : "xtKnob (3)",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1223.15",
+ "y" : "285.7459",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1Semi"
+ }
+ },
+ {
+ "name" : "midiLed",
+ "button" : {
+ "normalImage" : "0",
+ "overImage" : "1",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "113.3613",
+ "y" : "199.3724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "pageButtonPatchManager",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2320.399",
+ "y" : "69.64844",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "pageButtonWave",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2522.399",
+ "y" : "69.64844",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "pageButtonMain",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2824.109",
+ "y" : "69.64844",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "pageButtonMod",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3026.109",
+ "y" : "69.64844",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "pageButtonArp",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3228.109",
+ "y" : "69.64844",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "PartButtonSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2471.35",
+ "y" : "328.1724",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ }
+ },
+ {
+ "name" : "PartLedSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2487.35",
+ "y" : "393.1724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "PartButtonSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2583.85",
+ "y" : "328.1724",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ }
+ },
+ {
+ "name" : "PartLedSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2599.85",
+ "y" : "393.1724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "PartButtonSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2696.35",
+ "y" : "328.1724",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ }
+ },
+ {
+ "name" : "PartLedSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2712.35",
+ "y" : "393.1724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "PartButtonSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2808.85",
+ "y" : "328.1724",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ }
+ },
+ {
+ "name" : "PartLedSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2824.85",
+ "y" : "393.1724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "PartButtonSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2921.35",
+ "y" : "328.1724",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ }
+ },
+ {
+ "name" : "PartLedSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2937.35",
+ "y" : "393.1724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "PartButtonSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3033.85",
+ "y" : "328.1724",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ }
+ },
+ {
+ "name" : "PartLedSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3049.85",
+ "y" : "393.1724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "PartButtonSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3146.35",
+ "y" : "328.1724",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ }
+ },
+ {
+ "name" : "PartLedSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3162.35",
+ "y" : "393.1724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "PartButtonSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3258.85",
+ "y" : "328.1724",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ }
+ },
+ {
+ "name" : "PartLedSmall",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3274.85",
+ "y" : "393.1724",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ }
+ },
+ {
+ "name" : "pagePatchManager",
+ "image" : {
+ "x" : "35.99988",
+ "y" : "489",
+ "width" : "3322.7",
+ "height" : "1479.802",
+ "texture" : "xtPagePatchManager"
+ },
+ "children" : [
+ {
+ "name" : "ContainerPatchManager",
+ "component" : {
+ "x" : "0",
+ "y" : "48",
+ "width" : "3322.7",
+ "height" : "1431.802"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "pageOsc",
+ "image" : {
+ "x" : "35.99988",
+ "y" : "489",
+ "width" : "3322.7",
+ "height" : "1479.802",
+ "texture" : "xtPageOsc"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "105.8624",
+ "y" : "81.31683",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1Octave"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "328.8624",
+ "y" : "81.31683",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1Semi"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "105.8624",
+ "y" : "311.9504",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1Detune"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "328.8624",
+ "y" : "311.9504",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1FmAmount"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "105.8624",
+ "y" : "542.5841",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1KeyTrack"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "328.8624",
+ "y" : "542.5841",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O1BendRange"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "105.8624",
+ "y" : "821.2178",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O2Octave"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "328.8624",
+ "y" : "821.2178",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O2Semi"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "105.8624",
+ "y" : "1051.851",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O2Detune"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "328.8624",
+ "y" : "1051.851",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O2FmAmount"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "38.10242",
+ "y" : "1282.485",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O2KeyTrack"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "217.3624",
+ "y" : "1282.485",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O2BendRange"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "396.6224",
+ "y" : "1282.485",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O2Link"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "508.6224",
+ "y" : "1266.485",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "O2Link"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "527.0599",
+ "y" : "685.5664",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged_red",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Wave"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "679.2058",
+ "y" : "81.31683",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W1StartW"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "902.2058",
+ "y" : "81.31683",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W1StartP"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "728.2058",
+ "y" : "311.9504",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W1EnvAmount"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "951.2058",
+ "y" : "311.9504",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W1EnvVelAmount"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "756.7058",
+ "y" : "542.5841",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W1Keytrack"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "979.7058",
+ "y" : "542.5841",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W1Limit"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1091.706",
+ "y" : "526.5841",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W1Limit"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "756.7058",
+ "y" : "821.2178",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2StartW"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "979.7058",
+ "y" : "821.2178",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2StartP"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "728.2058",
+ "y" : "1051.851",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2EnvAmount"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "951.2058",
+ "y" : "1051.851",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2EnvVelAmount"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "651.4058",
+ "y" : "1282.485",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2Keytrack"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "828.2058",
+ "y" : "1282.485",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2Limit"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "940.2058",
+ "y" : "1266.485",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2Limit"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1005.006",
+ "y" : "1282.485",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2Link"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1117.006",
+ "y" : "1266.485",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "W2Link"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1312.489",
+ "y" : "97.48016",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "MixW1"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1350.389",
+ "y" : "298.4406",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "MixRingMod"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1371.789",
+ "y" : "499.4009",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "MixW2"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1381.999",
+ "y" : "700.3613",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "MixNoise"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1370.499",
+ "y" : "901.3215",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "MixExternal"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1674.036",
+ "y" : "99",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpVolume"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1674.036",
+ "y" : "299.29",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpVelocity"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1674.036",
+ "y" : "499.58",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpKeytrack"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1674.036",
+ "y" : "699.87",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Pan"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1674.036",
+ "y" : "900.1599",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "PanKeytrack"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Normal",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1696.973",
+ "y" : "1170.608",
+ "width" : "225",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpEnvTrigger"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1285.321",
+ "y" : "1281.715",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpEnvAttack"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1517.089",
+ "y" : "1281.715",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpEnvDecay"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1748.857",
+ "y" : "1281.715",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpEnvSustain"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1980.625",
+ "y" : "1281.715",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AmpEnvRelease"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1994.088",
+ "y" : "89",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Aliasing"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1994.088",
+ "y" : "358.5",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "TimeQuant"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1994.088",
+ "y" : "628",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Accuracy"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2106.087",
+ "y" : "612",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Accuracy"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1994.088",
+ "y" : "897.5",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Clipping"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2106.087",
+ "y" : "881.5",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Clipping"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "24dB LP",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2563.788",
+ "y" : "92.5",
+ "width" : "302.83",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1Type"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "6dB LP",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3124.847",
+ "y" : "92.5",
+ "width" : "170.6693",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F2Type"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2279.637",
+ "y" : "172.23",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1Cutoff"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2487.737",
+ "y" : "172.23",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1Resonance"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2695.837",
+ "y" : "172.23",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1KeyTrack"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2279.637",
+ "y" : "370.17",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1EnvAmount"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2487.737",
+ "y" : "370.17",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1EnvVelAmount"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2695.837",
+ "y" : "370.17",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1ContextSensitive"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3060.847",
+ "y" : "172.23",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F2Cutoff"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3060.847",
+ "y" : "370.17",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F2KeyTrack"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Normal",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2760.842",
+ "y" : "666.4",
+ "width" : "225",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1EnvTrigger"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2275.417",
+ "y" : "749.8726",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1EnvAttack"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2556.367",
+ "y" : "749.8726",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1EnvDecay"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2837.317",
+ "y" : "749.8726",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1EnvSustain"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3118.268",
+ "y" : "749.8726",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1EnvRelease"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Saw",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2479.896",
+ "y" : "1035.41",
+ "width" : "225",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo1Shape"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "On",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2559.695",
+ "y" : "1330.06",
+ "width" : "145.2011",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo1Sync"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2248.596",
+ "y" : "1109.255",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo1Symmetry"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2415.896",
+ "y" : "1109.255",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo1Rate"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2583.196",
+ "y" : "1109.255",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo1Delay"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2248.596",
+ "y" : "1285.955",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo1Humanize"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Saw",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3041.796",
+ "y" : "1035.41",
+ "width" : "225",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo2Shape"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Clock",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3121.595",
+ "y" : "1330.06",
+ "width" : "145.2011",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo2Sync"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2810.496",
+ "y" : "1109.255",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo2Symmetry"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2977.796",
+ "y" : "1109.255",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo2Rate"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3145.096",
+ "y" : "1109.255",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo2Delay"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2810.496",
+ "y" : "1285.955",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Lfo2Humanize"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "pageMod",
+ "image" : {
+ "x" : "35.99988",
+ "y" : "489",
+ "width" : "3322.7",
+ "height" : "1479.802",
+ "texture" : "xtPageMod"
+ },
+ "children" : [
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "170.7272",
+ "y" : "84.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot1Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "411.818",
+ "y" : "45.25",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot1Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "710.5452",
+ "y" : "84.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot1Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "170.7272",
+ "y" : "212.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot2Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "411.818",
+ "y" : "173.75",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot2Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "710.5452",
+ "y" : "212.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot2Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "170.7272",
+ "y" : "341.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot3Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "411.818",
+ "y" : "302.25",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot3Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "710.5452",
+ "y" : "341.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot3Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "170.7272",
+ "y" : "469.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot4Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "411.818",
+ "y" : "430.75",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot4Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "710.5452",
+ "y" : "469.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot4Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "170.7272",
+ "y" : "598.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot5Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "411.818",
+ "y" : "559.25",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot5Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "710.5452",
+ "y" : "598.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot5Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "170.7272",
+ "y" : "726.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot6Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "411.818",
+ "y" : "687.75",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot6Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "710.5452",
+ "y" : "726.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot6Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "170.7272",
+ "y" : "855.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot7Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "411.818",
+ "y" : "816.25",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot7Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "710.5452",
+ "y" : "855.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot7Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "170.7272",
+ "y" : "983.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot8Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "411.818",
+ "y" : "944.75",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot8Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "710.5452",
+ "y" : "983.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot8Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1144.173",
+ "y" : "84.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot9Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1385.264",
+ "y" : "45.25",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot9Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1683.991",
+ "y" : "84.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot9Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1144.173",
+ "y" : "212.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot10Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1385.264",
+ "y" : "173.75",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot10Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1683.991",
+ "y" : "212.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot10Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1144.173",
+ "y" : "341.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot11Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1385.264",
+ "y" : "302.25",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot11Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1683.991",
+ "y" : "341.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot11Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1144.173",
+ "y" : "469.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot12Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1385.264",
+ "y" : "430.75",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot12Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1683.991",
+ "y" : "469.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot12Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1144.173",
+ "y" : "598.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot13Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1385.264",
+ "y" : "559.25",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot13Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1683.991",
+ "y" : "598.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot13Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1144.173",
+ "y" : "726.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot14Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1385.264",
+ "y" : "687.75",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot14Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1683.991",
+ "y" : "726.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot14Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1144.173",
+ "y" : "855.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot15Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1385.264",
+ "y" : "816.25",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot15Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1683.991",
+ "y" : "855.25",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot15Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1144.173",
+ "y" : "983.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot16Source"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1385.264",
+ "y" : "944.75",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot16Amount"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1683.991",
+ "y" : "983.75",
+ "width" : "211.0908",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Slot16Destination"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "normal",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "27.08069",
+ "y" : "1315.668",
+ "width" : "223.42",
+ "height" : "46"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTrigger"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "322.3525",
+ "y" : "1151.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTime1"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "322.3525",
+ "y" : "1315.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvLevel1"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "539.895",
+ "y" : "1151.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTime2"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "539.895",
+ "y" : "1315.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvLevel2"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "757.4373",
+ "y" : "1151.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTime3"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "757.4373",
+ "y" : "1315.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvLevel3"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "974.9797",
+ "y" : "1151.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTime4"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "974.9797",
+ "y" : "1315.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvLevel4"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1192.522",
+ "y" : "1151.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTime5"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1192.522",
+ "y" : "1315.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvLevel5"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1410.064",
+ "y" : "1151.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTime6"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1410.064",
+ "y" : "1315.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvLevel6"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1627.607",
+ "y" : "1151.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTime7"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1627.607",
+ "y" : "1315.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvLevel7"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1845.149",
+ "y" : "1151.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvTime8"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "1845.149",
+ "y" : "1315.668",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveEnvLevel8"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2183.501",
+ "y" : "1151.534",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveKeyOnLoop"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2295.501",
+ "y" : "1135.534",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveKeyOnLoop"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "1",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2247.501",
+ "y" : "1326.935",
+ "width" : "140.039",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveKeyOnLoopStart"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "8",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2247.501",
+ "y" : "1389.955",
+ "width" : "140.039",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveKeyOnLoopEnd"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2518.58",
+ "y" : "1151.534",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveKeyOffLoop"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2630.58",
+ "y" : "1135.534",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveKeyOffLoop"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "1",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2582.58",
+ "y" : "1326.935",
+ "width" : "140.039",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveKeyOffLoopStart"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "8",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2582.58",
+ "y" : "1389.955",
+ "width" : "140.039",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "WaveKeyOffLoopEnd"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "127",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2382.712",
+ "y" : "84.09998",
+ "width" : "255",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvTrigger"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2031.739",
+ "y" : "199.2304",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvTime1"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2031.739",
+ "y" : "388.2304",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvLevel1"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2223.054",
+ "y" : "199.2304",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvTime2"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2223.054",
+ "y" : "388.2304",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvLevel2"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2414.369",
+ "y" : "199.2304",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvTime3"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2414.369",
+ "y" : "388.2304",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvLevel3"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2605.685",
+ "y" : "199.2304",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvReleaseTime"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2605.685",
+ "y" : "388.2304",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "FreeEnvReleaseLevel"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "LFO 2",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2382.712",
+ "y" : "727.7072",
+ "width" : "235",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ModDelaySource"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2318.712",
+ "y" : "860.4197",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ModDelayTime"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Mod Wheel",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3081.522",
+ "y" : "84.21252",
+ "width" : "221.18",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod1Source1"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "LFO 1",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3081.522",
+ "y" : "172.6375",
+ "width" : "221.18",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod1Source2"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "+",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2864.342",
+ "y" : "330.275",
+ "width" : "284.36",
+ "height" : "46"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod1Type"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3174.702",
+ "y" : "266.275",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod1Parameter"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Mod Wheel",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3081.522",
+ "y" : "437.9125",
+ "width" : "221.18",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod2Source1"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "LFO 1",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3081.522",
+ "y" : "526.3375",
+ "width" : "221.18",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod2Source2"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "+",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2864.342",
+ "y" : "683.9751",
+ "width" : "284.36",
+ "height" : "46"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod2Type"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3174.702",
+ "y" : "619.9751",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod2Parameter"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Mod Wheel",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3081.522",
+ "y" : "791.6125",
+ "width" : "221.18",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod3Source1"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "LFO 1",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3081.522",
+ "y" : "880.0375",
+ "width" : "221.18",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod3Source2"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "+",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2864.342",
+ "y" : "1037.675",
+ "width" : "284.36",
+ "height" : "46"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod3Type"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3174.702",
+ "y" : "973.675",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod3Parameter"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Mod Wheel",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3081.522",
+ "y" : "1145.313",
+ "width" : "221.18",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod4Source1"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "LFO 1",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "3081.522",
+ "y" : "1233.738",
+ "width" : "221.18",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod4Source2"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "+",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2864.342",
+ "y" : "1391.375",
+ "width" : "284.36",
+ "height" : "46"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod4Type"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3174.702",
+ "y" : "1327.375",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Mod4Parameter"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "pageArp",
+ "image" : {
+ "x" : "35.99988",
+ "y" : "489",
+ "width" : "3322.7",
+ "height" : "1479.802",
+ "texture" : "xtPageArp"
+ },
+ "children" : [
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Off",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "172.7548",
+ "y" : "136.2653",
+ "width" : "172.7547",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpMode"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "4/4",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "588.2643",
+ "y" : "136.2653",
+ "width" : "172.7547",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpClock"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "13",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1003.774",
+ "y" : "136.2653",
+ "width" : "172.7547",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpPattern"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Up",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1419.283",
+ "y" : "136.2653",
+ "width" : "172.7547",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpDirection"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "As played",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1834.793",
+ "y" : "136.2653",
+ "width" : "172.7547",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpNoteOrder"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Last Note",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2250.302",
+ "y" : "136.2653",
+ "width" : "172.7547",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpVelocity"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2509.057",
+ "y" : "96.79999",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpTempo"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2808.97",
+ "y" : "96.79999",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpRange"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3108.883",
+ "y" : "96.79999",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpReset"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "3220.883",
+ "y" : "80.79999",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpReset"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "31.99988",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern1"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "47.99988",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern1"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "219.1749",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern2"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "235.1749",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern2"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "406.3499",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern3"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "422.3499",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern3"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "593.5249",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern4"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "609.5249",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern4"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "780.6999",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern5"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "796.6999",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern5"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "967.8749",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern6"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "983.8749",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern6"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1155.05",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern7"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1171.05",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern7"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1342.225",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern8"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1358.225",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern8"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1529.4",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern9"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1545.4",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern9"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1716.575",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern10"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1732.575",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern10"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1903.75",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern11"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1919.75",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern11"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2090.925",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern12"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2106.925",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern12"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2278.1",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern13"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2294.1",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern13"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2465.275",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern14"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2481.275",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern14"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2652.45",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern15"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2668.45",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern15"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2839.625",
+ "y" : "397.9996",
+ "width" : "64",
+ "height" : "64",
+ "texture" : "xtknob_small",
+ "tileSizeX" : "64",
+ "tileSizeY" : "64"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern16"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "2855.625",
+ "y" : "462.9996",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPattern16"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3106.18",
+ "y" : "367.78",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ArpUserPatternLength"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Chorus",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "376.8201",
+ "y" : "695.6937",
+ "width" : "190",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectType"
+ }
+ },
+ {
+ "name" : "fxPageOff",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxOff"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "0"
+ }
+ },
+ {
+ "name" : "fxPageChorus",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxChorus"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "1"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxPageFlanger1",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxChorus"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "2"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxPageFlanger2",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxFlanger2"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "3"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxAutoWahLP",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxAutoWahLP"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "4"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxAutoWahBP",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxAutoWahLP"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "5"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxOverdrive",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxOverdrive"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "6"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxAmpMod",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxAmpMod"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "7"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxDelay",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxDelay"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "32"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxPanDelay",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxDelay"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "33"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "fxModDelay",
+ "image" : {
+ "x" : "-0.002807617",
+ "y" : "768.3688",
+ "width" : "746.4088",
+ "height" : "177.2867",
+ "texture" : "fxModDelay"
+ },
+ "condition" : {
+ "enableOnParameter" : "EffectType",
+ "enableOnValues" : "34"
+ },
+ "children" : [
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "60.40149",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamA"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "309.2045",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamB"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "558.0074",
+ "y" : "6.643311",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "EffectParamC"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "940.9567",
+ "y" : "777.9741",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ChorusEnabled"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1052.957",
+ "y" : "761.9741",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "ChorusEnabled"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Portamento",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1547.522",
+ "y" : "692.5537",
+ "width" : "221.825",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "GlideType"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Linear",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2098.271",
+ "y" : "692.5537",
+ "width" : "221.825",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "GlideMode"
+ }
+ },
+ {
+ "name" : "xtButton",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "1",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1474.799",
+ "y" : "779.9337",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xtknob",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "GlideEnabled"
+ }
+ },
+ {
+ "name" : "xtLed",
+ "button" : {
+ "isToggle" : "1",
+ "normalImage" : "0",
+ "overImage" : "0",
+ "downImage" : "0",
+ "normalImageOn" : "1",
+ "overImageOn" : "1",
+ "downImageOn" : "1",
+ "x" : "1586.799",
+ "y" : "763.9337",
+ "width" : "32",
+ "height" : "32",
+ "texture" : "led",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "GlideEnabled"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2043.002",
+ "y" : "779.9337",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "GlideTime"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2560.017",
+ "y" : "779.9337",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Detune"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "3025.841",
+ "y" : "779.9337",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "DePan"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Chorus",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "307.861",
+ "y" : "1186.728",
+ "width" : "240",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "PlayParamA"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Chorus",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "847.861",
+ "y" : "1186.728",
+ "width" : "240",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "PlayParamB"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Chorus",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "307.861",
+ "y" : "1296.728",
+ "width" : "240",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "PlayParamC"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Chorus",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "847.861",
+ "y" : "1296.728",
+ "width" : "240",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "PlayParamD"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Soundblaster II",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1516.885",
+ "y" : "1186.728",
+ "width" : "280",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1Type"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "MPU-401",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "1516.885",
+ "y" : "1296.728",
+ "width" : "280",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "F1Type"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Poly",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2239.211",
+ "y" : "1186.728",
+ "width" : "280",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "AllocationMode"
+ }
+ },
+ {
+ "name" : "dropdown",
+ "combobox" : {
+ "offsetL" : "15",
+ "offsetT" : "1",
+ "offsetR" : "-15",
+ "offsetB" : "-1",
+ "text" : "Dual",
+ "textHeight" : "30",
+ "color" : "000000FF",
+ "alignH" : "L",
+ "alignV" : "C",
+ "bold" : "1",
+ "x" : "2239.211",
+ "y" : "1296.728",
+ "width" : "280",
+ "height" : "50"
+ },
+ "parameterAttachment" : {
+ "parameter" : "Assignment"
+ }
+ },
+ {
+ "name" : "xtKnob",
+ "rotary" : {
+ },
+ "spritesheet" : {
+ "x" : "2897.537",
+ "y" : "1191.188",
+ "width" : "128",
+ "height" : "128",
+ "texture" : "xt_encoder_ranged",
+ "tileSizeX" : "128",
+ "tileSizeY" : "128"
+ },
+ "parameterAttachment" : {
+ "parameter" : "MainVolume"
+ }
+ }
+ ]
+ },
+ {
+ "name" : "pageWave",
+ "image" : {
+ "x" : "35.99988",
+ "y" : "489",
+ "width" : "3322.7",
+ "height" : "1479.802",
+ "texture" : "xtPageWave"
+ }
+ },
+ {
+ "name" : "FocusedParameterTooltip",
+ "label" : {
+ "text" : "42",
+ "textHeight" : "40",
+ "color" : "099DB5FF",
+ "backgroundColor" : "000000FF",
+ "alignH" : "C",
+ "alignV" : "C",
+ "x" : "1385",
+ "y" : "1225.5",
+ "width" : "96",
+ "height" : "55"
+ },
+ "componentProperties" : {
+ "offsetY" : "25"
+ }
+ }
+ ]
+}
+\ No newline at end of file
diff --git a/source/xtJucePlugin/skins/xtDefault/xtDefaultBG.png b/source/xtJucePlugin/skins/xtDefault/xtDefaultBG.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xtPageArp.png b/source/xtJucePlugin/skins/xtDefault/xtPageArp.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xtPageMod.png b/source/xtJucePlugin/skins/xtDefault/xtPageMod.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xtPageOsc.png b/source/xtJucePlugin/skins/xtDefault/xtPageOsc.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xtPagePatchManager.png b/source/xtJucePlugin/skins/xtDefault/xtPagePatchManager.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xtPageWave.png b/source/xtJucePlugin/skins/xtDefault/xtPageWave.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xt_encoder_ranged.png b/source/xtJucePlugin/skins/xtDefault/xt_encoder_ranged.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xt_encoder_ranged_red.png b/source/xtJucePlugin/skins/xtDefault/xt_encoder_ranged_red.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xtknob.png b/source/xtJucePlugin/skins/xtDefault/xtknob.png
Binary files differ.
diff --git a/source/xtJucePlugin/skins/xtDefault/xtknob_small.png b/source/xtJucePlugin/skins/xtDefault/xtknob_small.png
Binary files differ.
diff --git a/source/xtJucePlugin/version.h.in b/source/xtJucePlugin/version.h.in
@@ -0,0 +1,4 @@
+#pragma once
+
+static constexpr const char* const g_pluginVersionString = "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@";
+static constexpr uint32_t g_pluginVersion = @PROJECT_VERSION_MAJOR@@PROJECT_VERSION_MINOR@@PROJECT_VERSION_PATCH@;
diff --git a/source/xtJucePlugin/xtController.cpp b/source/xtJucePlugin/xtController.cpp
@@ -0,0 +1,594 @@
+#include "xtController.h"
+
+#include <fstream>
+
+#include "BinaryData.h"
+#include "PluginProcessor.h"
+
+#include "../xtLib/xtState.h"
+
+#include "../synthLib/os.h"
+
+#include "dsp56kEmu/logging.h"
+
+#include "xtFrontPanel.h"
+
+constexpr const char* g_midiPacketNames[] =
+{
+ "requestsingle",
+ "requestmulti",
+ "requestsinglebank",
+ "requestmultibank",
+ "requestglobal",
+ "requestmode",
+ "requestallsingles",
+ "singleparameterchange",
+ "multiparameterchange",
+ "globalparameterchange",
+ "singledump",
+ "multidump",
+ "globaldump",
+ "modedump",
+ "emuRequestLcd",
+ "emuRequestLeds",
+ "emuSendButton",
+ "emuSendRotary"
+};
+
+static_assert(std::size(g_midiPacketNames) == static_cast<size_t>(Controller::MidiPacketType::Count));
+
+namespace
+{
+ const char* midiPacketName(Controller::MidiPacketType _type)
+ {
+ return g_midiPacketNames[static_cast<uint32_t>(_type)];
+ }
+}
+
+Controller::Controller(AudioPluginAudioProcessor& p, unsigned char _deviceId) : pluginLib::Controller(p, loadParameterDescriptions()), m_deviceId(_deviceId)
+{
+ registerParams(p);
+
+// sendSysEx(RequestAllSingles);
+ sendSysEx(RequestGlobal);
+ sendSysEx(RequestMode);
+// sendGlobalParameterChange(xtLib::GlobalParameter::SingleMultiMode, 1);
+
+ startTimer(50);
+
+ onPlayModeChanged.addListener(0, [this](bool multiMode)
+ {
+ requestAllPatches();
+ });
+}
+
+Controller::~Controller() = default;
+
+void Controller::sendSingle(const std::vector<uint8_t>& _sysex)
+{
+ sendSingle(_sysex, getCurrentPart());
+}
+
+void Controller::sendSingle(const std::vector<uint8_t>& _sysex, const uint8_t _part)
+{
+ auto data = _sysex;
+
+ data[wLib::IdxBuffer] = static_cast<uint8_t>(isMultiMode() ? xt::LocationH::SingleEditBufferMultiMode : xt::LocationH::SingleEditBufferSingleMode);
+ data[wLib::IdxLocation] = isMultiMode() ? _part : 0;
+ data[wLib::IdxDeviceId] = m_deviceId;
+
+ const auto* p = getMidiPacket(g_midiPacketNames[SingleDump]);
+
+ if (!p->updateChecksums(data))
+ return;
+
+ pluginLib::Controller::sendSysEx(data);
+ parseSysexMessage(data);
+}
+
+const char* findEmbeddedResource(const std::string& _filename, uint32_t& _size)
+{
+ for(size_t i=0; i<BinaryData::namedResourceListSize; ++i)
+ {
+ if (BinaryData::originalFilenames[i] != _filename)
+ continue;
+
+ int size = 0;
+ const auto res = BinaryData::getNamedResource(BinaryData::namedResourceList[i], size);
+ _size = static_cast<uint32_t>(size);
+ return res;
+ }
+ return nullptr;
+}
+
+std::string Controller::loadParameterDescriptions()
+{
+ const auto name = "parameterDescriptions_xt.json";
+ const auto path = synthLib::getModulePath() + name;
+
+ const std::ifstream f(path.c_str(), std::ios::in);
+ if(f.is_open())
+ {
+ std::stringstream buf;
+ buf << f.rdbuf();
+ return buf.str();
+ }
+
+ uint32_t size;
+ const auto res = findEmbeddedResource(name, size);
+ if(res)
+ return {res, size};
+ return {};
+}
+
+void Controller::timerCallback()
+{
+ std::vector<synthLib::SMidiEvent> events;
+ getPluginMidiOut(events);
+
+ for (const auto& e : events)
+ {
+ if(!e.sysex.empty())
+ parseSysexMessage(e.sysex);
+ }
+}
+
+void Controller::onStateLoaded()
+{
+}
+
+std::string Controller::getSingleName(const pluginLib::MidiPacket::ParamValues& _values) const
+{
+ std::string name;
+ for(uint32_t i=0; i<16; ++i)
+ {
+ char paramName[16];
+ (void)snprintf(paramName, sizeof(paramName), "Name%02u", i);
+ const auto idx = getParameterIndexByName(paramName);
+ if(idx == InvalidParameterIndex)
+ break;
+
+ const auto it = _values.find(std::make_pair(pluginLib::MidiPacket::AnyPart, idx));
+ if(it == _values.end())
+ break;
+
+ name += static_cast<char>(it->second);
+ }
+ return name;
+}
+
+std::string Controller::getSingleName(const pluginLib::MidiPacket::AnyPartParamValues& _values) const
+{
+ return getString(_values, "Name", 16);
+}
+
+std::string Controller::getString(const pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _prefix, const size_t _len) const
+{
+ std::string name;
+ for(uint32_t i=0; i<_len; ++i)
+ {
+ char paramName[64];
+ (void)snprintf(paramName, sizeof(paramName), "%s%02u", _prefix.c_str(), 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;
+}
+
+bool Controller::setSingleName(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _value) const
+{
+ return setString(_values, "Name", 16, _value);
+}
+
+void Controller::applyPatchParameters(const pluginLib::MidiPacket::ParamValues& _params, const uint8_t _part) const
+{
+ for (const auto& it : _params)
+ {
+ auto* p = getParameter(it.first.second, _part);
+ p->setValueFromSynth(it.second, true, pluginLib::Parameter::ChangedBy::PresetChange);
+
+ for (const auto& derivedParam : p->getDerivedParameters())
+ derivedParam->setValueFromSynth(it.second, true, pluginLib::Parameter::ChangedBy::PresetChange);
+ }
+}
+
+void Controller::parseSingle(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _params)
+{
+ Patch patch;
+ patch.data = _msg;
+ patch.name = getSingleName(_params);
+
+ const auto bank = _data.at(pluginLib::MidiDataType::Bank);
+ const auto prog = _data.at(pluginLib::MidiDataType::Program);
+
+ if(bank == static_cast<uint8_t>(xt::LocationH::SingleEditBufferSingleMode) && prog == 0)
+ {
+ m_singleEditBuffer = patch;
+
+ if(!isMultiMode())
+ applyPatchParameters(_params, 0);
+ }
+ else if(bank == static_cast<uint8_t>(xt::LocationH::SingleEditBufferMultiMode))
+ {
+ m_singleEditBuffers[prog] = patch;
+
+ if (isMultiMode())
+ applyPatchParameters(_params, prog);
+
+ // if we switched to multi, all singles have to be requested. However, we cannot send all requests at once (device will miss some)
+ // so we chain them one after the other
+ if(prog + 1 < m_singleEditBuffers.size())
+ requestSingle(xt::LocationH::SingleEditBufferMultiMode, prog + 1);
+ }
+}
+
+void Controller::parseMulti(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _params) const
+{
+ Patch patch;
+ patch.data = _msg;
+ patch.name = getSingleName(_params);
+
+ const auto bank = _data.at(pluginLib::MidiDataType::Bank);
+// const auto prog = _data.at(pluginLib::MidiDataType::Program);
+
+ if(bank == static_cast<uint8_t>(xt::LocationH::MultiDumpMultiEditBuffer))
+ {
+ applyPatchParameters(_params, 0);
+ }
+}
+
+void Controller::parseSysexMessage(const pluginLib::SysEx& _msg)
+{
+ if(_msg.size() >= 5)
+ {
+ switch (const auto cmd = static_cast<xt::SysexCommand>(_msg[4]))
+ {
+ case xt::SysexCommand::EmuRotaries:
+ case xt::SysexCommand::EmuButtons:
+ case xt::SysexCommand::EmuLCD:
+ case xt::SysexCommand::EmuLEDs:
+ if(m_frontPanel)
+ m_frontPanel->processSysex(_msg);
+ return;
+ default:
+ break;
+ }
+ }
+
+ LOG("Got sysex of size " << _msg.size());
+
+ std::string name;
+ pluginLib::MidiPacket::Data data;
+ pluginLib::MidiPacket::ParamValues parameterValues;
+
+ if(pluginLib::Controller::parseMidiPacket(name, data, parameterValues, _msg))
+ {
+ if(name == midiPacketName(SingleDump))
+ {
+ parseSingle(_msg, data, parameterValues);
+ }
+ else if (name == midiPacketName(MultiDump))
+ {
+ parseMulti(_msg, data, parameterValues);
+ }
+ else if(name == midiPacketName(GlobalDump))
+ {
+ memcpy(m_globalData.data(), &_msg[xt::IdxGlobalParamFirst], sizeof(m_globalData));
+ }
+ else if(name == midiPacketName(ModeDump))
+ {
+ const auto lastPlayMode = isMultiMode();
+ memcpy(m_modeData.data(), &_msg[xt::IdxModeParamFirst], sizeof(m_modeData));
+ const auto newPlayMode = isMultiMode();
+
+ if(lastPlayMode != newPlayMode)
+ onPlayModeChanged(newPlayMode);
+ else
+ requestAllPatches();
+ }
+ else if(name == midiPacketName(SingleParameterChange))
+ {
+ const auto page = data[pluginLib::MidiDataType::Page];
+ const auto index = data[pluginLib::MidiDataType::ParameterIndex];
+ const auto part = data[pluginLib::MidiDataType::Part];
+ const auto value = data[pluginLib::MidiDataType::ParameterValue];
+
+ auto& params = findSynthParam(part, page, index);
+
+ for (auto& param : params)
+ param->setValueFromSynth(value, true, pluginLib::Parameter::ChangedBy::ControlChange);
+
+ LOG("Single parameter " << static_cast<int>(index) << ", page " << static_cast<int>(page) << " for part " << static_cast<int>(part) << " changed to value " << static_cast<int>(value));
+ }
+ else if(name == midiPacketName(GlobalParameterChange))
+ {
+ const auto index = (static_cast<uint32_t>(data[pluginLib::MidiDataType::Page]) << 7) + static_cast<uint32_t>(data[pluginLib::MidiDataType::ParameterIndex]);
+ const auto value = data[pluginLib::MidiDataType::ParameterValue];
+
+ if(m_globalData[index] != value)
+ {
+ LOG("Global parameter " << index << " changed to value " << static_cast<int>(value));
+ m_globalData[index] = value;
+ }
+ }
+ else
+ {
+ LOG("Received unknown sysex of size " << _msg.size());
+ }
+ }
+}
+
+bool Controller::parseMidiPacket(MidiPacketType _type, pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _params, const pluginLib::SysEx& _sysex) const
+{
+ const auto* p = getMidiPacket(g_midiPacketNames[_type]);
+ assert(p && "midi packet not found");
+ return pluginLib::Controller::parseMidiPacket(*p, _data, _params, _sysex);
+}
+
+bool Controller::sendSysEx(MidiPacketType _type) const
+{
+ std::map<pluginLib::MidiDataType, uint8_t> params;
+ return sendSysEx(_type, params);
+}
+
+bool Controller::sendSysEx(const MidiPacketType _type, std::map<pluginLib::MidiDataType, uint8_t>& _params) const
+{
+ _params.insert(std::make_pair(pluginLib::MidiDataType::DeviceId, m_deviceId));
+ return pluginLib::Controller::sendSysEx(midiPacketName(_type), _params);
+}
+
+bool Controller::isMultiMode() const
+{
+ return m_modeData.front() != 0;
+}
+
+void Controller::setPlayMode(const bool _multiMode)
+{
+ if(isMultiMode() == _multiMode)
+ return;
+
+ m_modeData[0] = _multiMode ? 1 : 0;
+
+ sendModeDump();
+
+ onPlayModeChanged(_multiMode);
+}
+
+void Controller::selectNextPreset()
+{
+ selectPreset(+1);
+}
+
+void Controller::selectPrevPreset()
+{
+ selectPreset(-1);
+}
+
+std::vector<uint8_t> Controller::createSingleDump(const xt::LocationH _buffer, const uint8_t _location, const uint8_t _part) const
+{
+ pluginLib::MidiPacket::Data data;
+
+ data.insert(std::make_pair(pluginLib::MidiDataType::DeviceId, m_deviceId));
+ data.insert(std::make_pair(pluginLib::MidiDataType::Bank, static_cast<uint8_t>(_buffer)));
+ data.insert(std::make_pair(pluginLib::MidiDataType::Program, _location));
+
+ std::vector<uint8_t> dst;
+
+ if (!createMidiDataFromPacket(dst, midiPacketName(SingleDump), data, _part))
+ return {};
+
+ return dst;
+}
+
+std::vector<uint8_t> Controller::createSingleDump(xt::LocationH _buffer, const uint8_t _location, const pluginLib::MidiPacket::AnyPartParamValues& _values) const
+{
+ pluginLib::MidiPacket::Data data;
+
+ data.insert(std::make_pair(pluginLib::MidiDataType::DeviceId, m_deviceId));
+ data.insert(std::make_pair(pluginLib::MidiDataType::Bank, static_cast<uint8_t>(_buffer)));
+ data.insert(std::make_pair(pluginLib::MidiDataType::Program, _location));
+
+ std::vector<uint8_t> dst;
+
+ if (!createMidiDataFromPacket(dst, midiPacketName(SingleDump), data, _values))
+ return {};
+
+ return dst;
+}
+
+bool Controller::parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _paramValues, const std::vector<uint8_t>& _sysex) const
+{
+ return parseMidiPacket(SingleDump, _data, _paramValues, _sysex);
+}
+
+bool Controller::setString(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _prefix, size_t _len, const std::string& _value) const
+{
+ for(uint32_t i=0; i<_len && i <_value.size(); ++i)
+ {
+ char paramName[64];
+ (void)snprintf(paramName, sizeof(paramName), "%s%02u", _prefix.c_str(), i);
+
+ const auto idx = getParameterIndexByName(paramName);
+ if(idx == InvalidParameterIndex)
+ break;
+
+ _values[idx] = static_cast<uint8_t>(_value[i]);
+ }
+ return true;
+}
+
+void Controller::setFrontPanel(xtJucePlugin::FrontPanel* _frontPanel)
+{
+ m_frontPanel = _frontPanel;
+}
+
+void Controller::selectPreset(const int _offset)
+{
+ auto& current = isMultiMode() ? m_currentSingles[getCurrentPart()] : m_currentSingle;
+
+ int index = static_cast<int>(current) + _offset;
+
+ if (index < 0)
+ index += 300;
+
+ if (index >= 300)
+ index -= 300;
+
+ current = static_cast<uint32_t>(index);
+
+ const int single = index % 100;
+ const int bank = index / 100;
+
+ if (isMultiMode())
+ {
+ // TODO: modify multi
+ }
+ else
+ {
+ sendMidiEvent(synthLib::M_CONTROLCHANGE, synthLib::MC_BANKSELECTMSB, m_deviceId);
+ sendMidiEvent(synthLib::M_CONTROLCHANGE, synthLib::MC_BANKSELECTLSB, static_cast<uint8_t>(xt::LocationH::SingleBankA) + bank);
+ sendMidiEvent(synthLib::M_PROGRAMCHANGE, static_cast<uint8_t>(single), 0);
+/*
+ sendGlobalParameterChange(xt::GlobalParameter::InstrumentABankNumber, static_cast<uint8_t>(bank));
+ sendGlobalParameterChange(xt::GlobalParameter::InstrumentASingleNumber, static_cast<uint8_t>(single));
+*/ }
+}
+
+void Controller::sendParameterChange(const pluginLib::Parameter& _parameter, const uint8_t _value)
+{
+ const auto &desc = _parameter.getDescription();
+
+ std::map<pluginLib::MidiDataType, uint8_t> data;
+
+ if (desc.page >= 100)
+ {
+ assert(false && "unable to send multi parameter change");
+ /*
+ uint8_t v;
+
+ if (!combineParameterChange(v, g_midiPacketNames[MultiDump], _parameter, _value))
+ return;
+
+ uint32_t idx = desc.index;
+
+ if(desc.page > 100)
+ idx += (static_cast<uint32_t>(xt::MultiParameter::Inst1) - static_cast<uint32_t>(xt::MultiParameter::Inst0)) * (desc.page - 101);
+
+ data.insert(std::make_pair(pluginLib::MidiDataType::Part, _parameter.getPart()));
+ data.insert(std::make_pair(pluginLib::MidiDataType::Page, idx >> 7));
+ data.insert(std::make_pair(pluginLib::MidiDataType::ParameterIndex, idx & 0x7f));
+ data.insert(std::make_pair(pluginLib::MidiDataType::ParameterValue, v));
+
+ sendSysEx(MultiParameterChange, data);
+ */
+ return;
+ }
+
+ uint8_t v;
+ if (!combineParameterChange(v, g_midiPacketNames[SingleDump], _parameter, _value))
+ return;
+
+ data.insert(std::make_pair(pluginLib::MidiDataType::Part, _parameter.getPart()));
+ data.insert(std::make_pair(pluginLib::MidiDataType::Page, desc.page));
+ data.insert(std::make_pair(pluginLib::MidiDataType::ParameterIndex, desc.index));
+ data.insert(std::make_pair(pluginLib::MidiDataType::ParameterValue, v));
+
+ sendSysEx(SingleParameterChange, data);
+}
+
+bool Controller::sendGlobalParameterChange(xt::GlobalParameter _param, uint8_t _value)
+{
+ const auto index = static_cast<uint32_t>(_param);
+
+ if(m_globalData[index] == _value)
+ return true;
+
+ std::map<pluginLib::MidiDataType, uint8_t> data;
+
+ data.insert(std::make_pair(pluginLib::MidiDataType::Page, index >> 7 ));
+ data.insert(std::make_pair(pluginLib::MidiDataType::ParameterIndex, index & 0x7f ));
+ data.insert(std::make_pair(pluginLib::MidiDataType::ParameterValue, _value));
+
+ m_globalData[index] = _value;
+
+ return sendSysEx(GlobalParameterChange, data);
+}
+
+bool Controller::sendModeDump() const
+{
+ std::vector<uint8_t> sysex;
+ std::map<pluginLib::MidiDataType, uint8_t> data;
+ data.insert({pluginLib::MidiDataType::DeviceId, m_deviceId});
+ if(!createMidiDataFromPacket(sysex, midiPacketName(ModeDump), data, 0))
+ return false;
+ sysex[xt::IdxModeParamFirst] = m_modeData.front();
+ pluginLib::Controller::sendSysEx(sysex);
+ return true;
+}
+
+void Controller::requestSingle(xt::LocationH _buf, uint8_t _location) const
+{
+ std::map<pluginLib::MidiDataType, uint8_t> params;
+ params[pluginLib::MidiDataType::Bank] = static_cast<uint8_t>(_buf);
+ params[pluginLib::MidiDataType::Program] = _location;
+ sendSysEx(RequestSingle, params);
+}
+
+void Controller::requestMulti(xt::LocationH _buf, uint8_t _location) const
+{
+ std::map<pluginLib::MidiDataType, uint8_t> params;
+ params[pluginLib::MidiDataType::Bank] = static_cast<uint8_t>(_buf);
+ params[pluginLib::MidiDataType::Program] = _location;
+ sendSysEx(RequestMulti, params);
+}
+
+uint8_t Controller::getGlobalParam(xt::GlobalParameter _type) const
+{
+ return m_globalData[static_cast<uint32_t>(_type)];
+}
+
+bool Controller::isDerivedParameter(pluginLib::Parameter& _derived, pluginLib::Parameter& _base) const
+{
+ if(_derived.getDescription().page >= 100)
+ return false;
+
+ const auto& packetName = g_midiPacketNames[SingleDump];
+ const auto* packet = getMidiPacket(packetName);
+
+ if (!packet)
+ {
+ LOG("Failed to find midi packet " << packetName);
+ return true;
+ }
+
+ const auto* defA = packet->getDefinitionByParameterName(_derived.getDescription().name);
+ const auto* defB = packet->getDefinitionByParameterName(_base.getDescription().name);
+
+ if (!defA || !defB)
+ return true;
+
+ return defA->doMasksOverlap(*defB);
+}
+
+void Controller::requestAllPatches() const
+{
+ if (isMultiMode())
+ {
+ requestMulti(xt::LocationH::MultiDumpMultiEditBuffer, 0);
+
+ // the other singles 1-15 are requested one after the other after a single has been received
+ requestSingle(xt::LocationH::SingleEditBufferMultiMode, 0);
+ }
+ else
+ {
+ requestSingle(xt::LocationH::SingleEditBufferSingleMode, 0);
+ }
+}
diff --git a/source/xtJucePlugin/xtController.h b/source/xtJucePlugin/xtController.h
@@ -0,0 +1,119 @@
+#pragma once
+
+#include "../jucePluginLib/controller.h"
+
+#include "../jucePluginLib/event.h"
+
+namespace xtJucePlugin
+{
+ class FrontPanel;
+}
+
+namespace xt
+{
+ enum class GlobalParameter;
+ enum class LocationH : uint8_t;
+}
+
+class AudioPluginAudioProcessor;
+
+class Controller : public pluginLib::Controller, juce::Timer
+{
+public:
+ enum MidiPacketType
+ {
+ RequestSingle,
+ RequestMulti,
+ RequestSingleBank,
+ RequestMultiBank,
+ RequestGlobal,
+ RequestMode,
+ RequestAllSingles,
+ SingleParameterChange,
+ MultiParameterChange,
+ GlobalParameterChange,
+ SingleDump,
+ MultiDump,
+ GlobalDump,
+ ModeDump,
+ EmuRequestLcd,
+ EmuRequestLeds,
+ EmuSendButton,
+ EmuSendRotary,
+
+ Count
+ };
+
+ struct Patch
+ {
+ std::string name;
+ std::vector<uint8_t> data;
+ };
+
+ pluginLib::Event<bool> onPlayModeChanged;
+
+ Controller(AudioPluginAudioProcessor &, unsigned char _deviceId = 0);
+ ~Controller() override;
+
+ void sendSingle(const std::vector<uint8_t>& _sysex);
+ void sendSingle(const std::vector<uint8_t>& _sysex, uint8_t _part);
+
+ bool sendSysEx(MidiPacketType _type) const;
+ bool sendSysEx(MidiPacketType _type, std::map<pluginLib::MidiDataType, uint8_t>& _params) const;
+
+ bool isMultiMode() const;
+ void setPlayMode(bool _multiMode);
+
+ void selectNextPreset();
+ void selectPrevPreset();
+
+ std::vector<uint8_t> createSingleDump(xt::LocationH _buffer, uint8_t _location, uint8_t _part) const;
+ std::vector<uint8_t> createSingleDump(xt::LocationH _buffer, uint8_t _location, const pluginLib::MidiPacket::AnyPartParamValues& _values) const;
+ bool parseSingle(pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _paramValues, const std::vector<uint8_t>& _sysex) const;
+
+ std::string getSingleName(const pluginLib::MidiPacket::ParamValues& _values) const;
+ std::string getSingleName(const pluginLib::MidiPacket::AnyPartParamValues& _values) const;
+ std::string getString(const pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _prefix, size_t _len) const;
+
+ bool setSingleName(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _value) const;
+ bool setCategory(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _value) const;
+ bool setString(pluginLib::MidiPacket::AnyPartParamValues& _values, const std::string& _prefix, size_t _len, const std::string& _value) const;
+
+ void setFrontPanel(xtJucePlugin::FrontPanel* _frontPanel);
+
+private:
+ void selectPreset(int _offset);
+
+ static std::string loadParameterDescriptions();
+
+ void timerCallback() override;
+ void onStateLoaded() override;
+
+ void applyPatchParameters(const pluginLib::MidiPacket::ParamValues& _params, uint8_t _part) const;
+ void parseSingle(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _params);
+ void parseMulti(const pluginLib::SysEx& _msg, const pluginLib::MidiPacket::Data& _data, const pluginLib::MidiPacket::ParamValues& _params) const;
+ void parseSysexMessage(const pluginLib::SysEx&) override;
+ bool parseMidiPacket(MidiPacketType _type, pluginLib::MidiPacket::Data& _data, pluginLib::MidiPacket::AnyPartParamValues& _params, const pluginLib::SysEx& _sysex) const;
+
+ void sendParameterChange(const pluginLib::Parameter& _parameter, uint8_t _value) override;
+ bool sendGlobalParameterChange(xt::GlobalParameter _param, uint8_t _value);
+ bool sendModeDump() const;
+ void requestSingle(xt::LocationH _buf, uint8_t _location) const;
+ void requestMulti(xt::LocationH _buf, uint8_t _location) const;
+
+ uint8_t getGlobalParam(xt::GlobalParameter _type) const;
+
+ bool isDerivedParameter(pluginLib::Parameter& _derived, pluginLib::Parameter& _base) const override;
+
+ void requestAllPatches() const;
+
+ const uint8_t m_deviceId;
+
+ Patch m_singleEditBuffer;
+ std::array<Patch,16> m_singleEditBuffers;
+ std::array<uint8_t, 39> m_globalData{};
+ std::array<uint8_t, 1> m_modeData{};
+ std::array<uint32_t, 16> m_currentSingles{0};
+ uint32_t m_currentSingle = 0;
+ xtJucePlugin::FrontPanel* m_frontPanel = nullptr;
+};
diff --git a/source/xtJucePlugin/xtEditor.cpp b/source/xtJucePlugin/xtEditor.cpp
@@ -0,0 +1,84 @@
+#include "xtEditor.h"
+
+#include "BinaryData.h"
+#include "PluginProcessor.h"
+
+#include "xtController.h"
+#include "xtFrontPanel.h"
+#include "xtPatchManager.h"
+
+#include "../jucePluginEditorLib/focusedParameter.h"
+
+namespace xtJucePlugin
+{
+ Editor::Editor(jucePluginEditorLib::Processor& _processor, pluginLib::ParameterBinding& _binding, std::string _skinFolder, const std::string& _jsonFilename)
+ : jucePluginEditorLib::Editor(_processor, _binding, std::move(_skinFolder))
+ , m_controller(dynamic_cast<Controller&>(_processor.getController()))
+ {
+ create(_jsonFilename);
+
+ m_focusedParameter.reset(new jucePluginEditorLib::FocusedParameter(m_controller, _binding, *this));
+
+ m_frontPanel.reset(new FrontPanel(*this, m_controller));
+
+ addMouseListener(this, true);
+
+ {
+ const auto container = findComponent("ContainerPatchManager");
+ constexpr auto scale = 1.3f;
+ const float x = static_cast<float>(container->getX());
+ const float y = static_cast<float>(container->getY());
+ const float w = static_cast<float>(container->getWidth());
+ const float h = static_cast<float>(container->getHeight());
+ container->setTransform(juce::AffineTransform::scale(scale, scale));
+ container->setSize(static_cast<int>(w / scale),static_cast<int>(h / scale));
+ container->setTopLeftPosition(static_cast<int>(x / scale),static_cast<int>(y / scale));
+
+ const auto configOptions = getProcessor().getConfigOptions();
+ const auto dir = configOptions.getDefaultFile().getParentDirectory();
+
+ setPatchManager(new PatchManager(*this, container, dir));
+ }
+ }
+
+ Editor::~Editor()
+ {
+ m_frontPanel.reset();
+ }
+
+ const char* Editor::findEmbeddedResource(const std::string& _filename, uint32_t& _size)
+ {
+ for(size_t i=0; i<BinaryData::namedResourceListSize; ++i)
+ {
+ if (BinaryData::originalFilenames[i] != _filename)
+ continue;
+
+ int size = 0;
+ const auto res = BinaryData::getNamedResource(BinaryData::namedResourceList[i], size);
+ _size = static_cast<uint32_t>(size);
+ return res;
+ }
+ return nullptr;
+ }
+
+ const char* Editor::findResourceByFilename(const std::string& _filename, uint32_t& _size)
+ {
+ return findEmbeddedResource(_filename, _size);
+ }
+
+ std::pair<std::string, std::string> Editor::getDemoRestrictionText() const
+ {
+ return {"Vavra",
+ "Xenia runs in demo mode\n"
+ "\n"
+ "The following features are disabled:\n"
+ "- Saving/Exporting Presets\n"
+ "- Plugin state is not preserve"
+ };
+ }
+
+ void Editor::mouseEnter(const juce::MouseEvent& _event)
+ {
+ m_focusedParameter->onMouseEnter(_event);
+ }
+}
diff --git a/source/xtJucePlugin/xtEditor.h b/source/xtJucePlugin/xtEditor.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "../jucePluginEditorLib/pluginEditor.h"
+
+class Controller;
+
+namespace jucePluginEditorLib
+{
+ class FocusedParameter;
+ class Processor;
+}
+
+namespace pluginLib
+{
+ class ParameterBinding;
+}
+
+namespace xtJucePlugin
+{
+ class FrontPanel;
+ class PatchManager;
+
+ class Editor final : public jucePluginEditorLib::Editor
+ {
+ public:
+ Editor(jucePluginEditorLib::Processor& _processor, pluginLib::ParameterBinding& _binding, std::string _skinFolder, const std::string& _jsonFilename);
+ ~Editor() override;
+
+ Editor(Editor&&) = delete;
+ Editor(const Editor&) = delete;
+ Editor& operator = (Editor&&) = delete;
+ Editor& operator = (const Editor&) = delete;
+
+ static const char* findEmbeddedResource(const std::string& _filename, uint32_t& _size);
+ const char* findResourceByFilename(const std::string& _filename, uint32_t& _size) override;
+ std::pair<std::string, std::string> getDemoRestrictionText() const override;
+
+ Controller& getXtController() const { return m_controller; }
+
+ private:
+ void mouseEnter(const juce::MouseEvent& _event) override;
+
+ Controller& m_controller;
+ std::unique_ptr<jucePluginEditorLib::FocusedParameter> m_focusedParameter;
+ std::unique_ptr<FrontPanel> m_frontPanel;
+ };
+}
diff --git a/source/xtJucePlugin/xtFrontPanel.cpp b/source/xtJucePlugin/xtFrontPanel.cpp
@@ -0,0 +1,93 @@
+#include "xtFrontPanel.h"
+
+#include "xtController.h"
+#include "xtEditor.h"
+#include "xtLcd.h"
+
+#include "../xtLib/xtMidiTypes.h"
+
+namespace xtJucePlugin
+{
+ constexpr const char* g_ledNames[] =
+ {
+ "midiLed"
+ };
+
+ FrontPanel::FrontPanel(const Editor& _editor, Controller& _controller) : m_controller(_controller)
+ {
+ m_leds.fill(nullptr);
+
+ for(size_t i=0; i<std::size(g_ledNames); ++i)
+ m_leds[i] = _editor.findComponentT<juce::Button>(g_ledNames[i], false);
+
+ auto *lcdArea = _editor.findComponentT<juce::Component>("lcdArea", false);
+
+ if (lcdArea)
+ m_lcd.reset(new XtLcd(*lcdArea));
+
+ auto* shadow = _editor.findComponent("lcdshadow", false);
+
+ if(shadow)
+ shadow->setInterceptsMouseClicks(false, false);
+
+ _controller.sendSysEx(Controller::EmuRequestLcd);
+ _controller.sendSysEx(Controller::EmuRequestLeds);
+
+ _controller.setFrontPanel(this);
+ }
+
+ FrontPanel::~FrontPanel()
+ {
+ m_controller.setFrontPanel(nullptr);
+ m_lcd.reset();
+ }
+
+ void FrontPanel::processSysex(const std::vector<uint8_t>& _msg) const
+ {
+ if(_msg.size() < 5)
+ return;
+
+ const auto cmd = static_cast<xt::SysexCommand>(_msg[4]);
+
+ switch (cmd)
+ {
+ case xt::SysexCommand::EmuLCD:
+ processLCDUpdate(_msg);
+ break;
+ case xt::SysexCommand::EmuLEDs:
+ processLedUpdate(_msg);
+ break;
+ case xt::SysexCommand::EmuRotaries:
+ case xt::SysexCommand::EmuButtons:
+ default:
+ break;
+ }
+ }
+
+ void FrontPanel::processLCDUpdate(const std::vector<uint8_t>& _msg) const
+ {
+ const auto* data = &_msg[5];
+
+ std::array<uint8_t, 80> d{};
+
+ for(size_t i=0; i<d.size(); ++i)
+ d[i] = data[i];
+
+ m_lcd->setText(d);
+ }
+
+ void FrontPanel::processLedUpdate(const std::vector<uint8_t>& _msg) const
+ {
+ const uint32_t leds =
+ (static_cast<uint32_t>(_msg[5]) << 24) |
+ (static_cast<uint32_t>(_msg[6]) << 16) |
+ (static_cast<uint32_t>(_msg[7]) << 8) |
+ static_cast<uint32_t>(_msg[8]);
+
+ for(size_t i=0; i<static_cast<uint32_t>(xt::LedType::Count); ++i)
+ {
+ if(m_leds[i])
+ m_leds[i]->setToggleState((leds & (1<<i)) != 0, juce::dontSendNotification);
+ }
+ }
+}
+\ No newline at end of file
diff --git a/source/xtJucePlugin/xtFrontPanel.h b/source/xtJucePlugin/xtFrontPanel.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "xtLcd.h"
+
+#include "juce_gui_basics/juce_gui_basics.h"
+
+#include "../xtLib/xtLeds.h"
+
+class Controller;
+
+namespace xtJucePlugin
+{
+ class Editor;
+
+ class FrontPanel
+ {
+ public:
+ explicit FrontPanel(const Editor& _editor, Controller& _controller);
+ ~FrontPanel();
+
+ void processSysex(const std::vector<uint8_t>& _msg) const;
+
+ private:
+ void processLCDUpdate(const std::vector<uint8_t>& _msg) const;
+ void processLedUpdate(const std::vector<uint8_t>& _msg) const;
+
+ std::array<juce::Button*, static_cast<uint32_t>(xt::LedType::Count)> m_leds{};
+
+ Controller& m_controller;
+ std::unique_ptr<XtLcd> m_lcd;
+ };
+}
diff --git a/source/xtJucePlugin/xtLcd.cpp b/source/xtJucePlugin/xtLcd.cpp
@@ -0,0 +1,39 @@
+#include "xtLcd.h"
+
+#include "version.h"
+
+#include "../wLib/lcdfonts.h"
+
+// 40*2 LCD simulation
+
+XtLcd::XtLcd(Component& _parent) : Lcd(_parent, 40, 2)
+{
+ postConstruct();
+}
+
+XtLcd::~XtLcd() = default;
+
+void XtLcd::setText(const std::array<uint8_t, 80>& _text)
+{
+ const std::vector<uint8_t> text{_text.begin(), _text.end()};
+ Lcd::setText(text);
+}
+
+bool XtLcd::getOverrideText(std::vector<std::vector<uint8_t>>& _lines)
+{
+ const std::string lineA(std::string("Vavra v") + g_pluginVersionString);
+ const std::string lineB = __DATE__ " " __TIME__;
+
+ _lines =
+ {
+ std::vector<uint8_t>(lineA.begin(), lineA.end()),
+ std::vector<uint8_t>(lineB.begin(), lineB.end())
+ };
+
+ return true;
+}
+
+const uint8_t* XtLcd::getCharacterData(const uint8_t _character) const
+{
+ return wLib::getCharacterData(_character);
+}
diff --git a/source/xtJucePlugin/xtLcd.h b/source/xtJucePlugin/xtLcd.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "juce_gui_basics/juce_gui_basics.h"
+
+#include "../jucePluginEditorLib/lcd.h"
+
+class XtLcd final : public jucePluginEditorLib::Lcd
+{
+public:
+ explicit XtLcd(Component& _parent);
+ ~XtLcd() override;
+
+ void setText(const std::array<uint8_t, 80> &_text);
+
+ bool getOverrideText(std::vector<std::vector<uint8_t>>& _lines) override;
+ const uint8_t* getCharacterData(uint8_t _character) const override;
+};
diff --git a/source/xtJucePlugin/xtPatchManager.cpp b/source/xtJucePlugin/xtPatchManager.cpp
@@ -0,0 +1,103 @@
+#include "xtPatchManager.h"
+
+#include "xtController.h"
+#include "xtEditor.h"
+
+#include "../jucePluginEditorLib/pluginProcessor.h"
+#include "../xtLib/xtMidiTypes.h"
+
+namespace xtJucePlugin
+{
+ PatchManager::PatchManager(Editor& _editor, juce::Component* _root, const juce::File& _dir)
+ : jucePluginEditorLib::patchManager::PatchManager(_editor, _root, _dir)
+ , m_editor(_editor)
+ , m_controller(_editor.getXtController())
+ {
+ startLoaderThread();
+ }
+
+ PatchManager::~PatchManager()
+ {
+ stopLoaderThread();
+ }
+
+ bool PatchManager::requestPatchForPart(pluginLib::patchDB::Data& _data, const uint32_t _part)
+ {
+ _data = m_controller.createSingleDump(xt::LocationH::SingleBankA, 0, static_cast<uint8_t>(_part));
+ return !_data.empty();
+ }
+
+ bool PatchManager::loadRomData(pluginLib::patchDB::DataList& _results, uint32_t _bank, uint32_t _program)
+ {
+ return false;
+ }
+
+ pluginLib::patchDB::PatchPtr PatchManager::initializePatch(pluginLib::patchDB::Data&& _sysex)
+ {
+ pluginLib::MidiPacket::Data data;
+ pluginLib::MidiPacket::AnyPartParamValues parameters;
+ if(!m_controller.parseSingle(data, parameters, _sysex))
+ return {};
+
+ auto p = std::make_shared<pluginLib::patchDB::Patch>();
+
+ p->sysex = std::move(_sysex);
+ p->name = m_controller.getSingleName(parameters);
+
+ return p;
+ }
+
+ 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.setSingleName(parameterValues, _patch->getName());
+
+ // apply program
+ uint32_t program = 0;
+ uint32_t bank = 0;
+ if(_patch->program != pluginLib::patchDB::g_invalidProgram)
+ {
+ program = std::clamp(_patch->program, 0u, 299u);
+
+ bank = program / 100;
+ program -= bank * 100;
+ }
+
+ return m_controller.createSingleDump(static_cast<xt::LocationH>(static_cast<uint8_t>(xt::LocationH::SingleBankA) + bank), static_cast<uint8_t>(program), parameterValues);
+ }
+
+ bool PatchManager::equals(const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b) const
+ {
+ if(_a == _b)
+ return true;
+
+ if(_a->hash == _b->hash)
+ return true;
+
+ return false;
+ }
+
+ uint32_t PatchManager::getCurrentPart() const
+ {
+ return m_editor.getProcessor().getController().getCurrentPart();
+ }
+
+ bool PatchManager::activatePatch(const pluginLib::patchDB::PatchPtr& _patch)
+ {
+ m_controller.sendSingle(_patch->sysex);
+ return true;
+ }
+
+ bool PatchManager::activatePatch(const pluginLib::patchDB::PatchPtr& _patch, uint32_t _part)
+ {
+ m_controller.sendSingle(_patch->sysex, static_cast<uint8_t>(_part));
+ return true;
+ }
+}
diff --git a/source/xtJucePlugin/xtPatchManager.h b/source/xtJucePlugin/xtPatchManager.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "../jucePluginEditorLib/patchmanager/patchmanager.h"
+
+class Controller;
+
+namespace xtJucePlugin
+{
+ class Editor;
+
+ class PatchManager : public jucePluginEditorLib::patchManager::PatchManager
+ {
+ public:
+ PatchManager(Editor& _editor, juce::Component* _root, const juce::File& _dir);
+ ~PatchManager() override;
+
+ // PatchManager overrides
+ bool requestPatchForPart(pluginLib::patchDB::Data& _data, uint32_t _part) override;
+ bool loadRomData(pluginLib::patchDB::DataList& _results, uint32_t _bank, uint32_t _program) override;
+ pluginLib::patchDB::PatchPtr initializePatch(pluginLib::patchDB::Data&& _sysex) override;
+ pluginLib::patchDB::Data prepareSave(const pluginLib::patchDB::PatchPtr& _patch) const override;
+ bool equals(const pluginLib::patchDB::PatchPtr& _a, const pluginLib::patchDB::PatchPtr& _b) const override;
+ uint32_t getCurrentPart() const override;
+ bool activatePatch(const pluginLib::patchDB::PatchPtr& _patch) override;
+ bool activatePatch(const pluginLib::patchDB::PatchPtr& _patch, uint32_t _part) override;
+
+ private:
+ Editor& m_editor;
+ Controller& m_controller;
+ };
+}
diff --git a/source/xtLib/CMakeLists.txt b/source/xtLib/CMakeLists.txt
@@ -0,0 +1,31 @@
+cmake_minimum_required(VERSION 3.10)
+project(xtLib)
+
+add_library(xtLib STATIC)
+
+set(SOURCES
+ xt.cpp xt.h
+ xtButtons.cpp xtButtons.h
+ xtDevice.cpp xtDevice.h
+ xtDSP.cpp xtDSP.h
+ xtHardware.cpp xtHardware.h
+ xtLcd.cpp xtLcd.h
+ xtLeds.cpp xtLeds.h
+ xtMidiTypes.h
+ xtPic.cpp xtPic.h
+ xtRom.cpp xtRom.h
+ xtState.cpp xtState.h
+ xtSysexRemoteControl.cpp xtSysexRemoteControl.h
+ xtUc.cpp xtUc.h
+)
+
+target_sources(xtLib PRIVATE ${SOURCES})
+source_group("source" FILES ${SOURCES})
+
+target_link_libraries(xtLib PUBLIC wLib)
+
+if(DSP56300_DEBUGGER)
+ target_link_libraries(xtLib PUBLIC dsp56kDebugger)
+endif()
+
+set_property(TARGET xtLib PROPERTY FOLDER "Xenia")
diff --git a/source/xtLib/xt.cpp b/source/xtLib/xt.cpp
@@ -0,0 +1,191 @@
+#include "xt.h"
+
+#include "../synthLib/midiTypes.h"
+#include "../synthLib/os.h"
+#include "../synthLib/deviceException.h"
+
+#include "dsp56kEmu/threadtools.h"
+
+#include "xtHardware.h"
+
+#include "../mc68k/logging.h"
+
+namespace xt
+{
+ Xt::Xt()
+ {
+ // create hardware, will use in-memory ROM if no ROM provided
+ const auto romFile = synthLib::findROM(g_romSize, g_romSize);
+ if(romFile.empty())
+ throw synthLib::DeviceException(synthLib::DeviceError::FirmwareMissing, "Failed to find device ROM");
+
+ MCLOG("Boot using ROM " << romFile);
+
+ m_hw.reset(new Hardware(romFile));
+
+ if(!isValid())
+ return;
+
+ m_midiOutBuffer.reserve(1024);
+
+ m_ucThread.reset(new std::thread([&]()
+ {
+ dsp56k::ThreadTools::setCurrentThreadName("MC68331");
+ while(!m_destroy)
+ processUcThread();
+ m_destroy = false;
+ m_hw->ucThreadTerminated();
+ }));
+
+ m_hw->initVoiceExpansion();
+
+ m_hw->getUC().setLcdDirtyCallback([this]
+ {
+ m_dirtyFlags.fetch_or(static_cast<uint32_t>(DirtyFlags::Lcd));
+ });
+ m_hw->getUC().setLedsDirtyCallback([this]
+ {
+ m_dirtyFlags.fetch_or(static_cast<uint32_t>(DirtyFlags::Leds));
+ });
+ }
+
+ Xt::~Xt()
+ {
+ if(!isValid())
+ return;
+
+ // we need to have passed the boot stage
+ m_hw->processAudio(1);
+
+ m_destroy = true;
+
+ // DSP needs to run to let the uc thread wake up
+ const auto& esai = m_hw->getDSP().getPeriph().getEssi0();
+ while(m_destroy)
+ {
+ if(!esai.getAudioOutputs().empty())
+ m_hw->processAudio(1);
+ else
+ std::this_thread::yield();
+ }
+
+ m_ucThread->join();
+ m_ucThread.reset();
+ m_hw.reset();
+ }
+
+ bool Xt::isValid() const
+ {
+ return m_hw && m_hw->isValid();
+ }
+
+ void Xt::process(const float** _inputs, float** _outputs, uint32_t _frames, uint32_t _latency)
+ {
+ std::lock_guard lock(m_mutex);
+
+ m_hw->ensureBufferSize(_frames);
+
+ // convert inputs from float to DSP words
+ auto& dspIns = m_hw->getAudioInputs();
+
+ for(size_t c=0; c<dspIns.size(); ++c)
+ {
+ for(uint32_t i=0; i<_frames; ++i)
+ dspIns[c][i] = dsp56k::sample2dsp(_inputs[c][i]);
+ }
+
+ internalProcess(_frames, _latency);
+
+ // convert outputs from DSP words to float
+ const auto& dspOuts = m_hw->getAudioOutputs();
+
+ for(size_t c=0; c<dspOuts.size(); ++c)
+ {
+ for(uint32_t i=0; i<_frames; ++i)
+ _outputs[c][i] = dsp56k::dsp2sample<float>(dspOuts[c][i]);
+ }
+ }
+
+ void Xt::process(uint32_t _frames, uint32_t _latency)
+ {
+ std::lock_guard lock(m_mutex);
+ internalProcess(_frames, _latency);
+ }
+
+ void Xt::internalProcess(uint32_t _frames, uint32_t _latency)
+ {
+ // process audio
+ m_hw->processAudio(_frames, _latency);
+
+ // receive midi output
+ m_hw->receiveMidi(m_midiOutBuffer);
+ }
+
+ TAudioInputs& Xt::getAudioInputs() const
+ {
+ return m_hw->getAudioInputs();
+ }
+
+ TAudioOutputs& Xt::getAudioOutputs() const
+ {
+ return m_hw->getAudioOutputs();
+ }
+
+ void Xt::sendMidiEvent(const synthLib::SMidiEvent& _ev) const
+ {
+ m_hw->sendMidi(_ev);
+ }
+
+ void Xt::receiveMidi(std::vector<uint8_t>& _buffer)
+ {
+ std::lock_guard lock(m_mutex);
+ std::swap(_buffer, m_midiOutBuffer);
+ m_midiOutBuffer.clear();
+ }
+
+ Hardware* Xt::getHardware() const
+ {
+ return m_hw.get();
+ }
+
+ bool Xt::isBootCompleted() const
+ {
+ return m_hw && m_hw->isBootCompleted();
+ }
+
+ Xt::DirtyFlags Xt::getDirtyFlags()
+ {
+ const auto r = m_dirtyFlags.exchange(0);
+ return static_cast<DirtyFlags>(r);
+ }
+
+ void Xt::readLCD(std::array<char, 80>& _lcdData) const
+ {
+ _lcdData = m_hw->getUC().getLcd().getData();
+ }
+
+ bool Xt::getLedState(LedType _led) const
+ {
+ return m_hw->getUC().getLedState(_led);
+ }
+
+ bool Xt::getButton(ButtonType _button) const
+ {
+ return m_hw->getUC().getButton(_button);
+ }
+
+ void Xt::processUcThread() const
+ {
+ for(size_t i=0; i<32; ++i)
+ {
+ m_hw->process();
+ m_hw->process();
+ m_hw->process();
+ m_hw->process();
+ m_hw->process();
+ m_hw->process();
+ m_hw->process();
+ m_hw->process();
+ }
+ }
+}
diff --git a/source/xtLib/xt.h b/source/xtLib/xt.h
@@ -0,0 +1,74 @@
+#pragma once
+
+#include <memory>
+#include <mutex>
+#include <vector>
+
+#include <thread>
+#include <atomic>
+
+#include "xtButtons.h"
+#include "xtLeds.h"
+#include "xtTypes.h"
+
+namespace synthLib
+{
+ struct SMidiEvent;
+}
+
+namespace xt
+{
+ class Hardware;
+
+ class Xt
+ {
+ public:
+ enum class DirtyFlags : uint32_t
+ {
+ None = 0,
+ Leds = 0x01,
+ Lcd = 0x02,
+ };
+
+ Xt();
+ ~Xt();
+
+ bool isValid() const;
+
+ void process(const float** _inputs, float** _outputs, uint32_t _frames, uint32_t _latency = 0);
+
+ void process(uint32_t _frames, uint32_t _latency = 0);
+
+ TAudioInputs& getAudioInputs() const;
+ TAudioOutputs& getAudioOutputs() const;
+
+ void sendMidiEvent(const synthLib::SMidiEvent& _ev) const;
+ void receiveMidi(std::vector<uint8_t>& _buffer);
+
+ Hardware* getHardware() const;
+
+ bool isBootCompleted() const;
+
+ DirtyFlags getDirtyFlags();
+
+ void readLCD(std::array<char, 80>& _lcdData) const;
+
+ bool getLedState(LedType _led) const;
+ bool getButton(ButtonType _button) const;
+
+ private:
+ void internalProcess(uint32_t _frames, uint32_t _latency);
+
+ void processUcThread() const;
+
+ std::unique_ptr<Hardware> m_hw;
+
+ std::mutex m_mutex;
+
+ std::vector<uint8_t> m_midiOutBuffer;
+
+ std::unique_ptr<std::thread> m_ucThread;
+ bool m_destroy = false;
+ std::atomic<uint32_t> m_dirtyFlags = 0;
+ };
+}
diff --git a/source/xtLib/xtButtons.cpp b/source/xtLib/xtButtons.cpp
diff --git a/source/xtLib/xtButtons.h b/source/xtLib/xtButtons.h
@@ -0,0 +1,17 @@
+#pragma once
+
+namespace xt
+{
+ enum class ButtonType
+ {
+ Play = 0,
+ Store = 1,
+ Recall = 2,
+ Compare = 3,
+ Undo = 4,
+ Utility = 5,
+ Power = 6,
+
+ Count
+ };
+}
+\ No newline at end of file
diff --git a/source/xtLib/xtDSP.cpp b/source/xtLib/xtDSP.cpp
@@ -0,0 +1,200 @@
+#include "xtDSP.h"
+
+#include "../wLib/dspBootCode.h"
+#include "xtHardware.h"
+
+#if DSP56300_DEBUGGER
+#include "dsp56kDebugger/debugger.h"
+#endif
+
+#include "../mc68k/hdi08.h"
+#include "dsp56kEmu/types.h"
+
+namespace xt
+{
+ static dsp56k::DefaultMemoryValidator g_memoryValidator;
+
+ DSP::DSP(Hardware& _hardware, mc68k::Hdi08& _hdiUC, const uint32_t _index)
+ : m_hardware(_hardware), m_hdiUC(_hdiUC)
+ , m_index(_index)
+ , m_name({static_cast<char>('A' + _index)})
+ , m_periphX()
+ , m_memory(g_memoryValidator, g_pMemSize, g_xyMemSize, g_bridgedAddr, m_memoryBuffer)
+ , m_dsp(m_memory, &m_periphX, &m_periphNop)
+ {
+ m_periphX.getEssiClock().setExternalClockFrequency(10'240'000); // 10,24 MHz
+ m_periphX.getEssiClock().setSamplerate(40000);
+ m_periphX.getEssiClock().setClockSource(dsp56k::EsaiClock::ClockSource::Cycles);
+
+ auto config = m_dsp.getJit().getConfig();
+
+ config.aguSupportBitreverse = true;
+ config.linkJitBlocks = true;
+ config.dynamicPeripheralAddressing = true;
+ config.maxInstructionsPerBlock = 0;
+
+ m_dsp.getJit().setConfig(config);
+
+ // fill P memory with something that reminds us if we jump to garbage
+ for(dsp56k::TWord i=0; i<m_memory.sizeP(); ++i)
+ {
+ m_memory.set(dsp56k::MemArea_P, i, 0x000200); // debug instruction
+ m_dsp.getJit().notifyProgramMemWrite(i);
+ }
+
+ const auto& bootCode = wLib::g_dspBootCode56303;
+
+ // rewrite bootloader to work at address g_bootCodeBase instead of $ff0000
+ for(uint32_t i=0; i<std::size(bootCode); ++i)
+ {
+ uint32_t code = bootCode[i];
+ if((code & 0xffff00) == 0xff0000)
+ {
+ code = g_bootCodeBase | (bootCode[i] & 0xff);
+ }
+
+ m_memory.set(dsp56k::MemArea_P, i + g_bootCodeBase, code);
+ m_dsp.getJit().notifyProgramMemWrite(i + g_bootCodeBase);
+ }
+
+// m_memory.saveAssembly("dspBootDisasm.asm", g_bootCodeBase, static_cast<uint32_t>(std::size(bootCode)), true, true, &m_periphX, nullptr);
+
+ // set OMR pins so that bootcode wants program data via HDI08 RX
+ m_dsp.setPC(g_bootCodeBase);
+ m_dsp.regs().omr.var |= OMR_MA | OMR_MB | OMR_MC | OMR_MD;
+
+// getPeriph().disableTimers(true); // only used to test DSP load, we report 0 all the time for now
+
+ m_periphX.getEssi0().writeEmptyAudioIn(8);
+
+ hdi08().setRXRateLimit(0);
+ hdi08().setTransmitDataAlwaysEmpty(false);
+
+ m_hdiUC.setRxEmptyCallback([&](const bool needMoreData)
+ {
+ onUCRxEmpty(needMoreData);
+ });
+ m_hdiUC.setWriteTxCallback([&](const uint32_t _word)
+ {
+ hdiTransferUCtoDSP(_word);
+ });
+ m_hdiUC.setWriteIrqCallback([&](const uint8_t _irq)
+ {
+ hdiSendIrqToDSP(_irq);
+ });
+ m_hdiUC.setReadIsrCallback([&](const uint8_t _isr)
+ {
+ return hdiUcReadIsr(_isr);
+ });
+
+#if DSP56300_DEBUGGER
+ m_thread.reset(new dsp56k::DSPThread(dsp(), m_name.c_str(), std::make_shared<dsp56kDebugger::Debugger>(m_dsp)));
+#else
+ m_thread.reset(new dsp56k::DSPThread(dsp(), m_name.c_str()));
+#endif
+
+ m_thread->setLogToStdout(false);
+ }
+
+ void DSP::exec()
+ {
+ m_thread->join();
+ m_thread.reset();
+
+ m_hdiUC.setRxEmptyCallback({});
+ m_dsp.exec();
+ }
+
+ void DSP::dumpPMem(const std::string& _filename)
+ {
+ m_memory.saveAssembly((_filename + ".asm").c_str(), 0, g_pMemSize, true, false, &m_periphX);
+ }
+
+ void DSP::dumpXYMem(const std::string& _filename) const
+ {
+ m_memory.save((_filename + "_X.txt").c_str(), dsp56k::MemArea_X);
+ m_memory.save((_filename + "_Y.txt").c_str(), dsp56k::MemArea_Y);
+ }
+
+ void DSP::transferHostFlagsUc2Dsdp()
+ {
+ const uint32_t hf01 = m_hdiUC.icr() & 0x18;
+
+ if (hf01 != m_hdiHF01)
+ {
+// LOG('[' << m_name << "] HDI HF01=" << HEXN((hf01>>3),1));
+ waitDspRxEmpty();
+ m_hdiHF01 = hf01;
+ hdi08().setPendingHostFlags01(hf01);
+ }
+ }
+
+ void DSP::onUCRxEmpty(bool _needMoreData)
+ {
+ hdi08().injectTXInterrupt();
+
+ if (_needMoreData)
+ {
+ m_hardware.ucYieldLoop([&]()
+ {
+ return dsp().hasPendingInterrupts() || (hdi08().txInterruptEnabled() && !hdi08().hasTX());
+ });
+ }
+
+ hdiTransferDSPtoUC();
+ }
+
+ bool DSP::hdiTransferDSPtoUC()
+ {
+ if (m_hdiUC.canReceiveData() && hdi08().hasTX())
+ {
+ const auto v = hdi08().readTX();
+// LOG('[' << m_name << "] HDI dsp2uc=" << HEX(v));
+ m_hdiUC.writeRx(v);
+ return true;
+ }
+ return false;
+ }
+
+ void DSP::hdiTransferUCtoDSP(dsp56k::TWord _word)
+ {
+ m_haveSentTXtoDSP = true;
+// LOG('[' << m_name << "] toDSP writeRX=" << HEX(_word));
+ hdi08().writeRX(&_word, 1);
+ }
+
+ void DSP::hdiSendIrqToDSP(uint8_t _irq)
+ {
+ waitDspRxEmpty();
+
+// LOG('[' << m_name << "] Inject interrupt" << HEXN(_irq,2));
+
+ dsp().injectExternalInterrupt(_irq);
+
+ m_hardware.ucYieldLoop([&]()
+ {
+ return dsp().hasPendingInterrupts();
+ });
+
+ hdiTransferDSPtoUC();
+ }
+
+ uint8_t DSP::hdiUcReadIsr(uint8_t _isr)
+ {
+ // transfer DSP host flags HF2&3 to uc
+ const auto hf23 = hdi08().readControlRegister() & 0x18;
+ _isr &= ~0x18;
+ _isr |= hf23;
+ return _isr;
+ }
+
+ void DSP::waitDspRxEmpty()
+ {
+ m_hardware.ucYieldLoop([&]()
+ {
+ return (hdi08().hasRXData() && hdi08().rxInterruptEnabled()) || dsp().hasPendingInterrupts();
+ });
+// LOG("writeRX wait over");
+ }
+
+}
diff --git a/source/xtLib/xtDSP.h b/source/xtLib/xtDSP.h
@@ -0,0 +1,76 @@
+#pragma once
+
+#include "dsp56kEmu/dsp.h"
+#include "dsp56kEmu/dspthread.h"
+#include "dsp56kEmu/peripherals.h"
+
+#include "../wLib/wDsp.h"
+
+namespace mc68k
+{
+ class Hdi08;
+}
+
+namespace xt
+{
+ class Hardware;
+
+ class DSP : public wLib::Dsp
+ {
+ public:
+ static constexpr dsp56k::TWord g_bridgedAddr = 0x020000; // start of external SRAM, mapped to X and Y
+ static constexpr dsp56k::TWord g_xyMemSize = 0x800000; // due to weird AAR mapping we just allocate enough so that everything fits into it
+ static constexpr dsp56k::TWord g_pMemSize = 0x020000; // DSP code does not use all of it, gives space for our boot code
+ static constexpr dsp56k::TWord g_bootCodeBase = 0x010000;
+
+ DSP(Hardware& _hardware, mc68k::Hdi08& _hdiUC, uint32_t _index);
+ void exec();
+
+ dsp56k::HDI08& hdi08()
+ {
+ return m_periphX.getHI08();
+ }
+
+ dsp56k::DSP& dsp()
+ {
+ return m_dsp;
+ }
+
+ dsp56k::Peripherals56303& getPeriph()
+ {
+ return m_periphX;
+ }
+
+ void dumpPMem(const std::string& _filename);
+ void dumpXYMem(const std::string& _filename) const;
+
+ void transferHostFlagsUc2Dsdp();
+ bool hdiTransferDSPtoUC();
+ void hdiSendIrqToDSP(uint8_t _irq);
+
+ dsp56k::DSPThread& thread() { return *m_thread; }
+ bool haveSentTXToDSP() const { return m_haveSentTXtoDSP; }
+
+ private:
+ void onUCRxEmpty(bool _needMoreData);
+ void hdiTransferUCtoDSP(dsp56k::TWord _word);
+ uint8_t hdiUcReadIsr(uint8_t _isr);
+ void waitDspRxEmpty();
+
+ Hardware& m_hardware;
+ mc68k::Hdi08& m_hdiUC;
+ const uint32_t m_index;
+ const std::string m_name;
+
+ dsp56k::PeripheralsNop m_periphNop;
+ dsp56k::Peripherals56303 m_periphX;
+ dsp56k::Memory m_memory;
+ dsp56k::DSP m_dsp;
+ dsp56k::TWord m_memoryBuffer[dsp56k::Memory::calcMemSize(g_pMemSize, g_xyMemSize, g_bridgedAddr)]{0};
+
+ bool m_haveSentTXtoDSP = false;
+ uint32_t m_hdiHF01 = 0; // uc => DSP
+
+ std::unique_ptr<dsp56k::DSPThread> m_thread;
+ };
+}
diff --git a/source/xtLib/xtDevice.cpp b/source/xtLib/xtDevice.cpp
@@ -0,0 +1,136 @@
+#include "xtDevice.h"
+
+#include "xtHardware.h"
+
+namespace mqLib
+{
+ class MicroQ;
+}
+
+namespace xt
+{
+ Device::Device() : m_state(m_xt), m_sysexRemote(m_xt)
+ {
+ while(!m_xt.isBootCompleted())
+ m_xt.process(8);
+
+ m_state.createInitState();
+
+ auto* hw = m_xt.getHardware();
+ hw->resetMidiCounter();
+ }
+
+ float Device::getSamplerate() const
+ {
+ return 40000.0f;
+ }
+
+ bool Device::isValid() const
+ {
+ return m_xt.isValid();
+ }
+
+ bool Device::getState(std::vector<uint8_t>& _state, synthLib::StateType _type)
+ {
+ return m_state.getState(_state, _type);
+ }
+
+ bool Device::setState(const std::vector<uint8_t>& _state, synthLib::StateType _type)
+ {
+ return m_state.setState(_state, _type);
+ }
+
+ uint32_t Device::getChannelCountIn()
+ {
+ return 2;
+ }
+
+ uint32_t Device::getChannelCountOut()
+ {
+ return 4;
+ }
+
+ void Device::readMidiOut(std::vector<synthLib::SMidiEvent>& _midiOut)
+ {
+ m_xt.receiveMidi(m_midiOutBuffer);
+ m_midiOutParser.write(m_midiOutBuffer);
+ m_midiOutParser.getEvents(_midiOut);
+ m_midiOutBuffer.clear();
+
+ wLib::Responses responses;
+
+ for (const auto& midiOut : _midiOut)
+ {
+ m_state.receive(responses, midiOut, State::Origin::Device);
+
+ for (auto& response : responses)
+ {
+ auto& r = _midiOut.emplace_back();
+ std::swap(response, r.sysex);
+ }
+ }
+
+ if(!m_customSysexOut.empty())
+ {
+ _midiOut.insert(_midiOut.begin(), m_customSysexOut.begin(), m_customSysexOut.end());
+ m_customSysexOut.clear();
+ }
+ }
+
+ void Device::processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _samples)
+ {
+ const float* inputs[2] = {_inputs[0], _inputs[1]};
+ float* outputs[4] = {_outputs[0], _outputs[1], _outputs[2], _outputs[3]};
+ m_xt.process(inputs, outputs, static_cast<uint32_t>(_samples), getExtraLatencySamples());
+
+ const auto dirty = static_cast<uint32_t>(m_xt.getDirtyFlags());
+
+ m_sysexRemote.handleDirtyFlags(m_customSysexOut, dirty);
+ }
+
+ bool Device::sendMidi(const synthLib::SMidiEvent& _ev, std::vector<synthLib::SMidiEvent>& _response)
+ {
+ const auto& sysex = _ev.sysex;
+
+ if (!sysex.empty())
+ {
+ if (m_sysexRemote.receive(m_customSysexOut, sysex))
+ return true;
+ }
+
+ Responses responses;
+
+ const auto res = m_state.receive(responses, _ev, State::Origin::External);
+
+ for (auto& response : responses)
+ {
+ auto& r = _response.emplace_back();
+ std::swap(response, r.sysex);
+ }
+
+ // do not forward to device if our cache was able to reply. It might have sent something to the device already on its own if a cache miss occured
+ if(res)
+ return true;
+
+ if(_ev.sysex.empty())
+ {
+ auto e = _ev;
+ e.offset += m_numSamplesProcessed + getExtraLatencySamples();
+ m_xt.sendMidiEvent(e);
+ }
+ else
+ {
+ m_xt.sendMidiEvent(_ev);
+ }
+ return true;
+ }
+
+ dsp56k::EsxiClock* Device::getDspEsxiClock() const
+ {
+ const auto& xt = const_cast<Xt&>(m_xt);
+ auto* p = dynamic_cast<dsp56k::Peripherals56303*>(xt.getHardware()->getDSP().dsp().getPeriph(dsp56k::MemArea_X));
+ if(!p)
+ return nullptr;
+ return &p->getEssiClock();
+ }
+}
diff --git a/source/xtLib/xtDevice.h b/source/xtLib/xtDevice.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include "xt.h"
+#include "xtState.h"
+#include "xtSysexRemoteControl.h"
+#include "../wLib/wDevice.h"
+
+namespace dsp56k
+{
+ class EsxiClock;
+}
+
+namespace xt
+{
+ class Device : public wLib::Device
+ {
+ public:
+ Device();
+
+ float getSamplerate() const override;
+ bool isValid() const override;
+ bool getState(std::vector<uint8_t>& _state, synthLib::StateType _type) override;
+ bool setState(const std::vector<uint8_t>& _state, synthLib::StateType _type) override;
+ uint32_t getChannelCountIn() override;
+ uint32_t getChannelCountOut() override;
+
+ protected:
+ void readMidiOut(std::vector<synthLib::SMidiEvent>& _midiOut) override;
+ void processAudio(const synthLib::TAudioInputs& _inputs, const synthLib::TAudioOutputs& _outputs, size_t _samples) override;
+ bool sendMidi(const synthLib::SMidiEvent& _ev, std::vector<synthLib::SMidiEvent>& _response) override;
+
+ dsp56k::EsxiClock* getDspEsxiClock() const override;
+ private:
+
+ Xt m_xt;
+ State m_state;
+ SysexRemoteControl m_sysexRemote;
+ };
+}
diff --git a/source/xtLib/xtHardware.cpp b/source/xtLib/xtHardware.cpp
@@ -0,0 +1,426 @@
+#include "xtHardware.h"
+
+#include "../synthLib/midiTypes.h"
+#include "../synthLib/midiBufferParser.h"
+#include "../synthLib/deviceException.h"
+
+#include <cstring> // memcpy
+
+namespace xt
+{
+ constexpr uint32_t g_syncEsaiFrameRate = 8;
+ constexpr uint32_t g_syncHaltDspEsaiThreshold = 16;
+
+ static_assert((g_syncEsaiFrameRate & (g_syncEsaiFrameRate - 1)) == 0, "esai frame sync rate must be power of two");
+ static_assert(g_syncHaltDspEsaiThreshold >= g_syncEsaiFrameRate * 2, "esai DSP halt threshold must be greater than two times the sync rate");
+
+ Hardware::Hardware(std::string _romFilename)
+ : m_romFileName(std::move(_romFilename))
+ , m_rom(m_romFileName, nullptr)
+ , m_uc(m_rom)
+ , m_dsps{DSP(*this, m_uc.getHdi08A().getHdi08(), 0)}
+ , m_midi(m_uc.getQSM())
+ {
+ if(!m_rom.isValid())
+ throw synthLib::DeviceException(synthLib::DeviceError::FirmwareMissing);
+ }
+
+ Hardware::~Hardware()
+ {
+ m_dsps.front().getPeriph().getEssi0().setCallback({}, 0);
+ }
+
+ void Hardware::process()
+ {
+ processUcCycle();
+ }
+
+ void Hardware::sendMidi(const synthLib::SMidiEvent& _ev)
+ {
+ m_midiIn.push_back(_ev);
+ }
+
+ void Hardware::receiveMidi(std::vector<uint8_t>& _data)
+ {
+ m_midi.readTransmitBuffer(_data);
+ }
+
+ void Hardware::resetMidiCounter()
+ {
+ // wait for DSP to enter blocking state
+
+ const auto& esai = m_dsps.front().getPeriph().getEssi0();
+
+ auto& inputs = esai.getAudioInputs();
+ auto& outputs = esai.getAudioOutputs();
+
+ while(inputs.size() > 2 && !outputs.full())
+ std::this_thread::yield();
+
+ m_midiOffsetCounter = 0;
+ }
+
+ void Hardware::hdiProcessUCtoDSPNMIIrq()
+ {
+ // QS6 is connected to DSP NMI pin but I've never seen this being triggered
+#if SUPPORT_NMI_INTERRUPT
+ const uint8_t requestNMI = m_uc.requestDSPinjectNMI();
+
+ if(m_requestNMI && !requestNMI)
+ {
+// LOG("uc request DSP NMI");
+ m_dsps.front().hdiSendIrqToDSP(dsp56k::Vba_NMI);
+
+ m_requestNMI = requestNMI;
+ }
+#endif
+ }
+
+ void Hardware::ucYieldLoop(const std::function<bool()>& _continue)
+ {
+ const auto dspHalted = m_haltDSP;
+
+ resumeDSP();
+
+ while(_continue())
+ {
+ if(m_processAudio)
+ {
+ std::this_thread::yield();
+ }
+ else
+ {
+ std::unique_lock uLock(m_esaiFrameAddedMutex);
+ m_esaiFrameAddedCv.wait(uLock);
+ }
+ }
+
+ if(dspHalted)
+ haltDSP();
+ }
+
+ void Hardware::initVoiceExpansion()
+ {
+ if (m_dsps.size() < 3)
+ {
+ setupEsaiListener();
+ return;
+ }
+ }
+
+ void Hardware::setupEsaiListener()
+ {
+ auto& esaiA = m_dsps.front().getPeriph().getEssi0();
+
+ esaiA.setCallback([&](dsp56k::Audio*)
+ {
+/* auto& dsp = m_dsps.front().dsp();
+ auto& mem = dsp.memory();
+ mem.saveAssembly("xt_dspA.asm", 0, mem.sizeP(), true, false, dsp.getPeriph(0), dsp.getPeriph(1));
+*/
+ m_bootCompleted = true;
+ ++m_esaiFrameIndex;
+
+ processMidiInput();
+
+ if((m_esaiFrameIndex & (g_syncEsaiFrameRate-1)) == 0)
+ m_esaiFrameAddedCv.notify_one();
+
+ m_requestedFramesAvailableMutex.lock();
+
+ if(m_requestedFrames && esaiA.getAudioOutputs().size() >= m_requestedFrames)
+ {
+ m_requestedFramesAvailableMutex.unlock();
+ m_requestedFramesAvailableCv.notify_one();
+ }
+ else
+ {
+ m_requestedFramesAvailableMutex.unlock();
+ }
+
+ std::unique_lock uLock(m_haltDSPmutex);
+ m_haltDSPcv.wait(uLock, [&]{ return m_haltDSP == false; });
+ }, 0);
+ }
+
+ void Hardware::processUcCycle()
+ {
+ syncUcToDSP();
+
+ const auto deltaCycles = m_uc.exec();
+ if(m_esaiFrameIndex > 0)
+ m_remainingUcCycles -= static_cast<int64_t>(deltaCycles);
+
+ for (auto& dsp : m_dsps)
+ dsp.transferHostFlagsUc2Dsdp();
+
+ hdiProcessUCtoDSPNMIIrq();
+
+ for (auto& dsp : m_dsps)
+ dsp.hdiTransferDSPtoUC();
+
+ if(m_uc.requestDSPReset())
+ {
+ for (auto& dsp : m_dsps)
+ {
+ if(dsp.haveSentTXToDSP())
+ {
+// m_uc.dumpMemory("DSPreset");
+ assert(false && "DSP needs reset even though it got data already. Needs impl");
+ }
+ }
+ m_uc.notifyDSPBooted();
+ }
+ }
+
+ void Hardware::haltDSP()
+ {
+ if(m_haltDSP)
+ return;
+
+ std::lock_guard uLockHalt(m_haltDSPmutex);
+ m_haltDSP = true;
+ }
+
+ void Hardware::resumeDSP()
+ {
+ if(!m_haltDSP)
+ return;
+
+ {
+ std::lock_guard uLockHalt(m_haltDSPmutex);
+ m_haltDSP = false;
+ }
+ m_haltDSPcv.notify_one();
+ }
+
+ void Hardware::syncUcToDSP()
+ {
+ if(m_remainingUcCycles > 0)
+ return;
+
+ // we can only use ESAI to clock the uc once it has been enabled
+ if(m_esaiFrameIndex <= 0)
+ return;
+
+ if(m_esaiFrameIndex == m_lastEsaiFrameIndex)
+ {
+ resumeDSP();
+ std::unique_lock uLock(m_esaiFrameAddedMutex);
+ m_esaiFrameAddedCv.wait(uLock, [this]{return m_esaiFrameIndex > m_lastEsaiFrameIndex;});
+ }
+
+ const auto esaiFrameIndex = m_esaiFrameIndex;
+
+ const auto ucClock = m_uc.getSim().getSystemClockHz();
+
+ constexpr double divInv = 1.0 / 40000.0;
+ const double ucCyclesPerFrame = static_cast<double>(ucClock) * divInv;
+
+ const auto esaiDelta = esaiFrameIndex - m_lastEsaiFrameIndex;
+
+ m_remainingUcCyclesD += ucCyclesPerFrame * static_cast<double>(esaiDelta);
+ m_remainingUcCycles += static_cast<int64_t>(m_remainingUcCyclesD);
+ m_remainingUcCyclesD -= static_cast<double>(m_remainingUcCycles);
+
+ if(esaiDelta > g_syncHaltDspEsaiThreshold)
+ {
+ haltDSP();
+ }
+ else
+ {
+ resumeDSP();
+ }
+
+ m_lastEsaiFrameIndex = esaiFrameIndex;
+ }
+
+ void Hardware::processMidiInput()
+ {
+ ++m_midiOffsetCounter;
+
+ while(!m_midiIn.empty())
+ {
+ const auto& e = m_midiIn.front();
+
+ if(e.offset > m_midiOffsetCounter)
+ break;
+
+ if(!e.sysex.empty())
+ {
+ m_midi.writeMidi(e.sysex);
+ }
+ else
+ {
+ m_midi.writeMidi(e.a);
+ const auto len = synthLib::MidiBufferParser::lengthFromStatusByte(e.a);
+ if (len > 1)
+ m_midi.writeMidi(e.b);
+ if (len > 2)
+ m_midi.writeMidi(e.c);
+ }
+
+ m_midiIn.pop_front();
+ }
+ }
+
+ void Hardware::processAudio(uint32_t _frames, uint32_t _latency)
+ {
+ ensureBufferSize(_frames);
+
+ if(m_esaiFrameIndex == 0)
+ return;
+
+ m_midi.process(_frames);
+
+ m_processAudio = true;
+
+ auto& esai = m_dsps.front().getPeriph().getEssi0();
+
+ const dsp56k::TWord* inputs[16]{nullptr};
+ dsp56k::TWord* outputs[16]{nullptr};
+
+ // TODO: Right audio input channel needs to be delayed by one frame
+ ::memcpy(&m_delayedAudioIn[1], m_audioInputs[1].data(), sizeof(dsp56k::TWord) * _frames);
+
+ inputs[1] = &m_audioInputs[0].front();
+ inputs[0] = m_delayedAudioIn.data();
+ inputs[2] = m_dummyInput.data();
+ inputs[3] = m_dummyInput.data();
+ inputs[4] = m_dummyInput.data();
+ inputs[5] = m_dummyInput.data();
+ inputs[6] = m_dummyInput.data();
+ inputs[7] = m_dummyInput.data();
+
+ outputs[1] = &m_audioOutputs[0].front();
+ outputs[0] = &m_audioOutputs[1].front();
+ outputs[3] = &m_audioOutputs[2].front();
+ outputs[2] = &m_audioOutputs[3].front();
+ outputs[5] = m_dummyOutput.data();
+ outputs[4] = m_dummyOutput.data();
+ outputs[6] = m_dummyOutput.data();
+ outputs[7] = m_dummyOutput.data();
+ outputs[8] = m_dummyOutput.data();
+ outputs[9] = m_dummyOutput.data();
+ outputs[10] = m_dummyOutput.data();
+ outputs[11] = m_dummyOutput.data();
+
+ const auto totalFrames = _frames;
+
+ while (_frames)
+ {
+ const auto processCount = std::min(_frames, static_cast<uint32_t>(1024));
+ _frames -= processCount;
+/*
+ if constexpr (g_useVoiceExpansion)
+ {
+ auto& esaiA = m_dsps[0].getPeriph().getEsai();
+ auto& esaiB = m_dsps[1].getPeriph().getEsai();
+ auto& esaiC = m_dsps[2].getPeriph().getEsai();
+
+ const auto tccrA = esaiA.getTccrAsString(); const auto rccrA = esaiA.getRccrAsString();
+ const auto tcrA = esaiA.getTcrAsString(); const auto rcrA = esaiA.getRcrAsString();
+
+ const auto tccrB = esaiB.getTccrAsString(); const auto rccrB = esaiB.getRccrAsString();
+ const auto tcrB = esaiB.getTcrAsString(); const auto rcrB = esaiB.getRcrAsString();
+
+ const auto tccrC = esaiC.getTccrAsString(); const auto rccrC = esaiC.getRccrAsString();
+ const auto tcrC = esaiC.getTcrAsString(); const auto rcrC = esaiC.getRcrAsString();
+
+ LOG("ESAI DSPmain:\n" << tccrA << '\n' << tcrA << '\n' << rccrA << '\n' << rcrA << '\n');
+ LOG("ESAI VexpA:\n" << tccrB << '\n' << tcrB << '\n' << rccrB << '\n' << rcrB << '\n');
+ LOG("ESAI VexpB:\n" << tccrC << '\n' << tcrC << '\n' << rccrC << '\n' << rcrC << '\n');
+
+ // vexp1 only needs the audio input
+ esaiB.processAudioInputInterleaved(inputs, processCount);
+
+ // transfer output from vexp1 to vexp2
+ esaiB.processAudioOutputInterleaved(outputs, processCount);
+
+ const dsp56k::TWord* in[] = { outputs[0], outputs[1], outputs[2], outputs[3], outputs[4], outputs[5], nullptr, nullptr };
+ esaiC.processAudioInputInterleaved(in, processCount);
+
+ // read output of vexp2 and send to main
+ esaiC.processAudioOutputInterleaved(outputs, processCount);
+
+ // RX1/2 = vexp2 output TX1/TX2
+ const dsp56k::TWord* inA[] = { inputs[1], inputs[0], outputs[2], outputs[3], outputs[4], outputs[5], nullptr, nullptr };
+ esaiA.processAudioInputInterleaved(inA, processCount);
+
+ // final output 0,1,2 = audio outs below
+ }
+ else
+*/ {
+ esai.processAudioInputInterleaved(inputs, processCount, _latency);
+ }
+
+ const auto requiredSize = processCount > 8 ? processCount - 8 : 0;
+
+ if(esai.getAudioOutputs().size() < requiredSize)
+ {
+ // reduce thread contention by waiting for output buffer to be full enough to let us grab the data without entering the read mutex too often
+
+ std::unique_lock uLock(m_requestedFramesAvailableMutex);
+ m_requestedFrames = requiredSize;
+ m_requestedFramesAvailableCv.wait(uLock, [&]()
+ {
+ if(esai.getAudioOutputs().size() < requiredSize)
+ return false;
+ m_requestedFrames = 0;
+ return true;
+ });
+ }
+
+ esai.processAudioOutputInterleaved(outputs, processCount);
+ /*
+ if constexpr (g_useVoiceExpansion)
+ {
+ for (uint32_t i = 1; i < 3; ++i)
+ {
+ auto& e = m_dsps[i].getPeriph().getEsai();
+
+ dsp56k::TWord* outs[16]{ nullptr };
+ if (e.getAudioOutputs().size() >= 512)
+ e.processAudioOutputInterleaved(outs, static_cast<uint32_t>(e.getAudioOutputs().size() >> 1));
+ }
+ }
+ */
+ inputs[0] += processCount;
+ inputs[1] += processCount;
+
+ outputs[0] += processCount;
+ outputs[1] += processCount;
+ outputs[2] += processCount;
+ outputs[3] += processCount;
+ outputs[4] += processCount;
+ outputs[5] += processCount;
+ }
+
+ m_delayedAudioIn[0] = m_audioInputs[1][totalFrames-1];
+
+ m_processAudio = false;
+ }
+
+ void Hardware::ensureBufferSize(const uint32_t _frames)
+ {
+ if(m_audioInputs.front().size() < _frames)
+ {
+ for (auto& input : m_audioInputs)
+ input.resize(_frames);
+ }
+
+ if(m_delayedAudioIn.size() < _frames + 1)
+ m_delayedAudioIn.resize(_frames + 1);
+
+ if(m_audioOutputs.front().size() < _frames)
+ {
+ for (auto& output : m_audioOutputs)
+ output.resize(_frames);
+ }
+
+ if(m_dummyInput.size() < _frames)
+ m_dummyInput.resize(_frames);
+ if(m_dummyOutput.size() < _frames)
+ m_dummyOutput.resize(_frames);
+ }
+}
diff --git a/source/xtLib/xtHardware.h b/source/xtLib/xtHardware.h
@@ -0,0 +1,109 @@
+#pragma once
+
+#include <string>
+
+#include "xtDSP.h"
+#include "xtRom.h"
+#include "xtUc.h"
+
+#include "dsp56kEmu/dspthread.h"
+
+#include "../synthLib/midiTypes.h"
+
+#include "dsp56kEmu/ringbuffer.h"
+
+#include "../wLib/wMidi.h"
+
+namespace xt
+{
+ class XtUc;
+
+ class Hardware
+ {
+ static constexpr uint32_t g_dspCount = 1;
+
+ public:
+ explicit Hardware(std::string _romFilename);
+ ~Hardware();
+
+ void process();
+
+ XtUc& getUC() { return m_uc; }
+ DSP& getDSP(uint32_t _index = 0) { return m_dsps[_index]; }
+ uint64_t getUcCycles() const { return m_uc.getCycles(); }
+
+ auto& getAudioInputs() { return m_audioInputs; }
+ auto& getAudioOutputs() { return m_audioOutputs; }
+
+ void sendMidi(const synthLib::SMidiEvent& _ev);
+ void receiveMidi(std::vector<uint8_t>& _data);
+
+ dsp56k::DSPThread& getDspThread(uint32_t _index = 0) { return m_dsps[_index].thread(); }
+
+ void processAudio(uint32_t _frames, uint32_t _latency = 0);
+
+ void ensureBufferSize(uint32_t _frames);
+
+ void ucThreadTerminated()
+ {
+ resumeDSP();
+ }
+
+ bool isValid() const { return m_rom.isValid(); }
+
+ bool isBootCompleted() const { return m_bootCompleted; }
+ void resetMidiCounter();
+
+ void ucYieldLoop(const std::function<bool()>& _continue);
+ void initVoiceExpansion();
+
+ private:
+ void setupEsaiListener();
+ void hdiProcessUCtoDSPNMIIrq();
+ void processUcCycle();
+ void haltDSP();
+ void resumeDSP();
+ void syncUcToDSP();
+ void processMidiInput();
+
+ const std::string m_romFileName;
+
+ const Rom m_rom;
+
+ bool m_requestNMI = false;
+
+ // timing
+ uint32_t m_esaiFrameIndex = 0;
+ uint32_t m_lastEsaiFrameIndex = 0;
+ int64_t m_remainingUcCycles = 0;
+ double m_remainingUcCyclesD = 0;
+
+ XtUc m_uc;
+ std::array<DSP,g_dspCount> m_dsps;
+
+ wLib::Midi m_midi;
+ dsp56k::RingBuffer<synthLib::SMidiEvent, 1024, true> m_midiIn;
+ uint32_t m_midiOffsetCounter = 0;
+
+ std::vector<dsp56k::TWord> m_delayedAudioIn;
+
+ std::vector<dsp56k::TWord> m_dummyInput;
+ std::vector<dsp56k::TWord> m_dummyOutput;
+
+ TAudioInputs m_audioInputs;
+ TAudioOutputs m_audioOutputs;
+
+ std::mutex m_esaiFrameAddedMutex;
+ std::condition_variable m_esaiFrameAddedCv;
+
+ std::mutex m_requestedFramesAvailableMutex;
+ std::condition_variable m_requestedFramesAvailableCv;
+ size_t m_requestedFrames = 0;
+
+ bool m_haltDSP = false;
+ std::condition_variable m_haltDSPcv;
+ std::mutex m_haltDSPmutex;
+ bool m_processAudio = false;
+ bool m_bootCompleted = false;
+ };
+}
diff --git a/source/xtLib/xtLcd.cpp b/source/xtLib/xtLcd.cpp
@@ -0,0 +1,32 @@
+#include "xtLcd.h"
+
+namespace xt
+{
+ Lcd::Lcd()
+ {
+ m_lcdData.fill(' ');
+ }
+
+ void Lcd::resetWritePos()
+ {
+ m_lcdWritePos = 0;
+ }
+
+ bool Lcd::writeCharacter(const char _c)
+ {
+ if(m_lcdData[m_lcdWritePos] == _c)
+ {
+ ++m_lcdWritePos;
+ return false;
+ }
+ m_lcdData[m_lcdWritePos++] = _c;
+ return true;
+ }
+
+ std::string Lcd::toString() const
+ {
+ const std::string lineA(m_lcdData.data(), 40);
+ const std::string lineB(m_lcdData.data() + 40, 40);
+ return lineA + '\n' + lineB;
+ }
+}
diff --git a/source/xtLib/xtLcd.h b/source/xtLib/xtLcd.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <array>
+#include <string>
+
+namespace xt
+{
+ class Lcd
+ {
+ public:
+ Lcd();
+
+ void resetWritePos();
+ bool writeCharacter(char _c);
+ const auto& getData() const { return m_lcdData; }
+ std::string toString() const;
+
+ private:
+ uint16_t m_lcdWritePos = 0;
+ std::array<char, 40*2> m_lcdData;
+ };
+}
diff --git a/source/xtLib/xtLeds.cpp b/source/xtLib/xtLeds.cpp
@@ -0,0 +1,5 @@
+#include "xtLeds.h"
+
+namespace xt
+{
+}
diff --git a/source/xtLib/xtLeds.h b/source/xtLib/xtLeds.h
@@ -0,0 +1,14 @@
+#pragma once
+
+namespace xt
+{
+ enum LedType
+ {
+ Midi, Grp1, Grp2, Grp3,
+ Sync, Grp4, Grp5, Play,
+ Glide, FilAmp,
+ FreeEnv, Wave5, Wave1,
+
+ Count
+ };
+}
diff --git a/source/xtLib/xtMidiTypes.h b/source/xtLib/xtMidiTypes.h
@@ -0,0 +1,115 @@
+#pragma once
+
+#include <cstdint>
+
+#include "../wLib/wMidiTypes.h"
+
+namespace xt
+{
+ enum MidiHeaderByte : uint8_t
+ {
+ IdMw2 = 0x0e,
+ };
+
+ enum class SysexCommand : uint8_t
+ {
+ Invalid = 0xff,
+
+ SingleRequest = 0x00, SingleDump = 0x10, SingleParameterChange = 0x20, SingleStore = 0x30, SingleRecall = 0x40, SingleCompare = 0x50,
+ MultiRequest = 0x01, MultiDump = 0x11, MultiParameterChange = 0x21, MultiStore = 0x31, MultiRecall = 0x41, MultiCompare = 0x51,
+ WaveRequest = 0x02, WaveDump = 0x12, WaveParameterChange = 0x22, WaveStore = 0x32, WaveRecall = 0x42, WaveCompare = 0x52,
+ WaveCtlRequest = 0x03, WaveCtlDump = 0x13, WaveCtlParameterChange = 0x23, WaveCtlStore = 0x33, WaveCtlRecall = 0x43, WaveCtlCompare = 0x53,
+ GlobalRequest = 0x04, GlobalDump = 0x14, GlobalParameterChange = 0x24, GlobalStore = 0x34, GlobalRecall = 0x44, GlobalCompare = 0x54,
+ DisplayRequest = 0x05, DisplayDump = 0x15, DisplayParameterChange = 0x25, DisplayStore = 0x35, DisplayRecall = 0x45, DisplayCompare = 0x55,
+ RemoteCtlRequest = 0x06, RemoteCtlDump = 0x16, RemoteCtlParameterChange = 0x26, RemoteCtlStore = 0x36, RemoteCtlRecall = 0x46, RemoteCtlCompare = 0x56,
+ ModeRequest = 0x07, ModeDump = 0x17, ModeParameterChange = 0x27, ModeStore = 0x37, ModeRecall = 0x47, ModeCompare = 0x57,
+ InfoRequest = 0x08, InfoDump = 0x18, InfoParameterChange = 0x28, InfoStore = 0x38, InfoRecall = 0x48, InfoCompare = 0x58,
+
+ EmuLCD = 0x60,
+ EmuLEDs = 0x61,
+ EmuButtons = 0x62,
+ EmuRotaries = 0x63
+ };
+
+ enum class LocationH : uint8_t
+ {
+ // Single Dump
+ SingleBankA = 0x00,
+ SingleBankB = 0x01,
+ AllSingles = 0x10,
+ SingleEditBufferSingleMode = 0x20,
+ SingleEditBufferMultiMode = 0x30,
+
+ // Multi Dump
+ MultiBankA = 0x00,
+ AllMultis = 0x10,
+ MultiDumpMultiEditBuffer = 0x20,
+ };
+
+ enum SysexIndex
+ {
+ // first parameter of a dump
+ IdxSingleParamFirst = 7,
+ IdxSingleChecksumStart = IdxSingleParamFirst,
+ IdxMultiParamFirst = IdxSingleParamFirst,
+ IdxGlobalParamFirst = wLib::IdxBuffer,
+ IdxModeParamFirst = wLib::IdxBuffer,
+
+ IdxSingleParamIndexH = wLib::IdxBuffer + 1,
+ IdxSingleParamIndexL = IdxSingleParamIndexH + 1,
+ IdxSingleParamValue = IdxSingleParamIndexL + 1,
+
+ IdxMultiParamIndexH = wLib::IdxBuffer,
+ IdxMultiParamIndexL = IdxMultiParamIndexH + 1,
+ IdxMultiParamValue = IdxMultiParamIndexL + 1,
+
+ IdxGlobalParamIndexH = wLib::IdxBuffer,
+ IdxGlobalParamIndexL = IdxGlobalParamIndexH + 1,
+ IdxGlobalParamValue = IdxGlobalParamIndexL + 1,
+
+ IdxModeParamIndexH = wLib::IdxBuffer,
+ IdxModeParamIndexL = IdxModeParamIndexH,
+ IdxModeParamValue = wLib::IdxBuffer
+ };
+
+ enum class GlobalParameter
+ {
+ Reserved0,
+ Version,
+ StartupSoundbank,
+ StartupSoundNum,
+ MidiChannel,
+ ProgramChangeMode,
+ DeviceId,
+ BendRange,
+ ControllerW,
+ ControllerX,
+ ControllerY,
+ ControllerZ,
+ MainVolume,
+ Reserved13,
+ Reserved14,
+ Transpose,
+ MasterTune,
+ DisplayTimeout,
+ LcdContrast,
+ Reserved19,
+ Reserved20,
+ Reserved21,
+ Reserved22,
+ StartupMultiNumber,
+ ArpNoteOutChannel,
+ MidiClockOutput,
+ ParameterSend,
+ ParameterReceive,
+ InputGain,
+ Reserved29,
+ Reserved30,
+ Reserved31
+ };
+
+ enum class ModeParameter
+ {
+ Mode = 0 // 0 = Single, 1 = Multi
+ };
+}
diff --git a/source/xtLib/xtPic.cpp b/source/xtLib/xtPic.cpp
@@ -0,0 +1,175 @@
+#include "xtPic.h"
+
+#include "xtLcd.h"
+
+#include "../mc68k/mc68k.h"
+#include "../mc68k/logging.h"
+
+#include <cassert>
+#include <cstdint>
+
+namespace xt
+{
+ Pic::Pic(mc68k::Mc68k& _uc, Lcd& _lcd)
+ {
+ _uc.getQSM().setSpiWriteCallback([&](const uint16_t _data, const uint8_t _index)->uint16_t
+ {
+ constexpr uint32_t transmitRamAddr = static_cast<uint32_t>(mc68k::PeriphAddress::TransmitRam0);
+ constexpr uint32_t receiveRamAddr = static_cast<uint32_t>(mc68k::PeriphAddress::ReceiveRam0);
+ constexpr uint32_t receiveRamSize = transmitRamAddr - receiveRamAddr;
+ static_assert(receiveRamSize == 32);
+
+ if(_index == 0)
+ {
+ // $30 enc 3 switches to multimode?
+ // $35 = Osc 1 Semitone
+ // $36 = Startwave1
+ // $37 = Mix Wave1
+
+ const uint8_t buttonA = m_spiButtons & 0x3f;
+ const uint8_t buttonB = (m_spiButtons>>1) & 0x20;
+
+ const uint16_t anus[] =
+ {
+ 'M', // don't care
+ 'W', // don't care
+ 0x30, // encoder address for encoders 1-4, range 0x30 - 0x57
+ 0, // encoder value 1
+ 0, // encoder value 2
+ 0, // encoder value 3
+ 0, // encoder value 4
+ 0x38, // encoder address for encoders 5-8, range 0x30 - 0x57
+ 0, // encoder value 5
+ 0, // encoder value 6
+ 0, // encoder value 7
+ 0, // encoder value 8
+ buttonA, // Button flags A
+ buttonB, // Button flags B
+ 255, // main volume pot
+ 'x' // We are an XT
+ };
+
+ for(uint32_t a=0; a<std::size(anus); ++a)
+ _uc.write16(a*2 + receiveRamAddr, anus[a]);
+ }
+
+ if(_index == 0)
+ {
+ m_picCommand0 = _data;
+ return 0;
+ }
+ else if(_index == 1)
+ {
+ if(m_picCommand0 == 0x10)
+ {
+ if(_data == 0x80)
+ _lcd.resetWritePos();
+
+ return 0;
+ }
+ }
+
+ // PIC Command 0x10 = new write, everything is for the LCD
+ // Pic Command 0x18 = continue to write and update LEDs too
+
+ assert(m_picCommand0 == 0x10 || m_picCommand0 == 0x18);
+
+ uint8_t lcdChar = m_picCommand0 == 0x10 ? static_cast<uint8_t>(_data) : 0;
+
+ if(m_picCommand0 == 0x18)
+ {
+ if(_index == 7)
+ m_picHasLedUpdate = _data == 0x14;
+
+ if(m_picHasLedUpdate)
+ {
+ switch (_index)
+ {
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ lcdChar = static_cast<uint8_t>(_data);
+ break;
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ {
+ const auto oldLedState = m_ledState;
+ m_ledState &= ~(0xff << ((_index-8) * 8));
+ m_ledState |= (_data & 0xff) << ((_index-8) * 8);
+ if(oldLedState != m_ledState)
+ {
+// MCLOG("LEDs: " << MCHEXN(m_ledState, 4));
+ m_cbkLedsDirty();
+ }
+ }
+ break;
+ default:
+ return 0;
+ }
+ }
+ else
+ {
+ lcdChar = static_cast<uint8_t>(_data);
+ }
+ }
+ if(lcdChar)
+ {
+ const auto ch = static_cast<char>(lcdChar);
+
+ if(_lcd.writeCharacter(ch))
+ {
+ m_cbkLcdDirty();
+ MCLOG("LCD:\n" << _lcd.toString());
+ }
+
+ return 0;
+ }
+ return 0;
+ });
+ }
+
+ Pic::~Pic() = default;
+
+ void Pic::setButton(const ButtonType _type, const bool _pressed)
+ {
+ const auto buttonMask = static_cast<uint8_t>(1 << static_cast<uint8_t>(_type));
+
+ // inverted on purpose, pressed buttons are 0, released ones 1
+ if(_pressed)
+ m_spiButtons &= ~buttonMask;
+ else
+ m_spiButtons |= buttonMask;
+ }
+
+ bool Pic::getButton(ButtonType _button) const
+ {
+ const auto buttonMask = static_cast<uint8_t>(1 << static_cast<uint8_t>(_button));
+
+ // inverted on purpose, pressed buttons are 0, released ones 1
+ return (m_spiButtons & buttonMask) == 0;
+ }
+
+ void Pic::setLcdDirtyCallback(const DirtyCallback& _cbk)
+ {
+ m_cbkLcdDirty = _cbk;
+ if(!m_cbkLcdDirty)
+ m_cbkLcdDirty = [] {};
+ }
+
+ void Pic::setLedsDirtyCallback(const DirtyCallback& _cbk)
+ {
+ m_cbkLedsDirty = _cbk;
+ if(!m_cbkLedsDirty)
+ m_cbkLedsDirty = [] {};
+ }
+
+ bool Pic::getLedState(const LedType _led) const
+ {
+ return m_ledState & (1 << _led);
+ }
+}
diff --git a/source/xtLib/xtPic.h b/source/xtLib/xtPic.h
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <cstdint>
+#include <functional>
+
+#include "xtButtons.h"
+#include "xtLeds.h"
+
+namespace mc68k
+{
+ class Mc68k;
+}
+
+namespace xt
+{
+ class Lcd;
+
+ class Pic
+ {
+ public:
+ using DirtyCallback = std::function<void()>;
+
+ Pic(mc68k::Mc68k& _uc, Lcd& _lcd);
+ ~Pic();
+
+ void setButton(ButtonType _type, bool _pressed);
+ bool getButton(ButtonType _button) const;
+
+ void setLcdDirtyCallback(const DirtyCallback& _cbk);
+ void setLedsDirtyCallback(const DirtyCallback& _cbk);
+ bool getLedState(LedType _led) const;
+
+ private:
+ uint16_t m_picCommand0 = 0;
+ bool m_picHasLedUpdate = false;
+
+ uint8_t m_spiButtons = 0xff;
+ uint32_t m_ledState = 0;
+ DirtyCallback m_cbkLedsDirty = [] {};
+ DirtyCallback m_cbkLcdDirty = [] {};
+ };
+}
diff --git a/source/xtLib/xtRom.cpp b/source/xtLib/xtRom.cpp
diff --git a/source/xtLib/xtRom.h b/source/xtLib/xtRom.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include "xtTypes.h"
+
+#include "../wLib/wRom.h"
+
+namespace xt
+{
+ class Rom : public wLib::ROM
+ {
+ public:
+ static constexpr uint32_t Size = g_romSize;
+
+ Rom(const std::string& _filename, const uint8_t* _data) : ROM(_filename, Size, _data)
+ {
+ }
+
+ uint32_t getSize() const override
+ {
+ return Size;
+ }
+ };
+}
diff --git a/source/xtLib/xtState.cpp b/source/xtLib/xtState.cpp
@@ -0,0 +1,710 @@
+#include "xtState.h"
+
+#include <cassert>
+
+#include "xtMidiTypes.h"
+#include "xt.h"
+
+#include "../synthLib/os.h"
+#include "../synthLib/midiToSysex.h"
+#include "../synthLib/midiBufferParser.h"
+#include "dsp56kEmu/logging.h"
+
+namespace xt
+{
+ static_assert(std::size(State::Dumps) == static_cast<uint32_t>(State::DumpType::Count), "data definition missing");
+
+ State::State(Xt& _xt) : m_xt(_xt)
+ {
+ }
+
+ bool State::loadState(const SysEx& _sysex)
+ {
+ std::vector<std::vector<uint8_t>> messages;
+ synthLib::MidiToSysex::splitMultipleSysex(messages, _sysex);
+
+ if(messages.empty())
+ return false;
+
+ Responses nop;
+
+ for (const auto& message : messages)
+ receive(nop, message, Origin::External);
+
+ // if device receives a multi, it switches to multi mode. Switch back to single mode if single mode was requested
+// if(getGlobalParameter(GlobalParameter::SingleMultiMode) == 0)
+// sendGlobalParameter(GlobalParameter::SingleMultiMode, 0);
+
+ return true;
+ }
+
+ bool State::receive(Responses& _responses, const synthLib::SMidiEvent& _data, Origin _sender)
+ {
+ if(!_data.sysex.empty())
+ {
+ return receive(_responses, _data.sysex, _sender);
+ }
+
+ if (_sender == Origin::Device)
+ LOG("Recv: " << HEXN(_data.a, 2) << ' ' << HEXN(_data.b, 2) << ' ' << HEXN(_data.c, 2));
+
+ switch(_data.a & 0xf0)
+ {
+ case synthLib::M_CONTROLCHANGE:
+ switch(_data.b)
+ {
+ case synthLib::MC_BANKSELECTMSB:
+ m_lastBankSelectMSB = _data;
+ break;
+ case synthLib::MC_BANKSELECTLSB:
+ m_lastBankSelectLSB = _data;
+ break;
+ default:
+ return false;
+ }
+ break;
+ case synthLib::M_PROGRAMCHANGE:
+ /*
+ switch(static_cast<BankSelectLSB>(m_lastBankSelectLSB.c))
+ {
+ case BankSelectLSB::BsDeprecatedSingleBankA:
+ case BankSelectLSB::BsDeprecatedSingleBankB:
+ case BankSelectLSB::BsDeprecatedSingleBankC:
+ case BankSelectLSB::BsSingleBankA:
+ case BankSelectLSB::BsSingleBankB:
+ case BankSelectLSB::BsSingleBankC:
+ if(getGlobalParameter(GlobalParameter::SingleMultiMode) == 0)
+ requestSingle(LocationH::SingleEditBufferSingleMode, MidiSoundLocation::EditBufferCurrentSingle);
+ break;
+ case BankSelectLSB::BsMultiBank:
+ if(getGlobalParameter(GlobalParameter::SingleMultiMode) != 0)
+ requestMulti(LocationH::MultiEditBuffer, 0);
+ break;
+ default:
+ return false;
+ }
+ */
+ break;
+ default:
+ return false;
+ }
+ return false;
+ }
+
+ bool State::receive(Responses& _responses, const SysEx& _data, Origin _sender)
+ {
+ const auto cmd = getCommand(_data);
+
+ if(cmd == SysexCommand::Invalid)
+ return false;
+
+ m_sender = _sender;
+ m_isEditBuffer = false;
+
+ switch (cmd)
+ {
+ case SysexCommand::SingleRequest: return getDump(DumpType::Single, _responses, _data);
+ case SysexCommand::MultiRequest: return getDump(DumpType::Multi,_responses, _data);
+ case SysexCommand::GlobalRequest: return getDump(DumpType::Global, _responses, _data);
+ case SysexCommand::ModeRequest: return getDump(DumpType::Mode, _responses, _data);
+
+ case SysexCommand::SingleDump: return parseDump(DumpType::Single, _data);
+ case SysexCommand::MultiDump: return parseDump(DumpType::Multi, _data);
+ case SysexCommand::GlobalDump: return parseDump(DumpType::Global, _data);
+ case SysexCommand::ModeDump: return parseDump(DumpType::Mode, _data);
+
+ case SysexCommand::SingleParameterChange: return modifyDump(DumpType::Single, _data);
+ case SysexCommand::MultiParameterChange: return modifyDump(DumpType::Multi, _data);
+ case SysexCommand::GlobalParameterChange: return modifyDump(DumpType::Global, _data);
+ case SysexCommand::ModeParameterChange: return modifyDump(DumpType::Mode, _data);
+
+/* case SysexCommand::EmuLCD:
+ case SysexCommand::EmuLEDs:
+ case SysexCommand::EmuButtons:
+ case SysexCommand::EmuRotaries:
+ return false;
+*/ default:
+ return false;
+ }
+ }
+
+ void State::createInitState()
+ {
+ // request global settings and wait for them. Once they are valid, send init state
+ requestGlobal();
+// m_xt.sendMidi({0xf0, IdWaldorf, IdMicroQ, IdDeviceOmni, static_cast<uint8_t>(SysexCommand::ModeRequest), 0xf7});
+
+ synthLib::MidiBufferParser parser;
+ Responses unused;
+ std::vector<uint8_t> midi;
+ std::vector<synthLib::SMidiEvent> events;
+
+ while(!isValid(m_global))// || !isValid(m_mode))
+ {
+ m_xt.process(8);
+ midi.clear();
+ m_xt.receiveMidi(midi);
+ parser.write(midi);
+
+ events.clear();
+ parser.getEvents(events);
+
+ for (const auto & event : events)
+ {
+ if(!event.sysex.empty())
+ {
+ if(!receive(unused, event.sysex, Origin::Device))
+ assert(false);
+ }
+ }
+ }
+
+ auto setParam = [&](const GlobalParameter _param, const uint8_t _value)
+ {
+ setGlobalParameter(_param, _value);
+ };
+
+ setParam(GlobalParameter::StartupSoundbank, 0); // First bank
+ setParam(GlobalParameter::StartupSoundNum, 0); // First sound
+ setParam(GlobalParameter::StartupMultiNumber, 0); // First Multi
+
+ setParam(GlobalParameter::MidiChannel, 0); // omni
+ setParam(GlobalParameter::ProgramChangeMode, 0); // single
+ setParam(GlobalParameter::MasterTune, 64); // 440 Hz
+ setParam(GlobalParameter::Transpose, 64); // +/- 0
+ setParam(GlobalParameter::ParameterSend, 2); // SysEx
+ setParam(GlobalParameter::ParameterReceive, 1); // on
+ setParam(GlobalParameter::ArpNoteOutChannel, 0); // off
+ setParam(GlobalParameter::MidiClockOutput, 0); // off
+ setParam(GlobalParameter::MidiChannel, 0); // omni
+ setParam(GlobalParameter::DeviceId, 0); // 0
+ setParam(GlobalParameter::InputGain, 3); // 4
+
+ receive(unused, convertTo(m_global), Origin::External);
+
+ // send default multi
+// std::vector<uint8_t> defaultMultiData;
+// createSequencerMultiData(defaultMultiData);
+// sendMulti(defaultMultiData);
+
+ std::vector<uint8_t> sysex;
+
+ // accept files up to 300k as larger files might be the OS
+ const auto midifile = synthLib::findFile(".mid", 0, 300 * 1024);
+ if(!midifile.empty())
+ {
+ synthLib::MidiToSysex::readFile(sysex, midifile.c_str());
+ }
+
+ if(sysex.empty())
+ {
+ const auto syxFile = synthLib::findFile(".syx", 0, 300 * 1024);
+ if(!syxFile.empty())
+ synthLib::readFile(sysex, syxFile);
+ }
+
+ if (!sysex.empty())
+ {
+ std::vector<std::vector<uint8_t>> messages;
+ synthLib::MidiToSysex::splitMultipleSysex(messages, sysex);
+
+ for (const auto& message : messages)
+ {
+ switch (getCommand(message))
+ {
+ case SysexCommand::SingleDump:
+ case SysexCommand::MultiDump:
+ {
+ auto m = message;
+ m[wLib::IdxDeviceId] = wLib::IdDeviceOmni;
+ loadState(m);
+ }
+ break;
+ default:;
+ }
+ }
+ }
+
+ // switch to Single mode as the multi dump causes it to go to Multi mode
+// sendGlobalParameter(GlobalParameter::SingleMultiMode, 0);
+
+ if(isValid(m_romSingles[0]))
+ {
+ auto dump = convertTo(m_romSingles[0]);
+
+ dump[wLib::IdxBuffer] = static_cast<uint8_t>(LocationH::SingleEditBufferSingleMode);
+ dump[wLib::IdxLocation] = 0;
+
+ forwardToDevice(dump);
+ }
+ }
+
+ bool State::getState(std::vector<uint8_t>& _state, synthLib::StateType _type) const
+ {
+ append(_state, m_global, wLib::IdxCommand);
+
+ append(_state, m_currentMulti, wLib::IdxCommand);
+
+ const auto multiMode = isMultiMode();
+
+ for(size_t i=0; i<m_currentMultiSingles.size(); ++i)
+ {
+ auto s = (multiMode || i >= m_currentInstrumentSingles.size()) ? m_currentMultiSingles[i] : m_currentInstrumentSingles[i];
+ append(_state, s, IdxSingleChecksumStart);
+ }
+
+ return !_state.empty();
+ }
+
+ bool State::setState(const std::vector<uint8_t>& _state, synthLib::StateType _type)
+ {
+ return loadState(_state);
+ }
+
+ bool State::parseSingleDump(const SysEx& _data)
+ {
+ Single single;
+
+ if(!convertTo(single, _data))
+ return false;
+
+ const auto buf = static_cast<LocationH>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
+
+ Single* dst = getSingle(buf, loc);
+
+ if(!dst)
+ return false;
+ *dst = single;
+ return true;
+ }
+
+ bool State::parseMultiDump(const SysEx& _data)
+ {
+ Multi multi;
+
+ if(!convertTo(multi, _data))
+ return false;
+
+ const auto buf = static_cast<LocationH>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
+
+ auto* m = getMulti(buf, loc);
+ if(!m)
+ return false;
+ *m = multi;
+ return true;
+ }
+
+ bool State::parseGlobalDump(const SysEx& _data)
+ {
+ return convertTo(m_global, _data);
+ }
+
+ bool State::parseModeDump(const SysEx& _data)
+ {
+ return convertTo(m_mode, _data);
+ }
+
+ bool State::modifySingle(const SysEx& _data)
+ {
+ auto* p = getSingleParameter(_data);
+ if(!p)
+ return false;
+ *p = _data[IdxSingleParamValue];
+ return true;
+ }
+
+ bool State::modifyMulti(const SysEx& _data)
+ {
+ auto* p = getMultiParameter(_data);
+ if(!p)
+ return false;
+
+ *p = _data[IdxMultiParamValue];
+ return true;
+ }
+
+ bool State::modifyGlobal(const SysEx& _data)
+ {
+ auto* p = getGlobalParameter(_data);
+ if(!p)
+ return false;
+
+ if(*p == _data[IdxGlobalParamValue])
+ return true;
+
+ *p = _data[IdxGlobalParamValue];
+
+ return true;
+ }
+
+ bool State::modifyMode(const SysEx& _data)
+ {
+ auto* p = getModeParameter(_data);
+ if(!p)
+ return false;
+
+ *p = _data[IdxModeParamValue];
+
+ // if the play mode is changed, request the edit buffer for the first single again, because on the xt, that edit buffer is shared between multi & single
+
+ requestSingle(isMultiMode() ? LocationH::SingleEditBufferMultiMode : LocationH::SingleEditBufferSingleMode, 0);
+
+ return true;
+ }
+
+ namespace
+ {
+ template<size_t Size>
+ uint8_t* getParameter(std::array<uint8_t, Size>& _dump, const SysEx& _data, State::DumpType _type)
+ {
+ const auto& dump = State::Dumps[static_cast<uint32_t>(_type)];
+
+ if(dump.idxParamIndexH >= _data.size() || dump.idxParamIndexL >= _data.size())
+ return nullptr;
+
+ const auto i = dump.firstParamIndex + ((static_cast<uint32_t>(_data[dump.idxParamIndexH]) << 7) | static_cast<uint32_t>(_data[dump.idxParamIndexL]));
+
+ if(i > _dump.size())
+ return nullptr;
+ return &_dump[i];
+ }
+ }
+
+ uint8_t* State::getSingleParameter(const SysEx& _data)
+ {
+ const auto loc = _data[wLib::IdxBuffer];
+
+ Single* s = getSingle(isMultiMode() ? LocationH::SingleEditBufferMultiMode : LocationH::SingleEditBufferSingleMode, loc);
+ if(!s)
+ return nullptr;
+ return getParameter(*s, _data, DumpType::Single);
+ }
+
+ uint8_t* State::getMultiParameter(const SysEx& _data)
+ {
+ return getParameter(m_currentMulti, _data, DumpType::Multi);
+ }
+
+ uint8_t* State::getGlobalParameter(const SysEx& _data)
+ {
+ return getParameter(m_global, _data, DumpType::Global);
+ }
+
+ uint8_t* State::getModeParameter(const SysEx& _data)
+ {
+ return getParameter(m_mode, _data, DumpType::Mode);
+ }
+
+ bool State::getSingle(Responses& _responses, const SysEx& _data)
+ {
+ const auto buf = static_cast<LocationH>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
+
+ const auto* s = getSingle(buf, loc);
+ if(!s || !isValid(*s))
+ return false;
+ _responses.push_back(convertTo(*s));
+ return true;
+ }
+
+ State::Single* State::getSingle(LocationH _buf, uint8_t _loc)
+ {
+ switch (_buf)
+ {
+ case LocationH::SingleBankA:
+ if(_loc >= 128)
+ return nullptr;
+ return &m_romSingles[_loc];
+ case LocationH::SingleBankB:
+ if(_loc >= 128)
+ return nullptr;
+ return &m_romSingles[_loc + 100];
+ case LocationH::SingleEditBufferSingleMode:
+ m_isEditBuffer = true;
+ return m_currentInstrumentSingles.data();
+ case LocationH::SingleEditBufferMultiMode:
+ {
+ m_isEditBuffer = true;
+ if(isMultiMode())
+ {
+ if(_loc >= m_currentMultiSingles.size())
+ return nullptr;
+ return &m_currentMultiSingles[_loc];
+ }
+ if(_loc < m_currentInstrumentSingles.size())
+ return &m_currentInstrumentSingles[_loc];
+ if (_loc < m_currentMultiSingles.size())
+ return &m_currentMultiSingles[_loc];
+ return nullptr;
+ }
+ default:
+ return nullptr;
+ }
+ }
+
+ bool State::getMulti(Responses& _responses, const SysEx& _data)
+ {
+ const auto buf = static_cast<LocationH>(_data[wLib::IdxBuffer]);
+ const auto loc = _data[wLib::IdxLocation];
+
+ const auto* m = getMulti(buf, loc);
+ if(!m || !isValid(*m))
+ return false;
+ _responses.push_back(convertTo(*m));
+ return true;
+ }
+
+ State::Multi* State::getMulti(LocationH buf, uint8_t loc)
+ {
+ switch (buf)
+ {
+ case LocationH::MultiDumpMultiEditBuffer:
+ m_isEditBuffer = true;
+ return &m_currentMulti;
+ case LocationH::MultiBankA:
+ if(loc >= m_romMultis.size())
+ return nullptr;
+ return &m_romMultis[loc];
+ default:
+ return nullptr;
+ }
+ }
+
+ bool State::getGlobal(Responses& _responses)
+ {
+ const auto* g = getGlobal();
+ if(g == nullptr)
+ return false;
+ _responses.push_back(convertTo(*g));
+ return true;
+ }
+
+ State::Global* State::getGlobal()
+ {
+ if(isValid(m_global))
+ {
+ m_isEditBuffer = true;
+ return &m_global;
+ }
+ return nullptr;
+ }
+
+ bool State::getMode(Responses& _responses)
+ {
+ const auto* m = getMode();
+ if(m == nullptr)
+ return false;
+ _responses.push_back(convertTo(*m));
+ return true;
+ }
+
+ State::Mode* State::getMode()
+ {
+ if(isValid(m_mode))
+ {
+ m_isEditBuffer = true;
+ return &m_mode;
+ }
+ return nullptr;
+ }
+
+ bool State::getDump(DumpType _type, Responses& _responses, const SysEx& _data)
+ {
+ bool res;
+
+ switch (_type)
+ {
+ case DumpType::Single: res = getSingle(_responses, _data); break;
+ case DumpType::Multi: res = getMulti(_responses, _data); break;
+ case DumpType::Global: res = getGlobal(_responses); break;
+ case DumpType::Mode: res = getMode(_responses); break;
+ default:
+ return false;
+ }
+
+ if(!res)
+ forwardToDevice(_data);
+ return true;
+ }
+
+ bool State::parseDump(DumpType _type, const SysEx& _data)
+ {
+ bool res;
+ switch (_type)
+ {
+ case DumpType::Single: res = parseSingleDump(_data); break;
+ case DumpType::Multi: res = parseMultiDump(_data); break;
+ case DumpType::Global: res = parseGlobalDump(_data); break;
+ case DumpType::Mode: res = parseModeDump(_data); break;
+ default:
+ return false;
+ }
+
+ if(res)
+ forwardToDevice(_data);
+ return res;
+ }
+
+ bool State::modifyDump(DumpType _type, const SysEx& _data)
+ {
+ bool res;
+ switch (_type)
+ {
+ case DumpType::Single: res = modifySingle(_data); break;
+ case DumpType::Multi: res = modifyMulti(_data); break;
+ case DumpType::Global: res = modifyGlobal(_data); break;
+ case DumpType::Mode: res = modifyMode(_data); break;
+ default:
+ return false;
+ }
+ if(res)
+ forwardToDevice(_data);
+ return res;
+ }
+
+ uint8_t State::getGlobalParameter(const GlobalParameter _parameter) const
+ {
+ return m_global[static_cast<uint32_t>(_parameter) + IdxGlobalParamFirst];
+ }
+
+ void State::setGlobalParameter(GlobalParameter _parameter, uint8_t _value)
+ {
+ m_global[static_cast<uint32_t>(_parameter) + IdxGlobalParamFirst] = _value;
+ }
+
+ uint8_t State::getModeParameter(const ModeParameter _parameter) const
+ {
+ return m_mode[static_cast<uint32_t>(_parameter) + IdxModeParamFirst];
+ }
+
+ SysexCommand State::getCommand(const SysEx& _data)
+ {
+ if (_data.size() < 5)
+ return SysexCommand::Invalid;
+
+ if (_data.front() != 0xf0 || _data.back() != 0xf7)
+ return SysexCommand::Invalid;
+
+ if (_data[wLib::IdxIdWaldorf] != wLib::IdWaldorf || _data[wLib::IdxIdMachine] != IdMw2)
+ return SysexCommand::Invalid;
+
+ return static_cast<SysexCommand>(_data[wLib::IdxCommand]);
+ }
+
+ void State::forwardToDevice(const SysEx& _data) const
+ {
+ if(m_sender != Origin::External)
+ return;
+
+ sendSysex(_data);
+ }
+
+ void State::requestGlobal() const
+ {
+ sendSysex({0xf0, wLib::IdWaldorf, IdMw2, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::GlobalRequest), 0xf7});
+ }
+
+ void State::requestSingle(LocationH _buf, uint8_t _location) const
+ {
+ sendSysex({0xf0, wLib::IdWaldorf, IdMw2, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::SingleRequest), static_cast<uint8_t>(_buf), static_cast<uint8_t>(_location), 0xf7});
+ }
+
+ void State::requestMulti(LocationH _buf, uint8_t _location) const
+ {
+ sendSysex({0xf0, wLib::IdWaldorf, IdMw2, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::MultiRequest), static_cast<uint8_t>(_buf), _location, 0xf7});
+ }
+
+ inline void State::sendMulti(const std::vector<uint8_t>& _multiData) const
+ {
+ std::vector<uint8_t> data = { 0xf0, wLib::IdWaldorf, IdMw2, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::MultiDump), static_cast<uint8_t>(LocationH::MultiBankA), 0};
+ data.insert(data.end(), _multiData.begin(), _multiData.end());
+
+ uint8_t checksum = 0;
+ for(size_t i=4; i<data.size(); ++i)
+ checksum += data[i];
+ data.push_back(checksum & 0x7f);
+ data.push_back(0xf7);
+ sendSysex(data);
+ }
+
+ void State::sendGlobalParameter(GlobalParameter _param, uint8_t _value)
+ {
+ setGlobalParameter(_param, _value);
+
+ const auto p = static_cast<uint8_t>(_param);
+
+ sendSysex({0xf0, wLib::IdWaldorf, IdMw2, wLib::IdDeviceOmni, static_cast<uint8_t>(SysexCommand::GlobalParameterChange),
+ static_cast<uint8_t>(p >> 7), static_cast<uint8_t>(p & 0x7f), _value, 0xf7});
+ }
+
+ void State::sendSysex(const std::initializer_list<uint8_t>& _data) const
+ {
+ synthLib::SMidiEvent e;
+ e.sysex = _data;
+ m_xt.sendMidiEvent(e);
+ }
+
+ void State::sendSysex(const SysEx& _data) const
+ {
+ synthLib::SMidiEvent e;
+ e.sysex = _data;
+ m_xt.sendMidiEvent(e);
+ }
+
+ void State::createSequencerMultiData(std::vector<uint8_t>& _data)
+ {
+ assert(false);
+ /*
+ static_assert(
+ (static_cast<uint32_t>(MultiParameter::Inst15) - static_cast<uint32_t>(MultiParameter::Inst0)) ==
+ (static_cast<uint32_t>(MultiParameter::Inst1) - static_cast<uint32_t>(MultiParameter::Inst0)) * 15,
+ "we need a consecutive offset");
+
+ _data.assign(static_cast<uint32_t>(xt::MultiParameter::Count), 0);
+
+ constexpr char name[] = "From TUS with <3";
+ static_assert(std::size(name) == 17, "wrong name length");
+ memcpy(&_data[static_cast<uint32_t>(MultiParameter::Name00)], name, sizeof(name) - 1);
+
+ auto setParam = [&](MultiParameter _param, const uint8_t _value)
+ {
+ _data[static_cast<uint32_t>(_param)] = _value;
+ };
+
+ auto setInstParam = [&](const uint8_t _instIndex, const MultiParameter _param, const uint8_t _value)
+ {
+ auto index = static_cast<uint32_t>(MultiParameter::Inst0) + (static_cast<uint32_t>(MultiParameter::Inst1) - static_cast<uint32_t>(MultiParameter::Inst0)) * _instIndex;
+ index += static_cast<uint32_t>(_param) - static_cast<uint32_t>(MultiParameter::Inst0);
+ _data[index] = _value;
+ };
+
+ setParam(MultiParameter::Volume, 127); // max volume
+
+ setParam(MultiParameter::ControlW, 121); // global
+ setParam(MultiParameter::ControlX, 121); // global
+ setParam(MultiParameter::ControlY, 121); // global
+ setParam(MultiParameter::ControlZ, 121); // global
+
+ for (uint8_t i = 0; i < 16; ++i)
+ {
+ setInstParam(i, MultiParameter::Inst0SoundBank, 0); // bank A
+ setInstParam(i, MultiParameter::Inst0SoundNumber, i); // sound number i
+ setInstParam(i, MultiParameter::Inst0MidiChannel, 2+i); // midi channel i
+ setInstParam(i, MultiParameter::Inst0Volume, 127); // max volume
+ setInstParam(i, MultiParameter::Inst0Transpose, 64); // no transpose
+ setInstParam(i, MultiParameter::Inst0Detune, 64); // no detune
+ setInstParam(i, MultiParameter::Inst0Output, 0); // main out
+ setInstParam(i, MultiParameter::Inst0Flags, 3); // RX = Local+MIDI / TX = off / Engine = Play
+ setInstParam(i, MultiParameter::Inst0Pan, 64); // center
+ setInstParam(i, MultiParameter::Inst0Pattern, 0); // no pattern
+ setInstParam(i, MultiParameter::Inst0VeloLow, 1); // full velocity range
+ setInstParam(i, MultiParameter::Inst0VeloHigh, 127);
+ setInstParam(i, MultiParameter::Inst0KeyLow, 0); // full key range
+ setInstParam(i, MultiParameter::Inst0KeyHigh, 127);
+ setInstParam(i, MultiParameter::Inst0MidiRxFlags, 63); // enable Pitchbend, Modwheel, Aftertouch, Sustain, Button 1/2, Program Change
+ }
+ */
+ }
+}
diff --git a/source/xtLib/xtState.h b/source/xtLib/xtState.h
@@ -0,0 +1,206 @@
+#pragma once
+
+#include <array>
+#include <vector>
+#include <cstddef>
+#include <cstdint>
+
+#include "xtMidiTypes.h"
+
+#include "../synthLib/deviceTypes.h"
+#include "../synthLib/midiTypes.h"
+
+#include "../wLib/wState.h"
+
+namespace synthLib
+{
+ struct SMidiEvent;
+}
+
+namespace xt
+{
+ class Xt;
+
+ using SysEx = wLib::SysEx;
+ using Responses = wLib::Responses;
+
+ class State : public wLib::State
+ {
+ public:
+ enum class Origin
+ {
+ Device,
+ External
+ };
+
+ enum class DumpType
+ {
+ Single,
+ Multi,
+ Global,
+ Mode,
+
+ Count
+ };
+
+ struct Dump
+ {
+ DumpType type;
+ SysexCommand cmdRequest;
+ SysexCommand cmdDump;
+ SysexCommand cmdParamChange;
+ uint32_t firstParamIndex;
+ uint32_t idxParamIndexH;
+ uint32_t idxParamIndexL;
+ uint32_t idxParamValue;
+ uint32_t dumpSize;
+ };
+
+ static constexpr Dump Dumps[] =
+ {
+ {DumpType::Single, SysexCommand::SingleRequest, SysexCommand::SingleDump, SysexCommand::SingleParameterChange, IdxSingleParamFirst, IdxSingleParamIndexH, IdxSingleParamIndexL, IdxSingleParamValue, 265},
+ {DumpType::Multi , SysexCommand::MultiRequest , SysexCommand::MultiDump , SysexCommand::MultiParameterChange , IdxMultiParamFirst , IdxMultiParamIndexH , IdxMultiParamIndexL , IdxMultiParamValue , 393},
+ {DumpType::Global, SysexCommand::GlobalRequest, SysexCommand::GlobalDump, SysexCommand::GlobalParameterChange, IdxGlobalParamFirst, IdxGlobalParamIndexH, IdxGlobalParamIndexL, IdxGlobalParamValue, 39},
+ {DumpType::Mode , SysexCommand::ModeRequest , SysexCommand::ModeDump , SysexCommand::ModeParameterChange , IdxModeParamFirst , IdxModeParamIndexH , IdxModeParamIndexL , IdxModeParamValue , 8},
+ };
+
+ using Single = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Single)].dumpSize>;
+ using Multi = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Multi)].dumpSize>;
+ using Global = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Global)].dumpSize>;
+ using Mode = std::array<uint8_t, Dumps[static_cast<uint32_t>(DumpType::Mode)].dumpSize>;
+
+ State(Xt& _xt);
+
+ bool loadState(const SysEx& _sysex);
+
+ bool receive(Responses& _responses, const synthLib::SMidiEvent& _data, Origin _sender);
+ bool receive(Responses& _responses, const SysEx& _data, Origin _sender);
+ void createInitState();
+
+ bool getState(std::vector<uint8_t>& _state, synthLib::StateType _type) const;
+ bool setState(const std::vector<uint8_t>& _state, synthLib::StateType _type);
+
+ static void createSequencerMultiData(std::vector<uint8_t>& _data);
+
+ private:
+
+ template<size_t Size> static bool append(SysEx& _dst, const std::array<uint8_t, Size>& _src, uint32_t _checksumStartIndex)
+ {
+ if(!isValid(_src))
+ return false;
+ auto src = _src;
+ wLib::State::updateChecksum(src, _checksumStartIndex);
+ _dst.insert(_dst.end(), src.begin(), src.end());
+ return true;
+ }
+
+ static bool updateChecksum(SysEx& _src, uint32_t _startIndex)
+ {
+ if(_src.size() < 3)
+ return false;
+ uint8_t& c = _src[_src.size() - 2];
+ c = 0;
+ for(size_t i= wLib::IdxCommand; i<_src.size()-2; ++i)
+ c += _src[i];
+ c &= 0x7f;
+ return true;
+ }
+
+ bool parseSingleDump(const SysEx& _data);
+ bool parseMultiDump(const SysEx& _data);
+ bool parseGlobalDump(const SysEx& _data);
+ bool parseModeDump(const SysEx& _data);
+
+ bool modifySingle(const SysEx& _data);
+ bool modifyMulti(const SysEx& _data);
+ bool modifyGlobal(const SysEx& _data);
+ bool modifyMode(const SysEx& _data);
+
+ uint8_t* getSingleParameter(const SysEx& _data);
+ uint8_t* getMultiParameter(const SysEx& _data);
+ uint8_t* getGlobalParameter(const SysEx& _data);
+ uint8_t* getModeParameter(const SysEx& _data);
+
+ bool getSingle(Responses& _responses, const SysEx& _data);
+ Single* getSingle(LocationH _buf, uint8_t _loc);
+
+ bool getMulti(Responses& _responses, const SysEx& _data);
+ Multi* getMulti(LocationH _buf, uint8_t _loc);
+
+ bool getGlobal(Responses& _responses);
+ Global* getGlobal();
+
+ bool getMode(Responses& _responses);
+ Mode* getMode();
+
+ bool getDump(DumpType _type, Responses& _responses, const SysEx& _data);
+ bool parseDump(DumpType _type, const SysEx& _data);
+ bool modifyDump(DumpType _type, const SysEx& _data);
+
+ uint8_t getGlobalParameter(GlobalParameter _parameter) const;
+ void setGlobalParameter(GlobalParameter _parameter, uint8_t _value);
+
+ uint8_t getModeParameter(ModeParameter _parameter) const;
+
+ bool isMultiMode() const
+ {
+ return getModeParameter(ModeParameter::Mode) != 0;
+ }
+
+ static bool isValid(const Single& _single)
+ {
+ return _single[IdxSingleParamFirst] == 1;
+ }
+
+ static bool isValid(const Multi& _multi)
+ {
+ return _multi.front() == 0xf0;
+ // we cannot do this anymore as drum map & multi have the same length
+// return _multi[IdxMultiParamFirst] > 0; // Note: no version number in a multi, assume that Multi volume has to be > 0 to be valid
+ }
+
+ static bool isValid(const Global& _global)
+ {
+ return _global.back() == 0xf7;
+ }
+
+ static bool isValid(const Mode& _mode)
+ {
+ return _mode.front() == 0xf0; // unable to derive from the packet itself
+ }
+
+ static SysexCommand getCommand(const SysEx& _data);
+
+ void forwardToDevice(const SysEx& _data) const;
+
+ void requestGlobal() const;
+ void requestSingle(LocationH _buf, uint8_t _location) const;
+ void requestMulti(LocationH _buf, uint8_t _location) const;
+ void sendMulti(const std::vector<uint8_t>& _multiData) const;
+ void sendGlobalParameter(GlobalParameter _param, uint8_t _value);
+ void sendSysex(const std::initializer_list<uint8_t>& _data) const;
+ void sendSysex(const SysEx& _data) const;
+
+ Xt& m_xt;
+
+ // ROM
+ std::array<Single, 256> m_romSingles{Single{}};
+ std::array<Multi, 128> m_romMultis{Multi{}};
+
+ // Edit Buffers
+ std::array<Single, 8> m_currentMultiSingles{Single{}};
+ std::array<Single, 1> m_currentInstrumentSingles{Single{}};
+ Multi m_currentMulti{};
+
+ // Global settings, only available once
+ Global m_global{};
+ Mode m_mode{};
+
+ // current state, valid while receiving data
+ Origin m_sender = Origin::External;
+ bool m_isEditBuffer = false;
+
+ synthLib::SMidiEvent m_lastBankSelectMSB;
+ synthLib::SMidiEvent m_lastBankSelectLSB;
+ };
+}
diff --git a/source/xtLib/xtSysexRemoteControl.cpp b/source/xtLib/xtSysexRemoteControl.cpp
@@ -0,0 +1,146 @@
+#include "xtSysexRemoteControl.h"
+
+#include "xtMidiTypes.h"
+#include "xt.h"
+#include "xtButtons.h"
+#include "xtHardware.h"
+#include "xtLeds.h"
+
+#include "../synthLib/midiTypes.h"
+
+namespace xt
+{
+ enum class ButtonType;
+
+ void SysexRemoteControl::createSysexHeader(std::vector<uint8_t>& _dst, xt::SysexCommand _cmd)
+ {
+ constexpr uint8_t devId = 0;
+ _dst.assign({0xf0, wLib::IdWaldorf, IdMw2, devId, static_cast<uint8_t>(_cmd)});
+ }
+
+ void SysexRemoteControl::sendSysexLCD(std::vector<synthLib::SMidiEvent>& _dst) const
+ {
+ std::array<char, 80> lcdData{};
+ m_mq.readLCD(lcdData);
+
+ synthLib::SMidiEvent ev;
+ createSysexHeader(ev.sysex, SysexCommand::EmuLCD);
+ ev.sysex.insert(ev.sysex.end(), lcdData.begin(), lcdData.end());
+
+ _dst.emplace_back(ev);
+ }
+
+ void SysexRemoteControl::sendSysexButtons(std::vector<synthLib::SMidiEvent>& _dst) const
+ {
+ static_assert(static_cast<uint32_t>(ButtonType::Count) < 24, "too many buttons");
+ uint32_t buttons = 0;
+ for(uint32_t i=0; i<static_cast<uint32_t>(ButtonType::Count); ++i)
+ {
+ if(m_mq.getButton(static_cast<ButtonType>(i)))
+ buttons |= (1<<i);
+ }
+
+ auto& ev = _dst.emplace_back();
+
+ createSysexHeader(ev.sysex, SysexCommand::EmuButtons);
+
+ ev.sysex.push_back((buttons>>16) & 0xff);
+ ev.sysex.push_back((buttons>>8) & 0xff);
+ ev.sysex.push_back(buttons & 0xff);
+ }
+
+ void SysexRemoteControl::sendSysexLEDs(std::vector<synthLib::SMidiEvent>& _dst) const
+ {
+ static_assert(static_cast<uint32_t>(LedType::Count) < 32, "too many LEDs");
+ uint32_t leds = 0;
+ for(uint32_t i=0; i<static_cast<uint32_t>(LedType::Count); ++i)
+ {
+ if(m_mq.getLedState(static_cast<LedType>(i)))
+ leds |= (1<<i);
+ }
+ auto& ev = _dst.emplace_back();
+ auto& response = ev.sysex;
+ createSysexHeader(response, SysexCommand::EmuLEDs);
+ response.push_back((leds>>24) & 0xff);
+ response.push_back((leds>>16) & 0xff);
+ response.push_back((leds>>8) & 0xff);
+ response.push_back(leds & 0xff);
+ }
+
+ void SysexRemoteControl::sendSysexRotaries(std::vector<synthLib::SMidiEvent>& _dst) const
+ {
+/* auto& ev= _dst.emplace_back();
+ auto& response = ev.sysex;
+
+ createSysexHeader(response, SysexCommand::EmuRotaries);
+
+ for(uint32_t i=0; i<static_cast<uint32_t>(Buttons::Encoders::Count); ++i)
+ {
+ const auto value = m_mq.getEncoder(static_cast<Buttons::Encoders>(i));
+ response.push_back(value);
+ }
+*/ }
+
+ bool SysexRemoteControl::receive(std::vector<synthLib::SMidiEvent>& _output, const std::vector<unsigned char>& _input) const
+ {
+ if(_input.size() < 5)
+ return false;
+
+ if(_input[1] != wLib::IdWaldorf || _input[2] != IdMw2)
+ return false;
+
+ const auto cmd = _input[4];
+
+ switch (static_cast<SysexCommand>(cmd))
+ {
+ case SysexCommand::EmuLCD:
+ sendSysexLCD(_output);
+ return true;
+ case SysexCommand::EmuButtons:
+ {
+ if(_input.size() > 6)
+ {
+ const auto button = static_cast<ButtonType>(_input[5]);
+ const auto state = _input[6];
+ m_mq.getHardware()->getUC().setButton(button, state != 0);
+ }
+ else
+ {
+ sendSysexButtons(_output);
+ }
+ }
+ return true;
+ case SysexCommand::EmuLEDs:
+ {
+ sendSysexLEDs(_output);
+ }
+ return true;
+ case SysexCommand::EmuRotaries:
+ {
+ return false;
+/* if(_input.size() > 6)
+ {
+ const auto encoder = static_cast<Encoders>(_input[5]);
+ const auto amount = static_cast<int>(_input[6]) - 64;
+ if(amount)
+ m_mq.rotateEncoder(encoder, amount);
+ }
+ else
+ {
+ sendSysexRotaries(_output);
+ }
+*/ }
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ void SysexRemoteControl::handleDirtyFlags(std::vector<synthLib::SMidiEvent>& _output, const uint32_t _dirtyFlags) const
+ {
+ if(_dirtyFlags & static_cast<uint32_t>(Xt::DirtyFlags::Lcd))
+ sendSysexLCD(_output);
+ if(_dirtyFlags & static_cast<uint32_t>(Xt::DirtyFlags::Leds))
+ sendSysexLEDs(_output);
+ }
+}
diff --git a/source/xtLib/xtSysexRemoteControl.h b/source/xtLib/xtSysexRemoteControl.h
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <vector>
+
+#include "xtMidiTypes.h"
+
+namespace synthLib
+{
+ struct SMidiEvent;
+}
+
+namespace xt
+{
+ class Xt;
+
+ class SysexRemoteControl
+ {
+ public:
+ SysexRemoteControl(Xt& _mq) : m_mq(_mq) {}
+
+ static void createSysexHeader(std::vector<uint8_t>& _dst, SysexCommand _cmd);
+
+ void sendSysexLCD(std::vector<synthLib::SMidiEvent>& _dst) const;
+ void sendSysexButtons(std::vector<synthLib::SMidiEvent>& _dst) const;
+ void sendSysexLEDs(std::vector<synthLib::SMidiEvent>& _dst) const;
+ void sendSysexRotaries(std::vector<synthLib::SMidiEvent>& _dst) const;
+
+ bool receive(std::vector<synthLib::SMidiEvent>& _output, const std::vector<uint8_t>& _input) const;
+ void handleDirtyFlags(std::vector<synthLib::SMidiEvent>& _output, uint32_t _dirtyFlags) const;
+
+ private:
+ Xt& m_mq;
+ };
+}
diff --git a/source/xtLib/xtTypes.h b/source/xtLib/xtTypes.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <cstdint>
+#include <array>
+#include <vector>
+
+#include "dsp56kEmu/types.h"
+
+namespace xt
+{
+ static constexpr uint32_t g_ramSize = 0x00020000;
+ static constexpr uint32_t g_romSize = 0x00040000;
+ static constexpr uint32_t g_romAddr = 0x00100000;
+ static constexpr uint32_t g_addrMask = 0x001fffff;
+
+ template<size_t Count> using TAudioBuffer = std::array<std::vector<dsp56k::TWord>, Count>;
+ using TAudioOutputs = TAudioBuffer<4>;
+ using TAudioInputs = TAudioBuffer<2>;
+}
diff --git a/source/xtLib/xtUc.cpp b/source/xtLib/xtUc.cpp
@@ -0,0 +1,235 @@
+#include "xtUc.h"
+
+#include <cassert>
+#include <cstring>
+
+#include "xtRom.h"
+
+#include "../mc68k/logging.h"
+#include "dsp56kEmu/utils.h"
+
+namespace xt
+{
+ XtUc::XtUc(const Rom& _rom)
+ : m_flash(m_romRuntimeData.data(), m_romRuntimeData.size(), false, true)
+ , m_pic(*this, m_lcd)
+ {
+ assert(_rom.isValid() && _rom.getSize() == m_romRuntimeData.size());
+
+ memcpy(m_romRuntimeData.data(), _rom.getData(), g_romSize);
+ m_memory.fill(0);
+
+// dumpAssembly("xt_68k.asm", g_romAddr, g_romSize);
+
+ reset();
+ setPC(0x100100);
+
+ getPortGP().setWriteTXCallback([this](const mc68k::Port&)
+ {
+// onPortGPWritten();
+ });
+
+ getPortE().setWriteTXCallback([this](const mc68k::Port&)
+ {
+// onPortEWritten();
+ });
+
+ getPortF().setWriteTXCallback([this](const mc68k::Port&)
+ {
+// onPortFWritten();
+ });
+
+ getPortQS().setDirectionChangeCallback([this](const mc68k::Port&)
+ {
+ onPortQSWritten();
+ });
+
+ getPortQS().setWriteTXCallback([this](const mc68k::Port&)
+ {
+ onPortQSWritten();
+ });
+ }
+
+ uint32_t XtUc::exec()
+ {
+// LOG("PC: " << HEX(getPC()));
+ const auto cycles = Mc68k::exec();
+ m_hdiA.exec(cycles);
+ return cycles;
+ }
+
+ uint16_t XtUc::readImm16(const uint32_t _addr)
+ {
+ const auto addr = _addr & g_addrMask;
+
+ if(addr < g_ramSize)
+ {
+ return readW(m_memory.data(), addr);
+ }
+
+ if(addr >= g_romAddr && addr < g_romAddr + Rom::Size)
+ {
+ const auto r = readW(m_romRuntimeData.data(), addr - g_romAddr);
+// LOG("read16 from ROM addr=" << HEXN(_addr, 8) << " val=" << HEXN(r, 4));
+ return r;
+ }
+ dsp56k::nativeDebugBreak();
+ return 0;
+ }
+
+ uint16_t XtUc::read16(const uint32_t _addr)
+ {
+ const auto addr = _addr & g_addrMask;
+
+ if(addr < g_ramSize)
+ {
+ return readW(m_memory.data(), addr);
+ }
+
+ if(addr >= g_romAddr && addr < g_romAddr + Rom::Size)
+ {
+ const auto r = readW(m_romRuntimeData.data(), addr - g_romAddr);
+// LOG("read16 from ROM addr=" << HEXN(_addr, 8) << " val=" << HEXN(r, 4));
+ return r;
+ }
+
+ const auto pa = static_cast<mc68k::PeriphAddress>(addr & mc68k::g_peripheralMask);
+
+ if (m_hdiA.isInRange(pa))
+ return m_hdiA.read16(pa);
+
+// LOG("read16 addr=" << HEXN(_addr, 8) << ", pc=" << HEXN(getPC(), 8));
+
+ return Mc68k::read16(addr);
+ }
+
+ uint8_t XtUc::read8(const uint32_t _addr)
+ {
+ const auto addr = _addr & g_addrMask;
+
+ if(addr < g_ramSize)
+ return m_memory[addr];
+
+ if(addr >= g_romAddr && addr < g_romAddr + Rom::Size)
+ return m_romRuntimeData[addr - g_romAddr];
+
+ const auto pa = static_cast<mc68k::PeriphAddress>(addr & mc68k::g_peripheralMask);
+
+ if(m_hdiA.isInRange(pa))
+ return m_hdiA.read8(pa);
+
+// LOG("read8 addr=" << HEXN(addr, 8) << ", pc=" << HEXN(getPC(), 8));
+
+ return Mc68k::read8(addr);
+ }
+
+ void XtUc::write16(const uint32_t _addr, uint16_t val)
+ {
+ const auto addr = _addr & g_addrMask;
+
+ if(addr < g_ramSize)
+ {
+ writeW(m_memory.data(), addr, val);
+ return;
+ }
+
+ if(addr >= g_romAddr && addr < g_romAddr + Rom::Size)
+ {
+ MCLOG("write16 TO ROM addr=" << MCHEXN(addr, 8) << ", value=" << MCHEXN(val,4) << ", pc=" << MCHEXN(getPC(), 8));
+ m_flash.write(addr - g_romAddr, val);
+ return;
+ }
+
+ const auto pa = static_cast<mc68k::PeriphAddress>(addr & mc68k::g_peripheralMask);
+
+ if (m_hdiA.isInRange(pa))
+ {
+ m_hdiA.write16(pa, val);
+ return;
+ }
+
+ Mc68k::write16(addr, val);
+ }
+
+ void XtUc::write8(const uint32_t _addr, uint8_t val)
+ {
+ const auto addr = _addr & g_addrMask;
+
+ if(addr < g_ramSize)
+ {
+ m_memory[addr] = val;
+ return;
+ }
+
+ if(addr >= g_romAddr && addr < g_romAddr + Rom::Size)
+ {
+ MCLOG("write8 TO ROM addr=" << MCHEXN(addr, 8) << ", value=" << MCHEXN(val,2) << ", pc=" << MCHEXN(getPC(), 8));
+ m_flash.write(addr - g_romAddr, val);
+ return;
+ }
+
+// LOG("write8 addr=" << HEXN(addr, 8) << ", value=" << HEXN(val,2) << ", pc=" << HEXN(getPC(), 8));
+
+ const auto pa = static_cast<mc68k::PeriphAddress>(addr & mc68k::g_peripheralMask);
+ if (m_hdiA.isInRange(pa))
+ {
+ m_hdiA.write8(pa, val);
+ return;
+ }
+
+ Mc68k::write8(addr, val);
+ }
+
+ void XtUc::onPortQSWritten()
+ {
+ const bool resetIsOutput = getPortQS().getDirection() & (1<<3);
+
+ if(resetIsOutput)
+ {
+ if(!(getPortQS().read() & (1<<3)))
+ {
+ if(!m_dspResetRequest)
+ {
+#ifdef _DEBUG
+ MCLOG("Request DSP RESET");
+#endif
+ m_dspResetRequest = true;
+ m_dspResetCompleted = false;
+ }
+ }
+ }
+ else
+ {
+ if(m_dspResetCompleted)
+ {
+ m_dspResetRequest = false;
+ getPortQS().writeRX(1<<3);
+ }
+ }
+ }
+
+ void XtUc::setButton(ButtonType _type, const bool _pressed)
+ {
+ m_pic.setButton(_type, _pressed);
+ }
+
+ void XtUc::setLcdDirtyCallback(const Pic::DirtyCallback& _callback)
+ {
+ m_pic.setLcdDirtyCallback(_callback);
+ }
+
+ void XtUc::setLedsDirtyCallback(const Pic::DirtyCallback& _callback)
+ {
+ m_pic.setLedsDirtyCallback(_callback);
+ }
+
+ bool XtUc::getLedState(LedType _led) const
+ {
+ return m_pic.getLedState(_led);
+ }
+
+ bool XtUc::getButton(ButtonType _button) const
+ {
+ return m_pic.getButton(_button);
+ }
+}
diff --git a/source/xtLib/xtUc.h b/source/xtLib/xtUc.h
@@ -0,0 +1,61 @@
+#pragma once
+
+#include "xtButtons.h"
+#include "xtLcd.h"
+#include "xtLeds.h"
+#include "xtPic.h"
+#include "xtTypes.h"
+
+#include "../mc68k/mc68k.h"
+#include "../mc68k/hdi08periph.h"
+
+#include "../wLib/am29f.h"
+
+namespace xt
+{
+ class Rom;
+
+ using xtHdi08A = mc68k::Hdi08Periph<0xfe000>;
+
+ class XtUc : public mc68k::Mc68k
+ {
+ public:
+ XtUc(const Rom& _rom);
+ uint32_t exec() override;
+
+ uint16_t readImm16(uint32_t _addr) override;
+ uint16_t read16(uint32_t _addr) override;
+ uint8_t read8(uint32_t _addr) override;
+
+ void write16(uint32_t _addr, uint16_t _val) override;
+ void write8(uint32_t _addr, uint8_t _val) override;
+
+ xtHdi08A& getHdi08A() { return m_hdiA; }
+
+ bool requestDSPReset() const { return m_dspResetRequest; }
+ void notifyDSPBooted() { m_dspResetCompleted = true; }
+
+ void onPortQSWritten();
+
+ void setButton(ButtonType _type, bool _pressed);
+
+ void setLcdDirtyCallback(const Pic::DirtyCallback& _callback);
+ void setLedsDirtyCallback(const Pic::DirtyCallback& _callback);
+
+ Lcd& getLcd() { return m_lcd; }
+ bool getLedState(LedType _led) const;
+ bool getButton(ButtonType _button) const;
+
+ private:
+ std::array<uint8_t, g_ramSize> m_memory;
+ std::array<uint8_t, g_romSize> m_romRuntimeData;
+
+ xtHdi08A m_hdiA;
+ wLib::Am29f m_flash;
+ Pic m_pic;
+ Lcd m_lcd;
+
+ bool m_dspResetRequest = false;
+ bool m_dspResetCompleted = false;
+ };
+}
diff --git a/source/xtTestConsole/CMakeLists.txt b/source/xtTestConsole/CMakeLists.txt
@@ -0,0 +1,15 @@
+cmake_minimum_required(VERSION 3.10)
+
+project(xtTestConsole)
+
+add_executable(xtTestConsole)
+
+set(SOURCES
+ xtTestConsole.cpp
+)
+
+target_sources(xtTestConsole PRIVATE ${SOURCES})
+source_group("source" FILES ${SOURCES})
+
+target_link_libraries(xtTestConsole PUBLIC xtLib)
+set_property(TARGET xtTestConsole PROPERTY FOLDER "Xenia")
diff --git a/source/xtTestConsole/xtTestConsole.cpp b/source/xtTestConsole/xtTestConsole.cpp
@@ -0,0 +1,172 @@
+#include "../synthLib/wavWriter.h"
+
+#include "../xtLib/xtUc.h"
+#include "../xtLib/xtHardware.h"
+#include "../xtLib/xt.h"
+
+#include "dsp56kEmu/jitunittests.h"
+
+//#include "../synthLib/os.h"
+
+int main()
+{
+// dsp56k::JitUnittests tests;
+
+ const std::unique_ptr uc(std::make_unique<xt::Xt>());
+
+ constexpr uint32_t blockSize = 64;
+ std::vector<dsp56k::TWord> stereoOutput;
+ stereoOutput.resize(blockSize<<1);
+
+ synthLib::AsyncWriter writer("xtEmu_Output.wav", 40000, false);
+
+ constexpr uint32_t period = 0x10000;
+
+ while(!uc->isBootCompleted())
+ uc->process(blockSize);
+
+ auto processLength = [&](const uint32_t _length)
+ {
+ size_t total = 0;
+ while(total < _length)
+ {
+ uc->process(blockSize);
+
+ auto& outs = uc->getAudioOutputs();
+
+ for(size_t i=0; i<blockSize; ++i)
+ {
+ stereoOutput[(i<<1) ] = outs[0][i];
+ stereoOutput[(i<<1)+1] = outs[1][i];
+ }
+
+ writer.append([&](std::vector<dsp56k::TWord>& _wavOut)
+ {
+ _wavOut.insert(_wavOut.end(), stereoOutput.begin(), stereoOutput.end());
+ });
+
+ total += blockSize;
+ }
+ };
+
+ for(size_t i=0; i<5000; ++i)
+ uc->process(blockSize);
+
+ auto sleep = [](const uint32_t _seconds = 1)
+ {
+ std::this_thread::sleep_for(std::chrono::seconds(_seconds));
+ };
+
+ auto sendButton = [&](const xt::ButtonType _button, const bool _pressed)
+ {
+ auto& u = uc->getHardware()->getUC();
+ u.setButton(_button, _pressed);
+ };
+
+ auto sendEncoder = [&](const uint8_t _encoder, const int8_t _delta)
+ {
+ synthLib::SMidiEvent e;
+ constexpr uint8_t idm = 0x26; // RMTP
+ const uint8_t uu = _encoder; // encoder
+ const uint8_t mm = 64 + _delta; // movement
+ const uint8_t checksum = (uu + mm) & 0x7f;
+ e.sysex = {0xf0, 0x3e, 0x0e, 0x7f, idm, uu, mm, checksum, 0xf7};
+ uc->sendMidiEvent(e);
+ };
+
+ sendButton(xt::ButtonType::Play, true); // play/shift
+ processLength(64 * 5);
+ sendButton(xt::ButtonType::Power, true); // power
+ processLength(64 * 5);
+
+ processLength(64 * 500);
+
+ sendButton(xt::ButtonType::Power, false); // power
+ processLength(64);
+ sendButton(xt::ButtonType::Play, false); // play/shift
+ processLength(64);
+
+ processLength(64 * 500);
+
+ sendEncoder(0x2, 1); // encoder #3
+
+ processLength(64 * 500);
+
+ sendEncoder(0x4, 2); // red encoder
+
+ const auto& mem = uc->getHardware()->getDSP(0).dsp().memory();
+
+// int count = 0;
+
+ while(true)
+ {
+ /*
+ ++count;
+ if(count == 10000)
+ {
+ // dump wavetables from DSP RAM
+ // Wavetables are stored with 64 waves per part
+ // Size of one wave is stored as "multisamples" with the
+ // first size being 128, the last two ones being 1
+ // This is used to ensure that WTs are band limited
+ std::vector<uint32_t> data;
+
+ for(uint32_t p=0; p<8; ++p)
+ {
+ uint32_t offP = p * 0x4000;
+
+ for(uint32_t wt=0; wt<64; ++wt)
+ {
+ uint32_t offWT = wt * 256;
+
+ for(uint32_t i=0; i<128; ++i)
+ {
+ auto v = mem.get(dsp56k::MemArea_Y, 0x20000 + offP + offWT + i);
+ data.push_back(v << 8);
+ }
+ }
+ }
+
+ synthLib::writeFile("wavetables.bin", reinterpret_cast<const uint8_t*>(data.data()), data.size() * 4);
+ }
+ */
+ processLength(64);
+ }
+
+ size_t repeats = 1;
+
+ sleep();
+
+ constexpr uint8_t notes[] = {60 - 12, 60, 60 + 12};
+
+ for(uint8_t p=0; p<=128; ++p)
+ {
+ sleep();
+ LOG("PROGRAM CHANGE " << static_cast<int>(p));
+ sleep();
+ uc->sendMidiEvent({synthLib::M_PROGRAMCHANGE, p, 0});
+
+ processLength(128);
+
+ for(size_t r=0; r<repeats; ++r)
+ {
+ for (uint8_t note : notes)
+ {
+ sleep();
+ LOG("NOTE ON " << static_cast<int>(note));
+ sleep();
+ uc->sendMidiEvent({synthLib::M_NOTEON, note, 127});
+ processLength(period);
+ sleep();
+ LOG("NOTE OFF " << static_cast<int>(note));
+ sleep();
+ uc->sendMidiEvent({synthLib::M_NOTEOFF, note, 127});
+ processLength(period);
+ }
+ }
+ }
+
+ LOG("END");
+
+ return 0;
+}
diff --git a/temp/cmake_win64/gearmulator.sln.DotSettings b/temp/cmake_win64/gearmulator.sln.DotSettings
@@ -5,7 +5,11 @@
<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/=ABC/@EntryIndexedValue">ABC</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 +23,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"><NamingElement Priority="1"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="__interface" /><type Name="class" /><type Name="struct" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></NamingElement></s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Class_0020and_0020struct_0020fields/@EntryIndexedValue"><NamingElement Priority="11"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="class field" /><type Name="struct field" /></Descriptor><Policy Inspect="True" Prefix="m_" Suffix="" Style="aaBb" /></NamingElement></s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CppNaming/Rules/=Class_0020and_0020struct_0020methods/@EntryIndexedValue"><NamingElement Priority="10"><Descriptor Static="Indeterminate" Constexpr="Indeterminate" Const="Indeterminate" Volatile="Indeterminate" Accessibility="NOT_APPLICABLE"><type Name="member function" /></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></NamingElement></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()