gearmulator

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

commit d536aff73d2d80ccabab11b7257837ec04d51da9
parent 80ac1b69246c991a3e9f73f771cc7fb30f4de7e0
Author: dsp56300 <dsp56300@users.noreply.github.com>
Date:   Wed, 17 Apr 2024 00:40:57 +0200

update Osirus open source version

Diffstat:
M.gitignore | 2++
M.gitmodules | 4++--
MCMakeLists.txt | 2+-
Mbase.cmake | 2+-
Mbuild_linux.sh | 2+-
Mdoc/changelog.txt | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msource/CMakeLists.txt | 3---
Msource/juce.cmake | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msource/jucePlugin/CMakeLists.txt | 2++
Msource/jucePlugin/ParameterNames.h | 1+
Msource/jucePlugin/PluginEditorState.cpp | 37+++++++++++++++++++++++++++++++++++++
Msource/jucePlugin/PluginProcessor.cpp | 279+++++++++++++++++++++++++------------------------------------------------------
Msource/jucePlugin/PluginProcessor.h | 52+++++++++++++++++++++++++++++++++++-----------------
Msource/jucePlugin/VirusController.cpp | 70+++++++++++++++++++++++++++++++++++++---------------------------------
Msource/jucePlugin/VirusController.h | 2+-
Msource/jucePlugin/parameterDescriptions_C.json | 16++++++++--------
Msource/jucePlugin/skins/Hoverland/ARP_2034x1238_x2.png | 0
Msource/jucePlugin/skins/Hoverland/VirusC_Hoverland.json | 104++++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msource/jucePlugin/ui3/FxPage.cpp | 4++--
Msource/jucePlugin/ui3/FxPage.h | 4++--
Asource/jucePlugin/ui3/Leds.cpp | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePlugin/ui3/Leds.h | 27+++++++++++++++++++++++++++
Msource/jucePlugin/ui3/PartButton.cpp | 65+++++++++++++++++++++++++++++++++++++++++++++++------------------
Msource/jucePlugin/ui3/PartButton.h | 3---
Msource/jucePlugin/ui3/Parts.cpp | 44++++++++++++++++++++++++++++++++++++++++++--
Msource/jucePlugin/ui3/Parts.h | 7+++++--
Msource/jucePlugin/ui3/PatchManager.cpp | 7-------
Msource/jucePlugin/ui3/PatchManager.h | 2--
Msource/jucePlugin/ui3/VirusEditor.cpp | 74+++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msource/jucePlugin/ui3/VirusEditor.h | 13+++++++++++--
Msource/jucePluginEditorLib/CMakeLists.txt | 3+++
Msource/jucePluginEditorLib/focusedParameter.cpp | 9++++++++-
Msource/jucePluginEditorLib/focusedParameterTooltip.cpp | 11+++++++++--
Msource/jucePluginEditorLib/focusedParameterTooltip.h | 1+
Asource/jucePluginEditorLib/lcd.cpp | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/lcd.h | 45+++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/led.cpp | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginEditorLib/led.h | 35+++++++++++++++++++++++++++++++++++
Msource/jucePluginEditorLib/midiPorts.cpp | 9+++++++++
Msource/jucePluginEditorLib/midiPorts.h | 1+
Msource/jucePluginEditorLib/patchmanager/datasourcetreeitem.cpp | 23+++++++++++++++++++----
Msource/jucePluginEditorLib/patchmanager/datasourcetreeitem.h | 2+-
Msource/jucePluginEditorLib/patchmanager/editable.cpp | 12+++++++++++-
Msource/jucePluginEditorLib/patchmanager/editable.h | 3++-
Msource/jucePluginEditorLib/patchmanager/grouptreeitem.cpp | 100+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msource/jucePluginEditorLib/patchmanager/info.cpp | 28++++++++++++++++++++++++++--
Msource/jucePluginEditorLib/patchmanager/info.h | 10++++++++--
Msource/jucePluginEditorLib/patchmanager/list.cpp | 9+++++++++
Msource/jucePluginEditorLib/patchmanager/list.h | 1+
Msource/jucePluginEditorLib/patchmanager/listitem.cpp | 9++++++++-
Msource/jucePluginEditorLib/patchmanager/patchmanager.cpp | 83++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msource/jucePluginEditorLib/patchmanager/patchmanager.h | 11+++++++++--
Msource/jucePluginEditorLib/patchmanager/state.cpp | 2+-
Msource/jucePluginEditorLib/patchmanager/tagstree.cpp | 3++-
Msource/jucePluginEditorLib/patchmanager/tagtreeitem.cpp | 75++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msource/jucePluginEditorLib/patchmanager/tagtreeitem.h | 2+-
Msource/jucePluginEditorLib/patchmanager/treeitem.cpp | 52+++++++++++++++++++++++++++++++++++++++++++++++++---
Msource/jucePluginEditorLib/patchmanager/treeitem.h | 11++++++++++-
Msource/jucePluginEditorLib/pluginEditor.cpp | 22++++++++++++++++++++++
Msource/jucePluginEditorLib/pluginEditor.h | 2++
Msource/jucePluginEditorLib/pluginEditorState.cpp | 50++++++++++++++++++++++++++++++++++++--------------
Msource/jucePluginEditorLib/pluginEditorState.h | 1+
Msource/jucePluginEditorLib/pluginEditorWindow.cpp | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Msource/jucePluginEditorLib/pluginEditorWindow.h | 8+++++---
Msource/jucePluginEditorLib/pluginProcessor.cpp | 72+++++++++++++++++++++++++++++++++++-------------------------------------
Msource/jucePluginEditorLib/pluginProcessor.h | 9++++++---
Msource/jucePluginLib/CMakeLists.txt | 2++
Msource/jucePluginLib/controller.cpp | 65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msource/jucePluginLib/controller.h | 15++++++++++++---
Msource/jucePluginLib/event.h | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Msource/jucePluginLib/parameterbinding.cpp | 5+++++
Msource/jucePluginLib/parameterbinding.h | 1+
Msource/jucePluginLib/parameterdescription.cpp | 7+++++++
Msource/jucePluginLib/parameterdescription.h | 4++++
Msource/jucePluginLib/parameterdescriptions.cpp | 66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Msource/jucePluginLib/parameterdescriptions.h | 4++--
Msource/jucePluginLib/parameterlink.h | 1-
Msource/jucePluginLib/parameterregion.h | 5+++++
Msource/jucePluginLib/patchdb/datasource.cpp | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePluginLib/patchdb/datasource.h | 12++++++++++++
Msource/jucePluginLib/patchdb/db.cpp | 394+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msource/jucePluginLib/patchdb/db.h | 15+++++++++++----
Msource/jucePluginLib/patchdb/patch.cpp | 56++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/jucePluginLib/patchdb/patch.h | 14++++++++++++++
Msource/jucePluginLib/patchdb/patchdbtypes.h | 15+++++++++++++++
Msource/jucePluginLib/patchdb/patchmodifications.cpp | 25++++++++++++++++++++++++-
Msource/jucePluginLib/patchdb/patchmodifications.h | 3+++
Msource/jucePluginLib/patchdb/tags.cpp | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msource/jucePluginLib/patchdb/tags.h | 12++++++++++++
Msource/jucePluginLib/processor.cpp | 339++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msource/jucePluginLib/processor.h | 41++++++++++++++++++++++++++++++++++++++---
Asource/jucePluginLib/softknob.cpp | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/jucePluginLib/softknob.h | 43+++++++++++++++++++++++++++++++++++++++++++
Msource/juceUiLib/CMakeLists.txt | 2++
Msource/juceUiLib/condition.cpp | 40++++++++++++++++++++++++++++++----------
Msource/juceUiLib/condition.h | 47+++++++++++++++++++++++++++++++++++++++++------
Msource/juceUiLib/controllerlink.h | 2+-
Msource/juceUiLib/editor.cpp | 7++++++-
Msource/juceUiLib/editor.h | 3++-
Msource/juceUiLib/image.h | 2+-
Msource/juceUiLib/rotaryStyle.h | 2--
Msource/juceUiLib/uiObject.cpp | 85+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Msource/juceUiLib/uiObject.h | 8+++++---
Msource/juceUiLib/uiObjectStyle.cpp | 41++++++++++++++++++++++++-----------------
Msource/juceUiLib/uiObjectStyle.h | 4+++-
Msource/libresample/CMakeLists.txt | 4++--
Msource/synthLib/CMakeLists.txt | 5++++-
Msource/synthLib/audiobuffer.h | 1+
Asource/synthLib/binarystream.cpp | 24++++++++++++++++++++++++
Msource/synthLib/binarystream.h | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msource/synthLib/device.cpp | 51++++++++++++++++++++++++++++++++++++++++++++++++---
Msource/synthLib/device.h | 16++++++++++++++++
Asource/synthLib/dspMemoryPatch.cpp | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/synthLib/dspMemoryPatch.h | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/synthLib/md5.cpp | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/synthLib/md5.h | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msource/synthLib/os.cpp | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msource/synthLib/os.h | 10+++++++++-
Msource/synthLib/plugin.cpp | 64+++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msource/synthLib/plugin.h | 13+++++++++++--
Msource/synthLib/resamplerInOut.cpp | 11+++++++++++
Msource/synthLib/resamplerInOut.h | 1+
Msource/virusConsoleLib/CMakeLists.txt | 1+
Msource/virusConsoleLib/consoleApp.cpp | 172+++++++++++++++++--------------------------------------------------------------
Msource/virusConsoleLib/consoleApp.h | 13++++---------
Msource/virusIntegrationTest/CMakeLists.txt | 2++
Msource/virusLib/CMakeLists.txt | 6++++++
Msource/virusLib/device.cpp | 131++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Msource/virusLib/device.h | 27+++++++++++++++++++++++----
Asource/virusLib/deviceModel.cpp | 20++++++++++++++++++++
Asource/virusLib/deviceModel.h | 30++++++++++++++++++++++++++++++
Asource/virusLib/dspMemoryPatches.cpp | 19+++++++++++++++++++
Asource/virusLib/dspMemoryPatches.h | 14++++++++++++++
Msource/virusLib/dspSingle.cpp | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msource/virusLib/dspSingle.h | 38++++++++++++++++++++++++++++++++------
Asource/virusLib/frontpanelState.cpp | 44++++++++++++++++++++++++++++++++++++++++++++
Asource/virusLib/frontpanelState.h | 39+++++++++++++++++++++++++++++++++++++++
Msource/virusLib/hdi08MidiQueue.cpp | 2+-
Msource/virusLib/hdi08MidiQueue.h | 4++--
Msource/virusLib/hdi08TxParser.cpp | 2+-
Msource/virusLib/microcontroller.cpp | 90+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msource/virusLib/microcontroller.h | 8+++++++-
Msource/virusLib/midiFileToRomData.cpp | 4++--
Msource/virusLib/midiFileToRomData.h | 2+-
Msource/virusLib/romfile.cpp | 123+++++++++++++------------------------------------------------------------------
Msource/virusLib/romfile.h | 36++++++++++++++++++------------------
Asource/virusLib/romloader.cpp | 218+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/virusLib/romloader.h | 41+++++++++++++++++++++++++++++++++++++++++
Msource/virusTestConsole/CMakeLists.txt | 1+
Msource/virusTestConsole/virusTestConsole.cpp | 5++++-
Mtemp/cmake_win64/gearmulator.sln.DotSettings | 1+
151 files changed, 4748 insertions(+), 1176 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -2,10 +2,12 @@ /vtune/ *.aps /.vs +/.vscode /out/ /temp2/cmake_win64 /source/jucePlugin_24.01.2022.zip /build_win64_VS2019.bat /out/ +/build/ *atom*.sh *debug.sh diff --git a/.gitmodules b/.gitmodules @@ -1,9 +1,9 @@ [submodule "source/dsp56300"] path = source/dsp56300 - url = ../dsp56300 + url = https://github.com/dsp56300/dsp56300 [submodule "source/JUCE"] path = source/JUCE url = https://github.com/dsp56300/JUCE [submodule "source/clap-juce-extensions"] path = source/clap-juce-extensions - url = ../../free-audio/clap-juce-extensions.git + url = https://github.com/free-audio/clap-juce-extensions diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -19,7 +19,7 @@ if(APPLE) message("CMAKE_OSX_DEPLOYMENT_TARGET: " ${CMAKE_OSX_DEPLOYMENT_TARGET}) endif() -project(gearmulator VERSION 1.3.5) +project(gearmulator VERSION 1.3.10) include(base.cmake) include(CTest) diff --git a/base.cmake b/base.cmake @@ -34,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") 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/doc/changelog.txt b/doc/changelog.txt @@ -1,6 +1,125 @@ Release Notes -1.3.5 (TBD) +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 + +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 + +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: diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt @@ -1,8 +1,5 @@ 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" on) # ----------------- DSP56300 emulator 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,6 +18,8 @@ set(SOURCES ui3/ControllerLinks.h ui3/FxPage.cpp ui3/FxPage.h + ui3/Leds.cpp + ui3/Leds.h ui3/Parts.cpp ui3/Parts.h ui3/PatchManager.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 @@ -71,5 +71,42 @@ bool PluginEditorState::initAdvancedContextMenu(juce::PopupMenu& _menu, bool _en _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/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); + if(_index >= m_roms.size()) + return false; + if(_index == m_selectedRom) + return true; + m_selectedRom = _index; - for (int channel = 0; channel < totalNumOutputChannels; ++channel) - outputs[channel] = buffer.getWritePointer(channel); - - for(const auto metadata : midiMessages) + try { - const auto message = metadata.getMessage(); - - synthLib::SMidiEvent ev{}; + synthLib::Device* device = createDevice(); + getPlugin().setDevice(device); + (void)m_device.release(); + m_device.reset(device); - if(message.isSysEx() || message.getRawDataSize() > 3) - { - ev.sysex.resize(message.getRawDataSize()); - memcpy(ev.sysex.data(), message.getRawData(), ev.sysex.size()); + evRomChanged.retain(getSelectedRom()); - // 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 - 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); - - applyOutputGain(outputs, buffer.getNumSamples()); - - 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); - } - } } -//============================================================================== - -jucePluginEditorLib::PluginEditorState* AudioPluginAudioProcessor::createEditorState() +synthLib::Device* AudioPluginAudioProcessor::createDevice() { - return new PluginEditorState(*this, getController()); + 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; jucePluginEditorLib::PluginEditorState* createEditorState() override; - const juce::String getName() const 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); @@ -413,10 +416,14 @@ 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, false, 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)); @@ -671,37 +678,23 @@ namespace Virus return dst; } - std::vector<uint8_t> Controller::modifySingleDump(const std::vector<uint8_t>& _sysex, const virusLib::BankNumber _newBank, const uint8_t _newProgram) + 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::AnyPartParamValues 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); - if(_newBank == virusLib::BankNumber::EditBuffer) - { - const auto lockedParams = getLockedParameters(); - - if(!lockedParams.empty()) - { - const uint8_t part = _newProgram == virusLib::SINGLE ? 0 : _newProgram; - pluginLib::MidiPacket::NamedParamValues currentParams; - createNamedParamValues(currentParams, midiPacketName(MidiPacketType::SingleDump), part); - - for (const auto& name : lockedParams) - { - const auto it = currentParams.find({pluginLib::MidiPacket::AnyPart, name}); - if(it == currentParams.end()) - continue; - - const uint32_t idx = getParameterIndexByName(name); - if(idx != InvalidParameterIndex) - parameterValues[idx] = it->second; - } - } - } - return createSingleDump(toMidiByte(_newBank), _newProgram, parameterValues); + 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(const uint8_t _part) @@ -729,7 +722,6 @@ namespace Virus bool Controller::activatePatch(const std::vector<unsigned char>& _sysex) { - // re-pack, force to edit buffer return activatePatch(_sysex, isMultiMode() ? getCurrentPart() : static_cast<uint8_t>(virusLib::ProgramType::SINGLE)); } @@ -751,12 +743,24 @@ namespace Virus 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); diff --git a/source/jucePlugin/VirusController.h b/source/jucePlugin/VirusController.h @@ -68,7 +68,7 @@ namespace Virus 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::AnyPartParamValues& _paramValues); - std::vector<uint8_t> modifySingleDump(const std::vector<uint8_t>& _sysex, virusLib::BankNumber _newBank, uint8_t _newProgram); + 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); diff --git a/source/jucePlugin/parameterDescriptions_C.json b/source/jucePlugin/parameterDescriptions_C.json @@ -186,8 +186,8 @@ {"page":113, "index":42, "name":"Osc3 Volume", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, {"page":113, "index":43, "name":"Osc3 Semitone", "min":16, "max":112, "toText":"signed", "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, {"page":113, "index":44, "name":"Osc3 Detune", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, - {"page":113, "index":45, "name":"LowEQ Frequency", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, - {"page":113, "index":46, "name":"HighEQ Frequency", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, + {"page":113, "index":45, "name":"LowEQ Frequency", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, + {"page":113, "index":46, "name":"HighEQ Frequency", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, {"page":113, "index":47, "name":"Osc1 Shape Velocity", "min":0, "max":127, "toText":"signed", "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, {"page":113, "index":48, "name":"Osc2 Shape Velocity", "min":0, "max":127, "toText":"signed", "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, {"page":113, "index":49, "name":"PulseWidth Velocity", "min":0, "max":127, "toText":"signed", "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, @@ -233,11 +233,11 @@ {"page":113, "index":89, "name":"Phaser Feedback", "min":0, "max":127, "toText":"signed", "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, {"page":113, "index":90, "name":"Phaser Spread", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, {"page":113, "index":91, "name":"B_Undefined91", "min":0, "max":127, "toText":"unsignedZero", "isPublic":false, "isDiscrete":false, "isBool":false}, - {"page":113, "index":92, "name":"MidEQ Gain", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, - {"page":113, "index":93, "name":"MidEQ Frequency", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, - {"page":113, "index":94, "name":"MidEQ Q-Factor", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, - {"page":113, "index":95, "name":"LowEQ Gain", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, - {"page":113, "index":96, "name":"HighEQ Gain", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, + {"page":113, "index":92, "name":"MidEQ Gain", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, + {"page":113, "index":93, "name":"MidEQ Frequency", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, + {"page":113, "index":94, "name":"MidEQ Q-Factor", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar":true}, + {"page":113, "index":95, "name":"LowEQ Gain", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar": true}, + {"page":113, "index":96, "name":"HighEQ Gain", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false, "isBipolar": true}, {"page":113, "index":97, "name":"Bass Intensity", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, {"page":113, "index":98, "name":"Bass Tune", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, {"page":113, "index":99, "name":"Input Ringmodulator", "min":0, "max":127, "isPublic":true, "isDiscrete":false, "isBool":false}, @@ -325,7 +325,7 @@ {"page":114, "index":73, "name":"Part Midi Volume Enable", "min":0, "max":1, "isPublic":false, "isDiscrete":false, "isBool":true}, {"page":114, "index":74, "name":"Part Hold Pedal Enable", "min":0, "max":1, "isPublic":false, "isDiscrete":false, "isBool":true}, {"page":114, "index":75, "name":"Keyb To Midi", "min":0, "max":1, "isPublic":false, "isDiscrete":false, "isBool":true}, - {"page":114, "index":76, "name":"C_Undefined76", "min":0, "max":127, "toText":"unsignedZero", "isPublic":false, "isDiscrete":false, "isBool":false}, + {"page":114, "index":76, "name":"Pure Tuning", "min":0, "max":127, "toText":"unsignedZero", "isPublic":true, "isDiscrete":false, "isBool":false}, {"page":114, "index":77, "name":"Note Steal Priority", "min":0, "max":1, "isPublic":false, "isDiscrete":false, "isBool":true}, {"page":114, "index":78, "name":"Part Prog Change Enable", "min":0, "max":1, "isPublic":false, "isDiscrete":false, "isBool":true}, {"page":114, "index":79, "name":"C_Undefined79", "min":0, "max":127, "toText":"unsignedZero", "isPublic":false, "isDiscrete":false, "isBool":false}, diff --git a/source/jucePlugin/skins/Hoverland/ARP_2034x1238_x2.png b/source/jucePlugin/skins/Hoverland/ARP_2034x1238_x2.png Binary files differ. diff --git a/source/jucePlugin/skins/Hoverland/VirusC_Hoverland.json b/source/jucePlugin/skins/Hoverland/VirusC_Hoverland.json @@ -1927,7 +1927,7 @@ "y" : "90", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -1961,7 +1961,7 @@ "y" : "89", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -1995,7 +1995,7 @@ "y" : "328", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2029,7 +2029,7 @@ "y" : "327", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2097,7 +2097,7 @@ "y" : "517", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2114,7 +2114,7 @@ "y" : "517", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2131,7 +2131,7 @@ "y" : "761", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2216,7 +2216,7 @@ "y" : "997.7", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2418,7 +2418,7 @@ "y" : "519", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2540,7 +2540,7 @@ "y" : "90", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2557,7 +2557,7 @@ "y" : "89", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2574,7 +2574,7 @@ "y" : "89", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2678,7 +2678,7 @@ "y" : "519.7", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2695,7 +2695,7 @@ "y" : "518.7", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2712,7 +2712,7 @@ "y" : "518.7", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -2924,7 +2924,7 @@ "y" : "761", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3009,7 +3009,7 @@ "y" : "998", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3161,7 +3161,7 @@ "y" : "250", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3195,7 +3195,7 @@ "y" : "84", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3212,7 +3212,7 @@ "y" : "84", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3229,7 +3229,7 @@ "y" : "251", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3246,7 +3246,7 @@ "y" : "250", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3263,7 +3263,7 @@ "y" : "84", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3280,7 +3280,7 @@ "y" : "84", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3440,7 +3440,7 @@ "y" : "630", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3474,7 +3474,7 @@ "y" : "464", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3491,7 +3491,7 @@ "y" : "464", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3508,7 +3508,7 @@ "y" : "631", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3525,7 +3525,7 @@ "y" : "630", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3542,7 +3542,7 @@ "y" : "464", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -3559,7 +3559,7 @@ "y" : "464", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -4288,7 +4288,7 @@ "y" : "324", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -4389,7 +4389,7 @@ "y" : "562", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -4457,7 +4457,7 @@ "y" : "800", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -4491,7 +4491,7 @@ "y" : "800", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -4508,7 +4508,7 @@ "y" : "800", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -4542,7 +4542,7 @@ "y" : "800", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -4691,7 +4691,7 @@ "y" : "36", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -4807,7 +4807,7 @@ "y" : "36", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -5060,7 +5060,7 @@ "y" : "770", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -5111,7 +5111,7 @@ "y" : "952", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -5347,6 +5347,23 @@ } }, { + "name" : "PureTuning", + "parameterAttachment" : { + "parameter" : "Pure Tuning" + }, + "rotary" : { + }, + "spritesheet" : { + "x" : "107", + "y" : "769", + "width" : "140", + "height" : "140", + "texture" : "Gen_140x140_x2", + "tileSizeX" : "140", + "tileSizeY" : "140" + } + }, + { "name" : "ArpModeButton", "parameterAttachment" : { "parameter" : "Arp Mode" @@ -5482,7 +5499,7 @@ "y" : "90", "width" : "140", "height" : "140", - "texture" : "Gen_140x140_x2", + "texture" : "GenPol_140x140_x2", "tileSizeX" : "140", "tileSizeY" : "140" } @@ -5841,4 +5858,4 @@ } } ] -} -\ No newline at end of file +} 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 @@ -32,23 +32,52 @@ namespace genericVirusUI void PartButton::selectPreset(uint8_t _part) const { - juce::PopupMenu selector; - - for (uint8_t b = 0; b < static_cast<uint8_t>(m_editor.getController().getBankCount()); ++b) - { - const auto bank = virusLib::fromArrayIndex(b); - auto presetNames = m_editor.getController().getSinglePresetNames(bank); - juce::PopupMenu p; - for (uint8_t j = 0; j < static_cast<uint8_t>(presetNames.size()); j++) - { - const auto& presetName = presetNames[j]; - p.addItem(presetName, [this, bank, j, _part] - { - m_editor.selectRomPreset(_part, bank, j); - }); - } - selector.addSubMenu(m_editor.getController().getBankName(b), p); - } - selector.showMenuAsync(juce::PopupMenu::Options()); + 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 @@ -1,8 +1,5 @@ #pragma once -#include "PatchManager.h" -#include "juce_gui_basics/juce_gui_basics.h" - #include "../../jucePluginEditorLib/partbutton.h" namespace genericVirusUI diff --git a/source/jucePlugin/ui3/Parts.cpp b/source/jucePlugin/ui3/Parts.cpp @@ -4,11 +4,15 @@ #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 "../../jucePluginEditorLib/patchmanager/savepatchdesc.h" +#include "../../virusLib/device.h" namespace genericVirusUI { @@ -21,6 +25,7 @@ namespace genericVirusUI _editor.findComponents<juce::Slider>(m_partVolume, "PartVolume"); _editor.findComponents<juce::Slider>(m_partPan, "PartPan"); _editor.findComponents<PartButton>(m_presetName, "PresetName"); + _editor.findComponents<juce::Component>(m_partActive, "PartActive"); for(size_t i=0; i<m_partSelect.size(); ++i) { @@ -55,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; @@ -150,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); @@ -166,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 @@ -26,11 +26,11 @@ namespace genericVirusUI std::function<void(const juce::MouseEvent&, int)> m_callback; }; - class Parts + class Parts : juce::Timer { public: explicit Parts(VirusEditor& _editor); - virtual ~Parts(); + ~Parts() override; void onProgramChange() const; void onPlayModeChanged() const; @@ -47,6 +47,8 @@ namespace genericVirusUI void updateAll() const; void updateSingleOrMultiMode() const; + void timerCallback() override; + VirusEditor& m_editor; std::vector<genericUI::Button<juce::DrawableButton>*> m_partSelect; @@ -55,6 +57,7 @@ namespace genericVirusUI std::vector<juce::Slider*> m_partVolume; std::vector<juce::Slider*> m_partPan; + std::vector<juce::Component*> m_partActive; std::vector<PartButton*> m_presetName; }; diff --git a/source/jucePlugin/ui3/PatchManager.cpp b/source/jucePlugin/ui3/PatchManager.cpp @@ -22,13 +22,6 @@ namespace Virus namespace genericVirusUI { - void PatchManager::selectRomPreset(uint32_t _part, virusLib::BankNumber _bank, uint8_t _program) - { - const pluginLib::patchDB::DataSource ds = createRomDataSource(toArrayIndex(_bank)); - - selectPatch(_part, ds, _program); - } - PatchManager::PatchManager(VirusEditor& _editor, juce::Component* _root, const juce::File& _dir) : jucePluginEditorLib::patchManager::PatchManager(_editor, _root, _dir), m_controller(_editor.getController()) { addRomPatches(); diff --git a/source/jucePlugin/ui3/PatchManager.h b/source/jucePlugin/ui3/PatchManager.h @@ -20,8 +20,6 @@ namespace genericVirusUI PatchManager(VirusEditor& _editor, juce::Component* _root, const juce::File& _dir); ~PatchManager() override; - void selectRomPreset(uint32_t _part, virusLib::BankNumber _bank, uint8_t _program); - // PatchDB impl bool loadRomData(pluginLib::patchDB::DataList& _results, uint32_t _bank, uint32_t _program) override; std::shared_ptr<pluginLib::patchDB::Patch> initializePatch(std::vector<uint8_t>&& _sysex) override; diff --git a/source/jucePlugin/ui3/VirusEditor.cpp b/source/jucePlugin/ui3/VirusEditor.cpp @@ -19,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) @@ -71,12 +73,29 @@ 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(static_cast<int>(m_processor.getSelectedRomIndex()) + 1, juce::dontSendNotification); - m_romSelector->setSelectedId(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](int _part) { onProgramChange(_part); }; @@ -96,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(); }; @@ -129,6 +146,13 @@ namespace genericVirusUI updatePresetName(); updatePlayModeButtons(); + + m_romChangedListener = [this](auto) + { + updateDeviceModel(); + updateKeyValueConditions("deviceModel", virusLib::getModelName(m_processor.getModel())); + m_parts->onPlayModeChanged(); + }; } VirusEditor::~VirusEditor() @@ -149,18 +173,6 @@ namespace genericVirusUI return static_cast<Virus::Controller&>(m_processor.getController()); } - void VirusEditor::selectRomPreset(const uint8_t _part, const virusLib::BankNumber _bank, const uint8_t _program) const - { - if(getPatchManager()) - { - static_cast<PatchManager*>(getPatchManager())->selectRomPreset(_part, _bank, _program); - } - else - { - getController().setCurrentPartPreset(_part, _bank, _program); - } - } - const char* VirusEditor::findEmbeddedResource(const std::string& _filename, uint32_t& _size) { for(size_t i=0; i<BinaryData::namedResourceListSize; ++i) @@ -204,7 +216,6 @@ namespace genericVirusUI m_parts->onProgramChange(); updatePresetName(); updatePlayModeButtons(); - updateDeviceModel(); if(getPatchManager()) getPatchManager()->onProgramChanged(_part); } @@ -247,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; @@ -265,13 +279,17 @@ namespace genericVirusUI } m_deviceModel->setText(m, juce::dontSendNotification); - m_deviceModel = nullptr; // only update once } void VirusEditor::savePreset() { juce::PopupMenu menu; + 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; @@ -282,14 +300,14 @@ namespace genericVirusUI _menu.addSubMenu(_name, subMenu); }; - addEntry(menu, "Current Single (Edit Buffer)", [this](jucePluginEditorLib::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](jucePluginEditorLib::FileType _type) + addEntry(menu, "Export Arrangement (Multi + 16 Singles)", [this](jucePluginEditorLib::FileType _type) { savePresets(SaveType::Arrangement, _type); }); @@ -304,7 +322,7 @@ namespace genericVirusUI }); } - menu.addSubMenu("Bank", banksMenu); + menu.addSubMenu("Export Bank", banksMenu); menu.showMenuAsync(juce::PopupMenu::Options()); } @@ -333,7 +351,7 @@ namespace genericVirusUI if (results.size() == 1) { - c.sendSysEx(results.front()); + c.activatePatch(results.front()); } else if(results.size() > 1) { @@ -342,8 +360,6 @@ namespace genericVirusUI "Go to the Patch Manager, right click the 'Data Sources' node and select 'Add File...' to import it." ); } - - c.onStateLoaded(); }); } 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 "PatchManager.h" #include "ControllerLinks.h" +#include "Leds.h" + +namespace virusLib +{ + class ROMFile; +} namespace jucePluginEditorLib { @@ -45,8 +53,6 @@ namespace genericVirusUI Virus::Controller& getController() const; - void selectRomPreset(uint8_t _part, virusLib::BankNumber _bank, uint8_t _program) const; - static const char* findEmbeddedResource(const std::string& _filename, uint32_t& _size); const char* findResourceByFilename(const std::string& _filename, uint32_t& _size) override; @@ -78,6 +84,7 @@ namespace genericVirusUI 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; @@ -97,5 +104,7 @@ namespace genericVirusUI juce::Label* m_deviceModel = nullptr; 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,6 +4,8 @@ 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 partbutton.cpp partbutton.h pluginEditor.cpp pluginEditor.h @@ -48,3 +50,4 @@ 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/focusedParameterTooltip.cpp b/source/jucePluginEditorLib/focusedParameterTooltip.cpp @@ -4,7 +4,7 @@ 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); } @@ -35,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(); @@ -52,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 @@ -20,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/patchmanager/datasourcetreeitem.cpp b/source/jucePluginEditorLib/patchmanager/datasourcetreeitem.cpp @@ -22,10 +22,20 @@ namespace jucePluginEditorLib::patchManager case pluginLib::patchDB::SourceType::Count: return {}; case pluginLib::patchDB::SourceType::Rom: - case pluginLib::patchDB::SourceType::File: - case pluginLib::patchDB::SourceType::Folder: case pluginLib::patchDB::SourceType::LocalStorage: return _ds.name; + 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"; @@ -64,7 +74,7 @@ namespace jucePluginEditorLib::patchManager return m_dataSource->type == pluginLib::patchDB::SourceType::LocalStorage; } - void DatasourceTreeItem::patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) + void DatasourceTreeItem::patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches, const SavePatchDesc* _savePatchDesc/* = nullptr*/) { TreeItem::patchesDropped(_patches); @@ -81,7 +91,9 @@ namespace jucePluginEditorLib::patchManager #if SYNTHLIB_DEMO_MODE getPatchManager().getEditor().showDemoRestrictionMessageBox(); #else - getPatchManager().copyPatchesTo(m_dataSource, _patches); + const int part = _savePatchDesc ? _savePatchDesc->getPart() : -1; + + getPatchManager().copyPatchesToLocalStorage(m_dataSource, _patches, part); #endif } } @@ -89,7 +101,10 @@ namespace jucePluginEditorLib::patchManager void DatasourceTreeItem::itemClicked(const juce::MouseEvent& _mouseEvent) { if(!_mouseEvent.mods.isPopupMenu()) + { + TreeItem::itemClicked(_mouseEvent); return; + } juce::PopupMenu menu; diff --git a/source/jucePluginEditorLib/patchmanager/datasourcetreeitem.h b/source/jucePluginEditorLib/patchmanager/datasourcetreeitem.h @@ -22,7 +22,7 @@ namespace jucePluginEditorLib::patchManager bool isInterestedInSavePatchDesc(const SavePatchDesc& _desc) override; bool isInterestedInPatchList(const List* _list, const juce::Array<juce::var>& _indices) override; - void patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) override; + void patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches, const SavePatchDesc* _savePatchDesc = nullptr) override; void itemClicked(const juce::MouseEvent&) override; void refresh(); diff --git a/source/jucePluginEditorLib/patchmanager/editable.cpp b/source/jucePluginEditorLib/patchmanager/editable.cpp @@ -28,6 +28,7 @@ namespace jucePluginEditorLib::patchManager _parent->addAndMakeVisible(m_editorLabel); m_editorLabel->showEditor(); + m_editorLabel->getCurrentTextEditor()->addListener(this); m_finishedEditingCallback = std::move(_callback); @@ -43,11 +44,20 @@ namespace jucePluginEditorLib::patchManager m_finishedEditingCallback(true, text); destroyEditorLabel(); } - Listener::editorHidden(_label, _textEditor); + 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() diff --git a/source/jucePluginEditorLib/patchmanager/editable.h b/source/jucePluginEditorLib/patchmanager/editable.h @@ -7,7 +7,7 @@ namespace jucePluginEditorLib::patchManager { - class Editable : juce::Label::Listener + class Editable : juce::Label::Listener, juce::TextEditor::Listener { public: using FinishedEditingCallback = std::function<void(bool, const std::string&)>; @@ -20,6 +20,7 @@ namespace jucePluginEditorLib::patchManager // juce::Label::Listener void editorHidden(juce::Label*, juce::TextEditor&) override; void labelTextChanged(juce::Label* _label) override; + void textEditorTextChanged (juce::TextEditor&) override; private: void destroyEditorLabel(); diff --git a/source/jucePluginEditorLib/patchmanager/grouptreeitem.cpp b/source/jucePluginEditorLib/patchmanager/grouptreeitem.cpp @@ -48,6 +48,8 @@ namespace jucePluginEditorLib::patchManager if (getNumSubItems() == 1 && oldNumSubItems == 0) setOpen(true); } + + setDeselectonSecondClick(true); } void GroupTreeItem::removeItem(const DatasourceTreeItem* _item) @@ -120,76 +122,91 @@ namespace jucePluginEditorLib::patchManager void GroupTreeItem::itemClicked(const juce::MouseEvent& _mouseEvent) { - if(_mouseEvent.mods.isPopupMenu()) + if(!_mouseEvent.mods.isPopupMenu()) { TreeItem::itemClicked(_mouseEvent); + return; + } - juce::PopupMenu menu; + juce::PopupMenu menu; - const auto tagType = toTagType(m_type); + const auto tagType = toTagType(m_type); - if(m_type == GroupType::DataSources) + if(m_type == GroupType::DataSources) + { + menu.addItem("Add Folders...", [this] { - menu.addItem("Add Folder...", [this] - { - juce::FileChooser fc("Select Folder"); + juce::FileChooser fc("Select Folders"); - if(fc.browseForDirectory()) + if(fc.showDialog( + juce::FileBrowserComponent::openMode | + juce::FileBrowserComponent::canSelectDirectories | + juce::FileBrowserComponent::canSelectMultipleItems + , nullptr)) + { + for (const auto& r : fc.getResults()) { - const auto result = fc.getResult().getFullPathName().toStdString(); + 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 File...", [this] + menu.addItem("Add Files...", [this] + { + juce::FileChooser fc("Select Files"); + if(fc.showDialog( + juce::FileBrowserComponent::openMode | + juce::FileBrowserComponent::canSelectFiles | + juce::FileBrowserComponent::canSelectMultipleItems, + nullptr)) { - juce::FileChooser fc("Select File"); - if(fc.browseForFileToOpen()) + for (const auto&r : fc.getResults()) { - const auto result = fc.getResult().getFullPathName().toStdString(); + 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) + } + }); + } + else if(m_type == GroupType::LocalStorage) + { + menu.addItem("Create...", [this] { - menu.addItem("Create...", [this] + beginEdit("Enter name...", [this](bool _success, const std::string& _newText) { - beginEdit("Enter name...", [this](bool _success, const std::string& _newText) - { - pluginLib::patchDB::DataSource ds; + 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(); + 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); - }); + getPatchManager().addDataSource(ds); }); - } - if(tagType != pluginLib::patchDB::TagType::Invalid) + }); + } + if(tagType != pluginLib::patchDB::TagType::Invalid) + { + menu.addItem("Add...", [this] { - menu.addItem("Add...", [this] + beginEdit("Enter name...", [this](bool _success, const std::string& _newText) { - beginEdit("Enter name...", [this](bool _success, const std::string& _newText) - { - if (!_newText.empty()) - getPatchManager().addTag(toTagType(m_type), _newText); - }); + if (!_newText.empty()) + getPatchManager().addTag(toTagType(m_type), _newText); }); - } - - menu.showMenuAsync(juce::PopupMenu::Options()); + }); } + + menu.showMenuAsync(juce::PopupMenu::Options()); } void GroupTreeItem::setFilter(const std::string& _filter) @@ -273,13 +290,8 @@ namespace jucePluginEditorLib::patchManager m_itemsByDataSource.insert({ _dataSource, item }); - const auto oldNumSubItems = getNumSubItems(); - validateParent(_dataSource, item); - if(getNumSubItems() == 1 && oldNumSubItems == 0) - setOpen(true); - return item; } @@ -291,6 +303,8 @@ namespace jucePluginEditorLib::patchManager m_itemsByTag.insert({ _tag, item }); + item->onParentSearchChanged(getParentSearchRequest()); + return item; } diff --git a/source/jucePluginEditorLib/patchmanager/info.cpp b/source/jucePluginEditorLib/patchmanager/info.cpp @@ -76,7 +76,7 @@ namespace jucePluginEditorLib::patchManager m_content.deleteAllChildren(); } - void Info::setPatch(const pluginLib::patchDB::PatchPtr& _patch) const + void Info::setPatch(const pluginLib::patchDB::PatchPtr& _patch) { if (!_patch) { @@ -84,6 +84,18 @@ namespace jucePluginEditorLib::patchManager 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); @@ -92,8 +104,12 @@ namespace jucePluginEditorLib::patchManager doLayout(); } - void Info::clear() const + 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); @@ -148,6 +164,14 @@ namespace jucePluginEditorLib::patchManager 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); diff --git a/source/jucePluginEditorLib/patchmanager/info.h b/source/jucePluginEditorLib/patchmanager/info.h @@ -19,13 +19,16 @@ namespace jucePluginEditorLib::patchManager Info(PatchManager& _pm); ~Info() override; - void setPatch(const pluginLib::patchDB::PatchPtr& _patch) const; - void clear() const; + 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; @@ -42,5 +45,8 @@ namespace jucePluginEditorLib::patchManager 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 @@ -59,6 +59,15 @@ namespace jucePluginEditorLib::patchManager 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); diff --git a/source/jucePluginEditorLib/patchmanager/list.h b/source/jucePluginEditorLib/patchmanager/list.h @@ -29,6 +29,7 @@ namespace jucePluginEditorLib::patchManager void setContent(const pluginLib::patchDB::SearchHandle& _handle); void setContent(pluginLib::patchDB::SearchRequest&& _request); + void clear(); void refreshContent(); diff --git a/source/jucePluginEditorLib/patchmanager/listitem.cpp b/source/jucePluginEditorLib/patchmanager/listitem.cpp @@ -125,7 +125,14 @@ namespace jucePluginEditorLib::patchManager #if SYNTHLIB_DEMO_MODE pm.getEditor().showDemoRestrictionMessageBox(); #else - pm.copyPatchesTo(source, {patch}, row); + 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 } diff --git a/source/jucePluginEditorLib/patchmanager/patchmanager.cpp b/source/jucePluginEditorLib/patchmanager/patchmanager.cpp @@ -13,6 +13,7 @@ #include "../pluginEditor.h" #include "../../jucePluginLib/types.h" + #include "juce_gui_extra/misc/juce_ColourSelector.h" namespace jucePluginEditorLib::patchManager @@ -94,6 +95,10 @@ namespace jucePluginEditorLib::patchManager 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); @@ -148,6 +153,8 @@ namespace jucePluginEditorLib::patchManager m_status->setScanning(isScanning()); + m_info->processDirty(dirty); + if(!dirty.errors.empty()) { std::string msg = "Patch Manager encountered errors:\n\n"; @@ -235,6 +242,14 @@ namespace jucePluginEditorLib::patchManager 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)) @@ -271,6 +286,72 @@ namespace jucePluginEditorLib::patchManager 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); @@ -288,7 +369,7 @@ namespace jucePluginEditorLib::patchManager if(searchHandle == pluginLib::patchDB::g_invalidSearchHandle) return false; - auto s = getSearch(searchHandle); + const auto s = getSearch(searchHandle); if(!s) return false; diff --git a/source/jucePluginEditorLib/patchmanager/patchmanager.h b/source/jucePluginEditorLib/patchmanager/patchmanager.h @@ -67,13 +67,20 @@ namespace jucePluginEditorLib::patchManager 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; - private: - bool selectPatch(uint32_t _part, int _offset); + 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; diff --git a/source/jucePluginEditorLib/patchmanager/state.cpp b/source/jucePluginEditorLib/patchmanager/state.cpp @@ -111,7 +111,7 @@ namespace jucePluginEditorLib::patchManager for(uint32_t i=0; i<patches.size(); ++i) { - if(pluginLib::patchDB::PatchKey(*patches[i]) == _patch) + if(*patches[i] == _patch) { index = i; break; diff --git a/source/jucePluginEditorLib/patchmanager/tagstree.cpp b/source/jucePluginEditorLib/patchmanager/tagstree.cpp @@ -1,5 +1,6 @@ #include "tagstree.h" +#include "grouptreeitem.h" #include "notagtreeitem.h" namespace jucePluginEditorLib::patchManager @@ -8,7 +9,7 @@ namespace jucePluginEditorLib::patchManager { addGroup(GroupType::Categories); m_uncategorized = new NoTagTreeItem(_pm, pluginLib::patchDB::TagType::Category, "Uncategorized"); - getRootItem()->addSubItem(m_uncategorized); + getItem(GroupType::Categories)->addSubItem(m_uncategorized); addGroup(GroupType::Tags); setMultiSelectEnabled(true); diff --git a/source/jucePluginEditorLib/patchmanager/tagtreeitem.cpp b/source/jucePluginEditorLib/patchmanager/tagtreeitem.cpp @@ -17,6 +17,8 @@ namespace jucePluginEditorLib::patchManager search(std::move(sr)); } + + setDeselectonSecondClick(true); } bool TagTreeItem::isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) @@ -29,7 +31,7 @@ namespace jucePluginEditorLib::patchManager return hasSearch() && toTagType(getGroupType()) != pluginLib::patchDB::TagType::Invalid; } - void TagTreeItem::patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) + void TagTreeItem::patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches, const SavePatchDesc*/* _savePatchDesc = nullptr*/) { const auto tagType = toTagType(getGroupType()); @@ -61,50 +63,53 @@ namespace jucePluginEditorLib::patchManager void TagTreeItem::itemClicked(const juce::MouseEvent& _mouseEvent) { - if(_mouseEvent.mods.isPopupMenu()) + if(!_mouseEvent.mods.isPopupMenu()) { - const auto tagType = toTagType(getGroupType()); + TreeItem::itemClicked(_mouseEvent); + return; + } + + const auto tagType = toTagType(getGroupType()); - if(tagType != pluginLib::patchDB::TagType::Invalid) + if(tagType != pluginLib::patchDB::TagType::Invalid) + { + juce::PopupMenu menu; + const auto& s = getPatchManager().getSearch(getSearchHandle()); + if(s && !s->getResultSize()) { - 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] + menu.addItem("Remove", [this, tagType] { - juce::ColourSelector* cs = new juce::ColourSelector(juce::ColourSelector::showColourAtTop | juce::ColourSelector::showSliders | juce::ColourSelector::showColourspace); + 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->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()); + 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()); + 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) + juce::CallOutBox::launchAsynchronously(std::unique_ptr<juce::Component>(cs), rect, nullptr); + }); + if(getColor() != pluginLib::patchDB::g_invalidColor) + { + menu.addItem("Clear Color", [this, tagType] { - menu.addItem("Clear Color", [this, tagType] - { - getPatchManager().setTagColor(tagType, getTag(), pluginLib::patchDB::g_invalidColor); - getPatchManager().repaint(); - }); - } - - menu.showMenuAsync({}); + getPatchManager().setTagColor(tagType, getTag(), pluginLib::patchDB::g_invalidColor); + getPatchManager().repaint(); + }); } + + menu.showMenuAsync({}); } } diff --git a/source/jucePluginEditorLib/patchmanager/tagtreeitem.h b/source/jucePluginEditorLib/patchmanager/tagtreeitem.h @@ -19,7 +19,7 @@ namespace jucePluginEditorLib::patchManager bool isInterestedInDragSource(const juce::DragAndDropTarget::SourceDetails& dragSourceDetails) override; bool isInterestedInPatchList(const List* _list, const juce::Array<juce::var>& _indices) override; - void patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) override; + void 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; } diff --git a/source/jucePluginEditorLib/patchmanager/treeitem.cpp b/source/jucePluginEditorLib/patchmanager/treeitem.cpp @@ -105,6 +105,19 @@ namespace jucePluginEditorLib::patchManager 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()) @@ -138,7 +151,7 @@ namespace jucePluginEditorLib::patchManager return; if(auto patch = getPatchManager().requestPatchForPart(desc->getPart())) - patchesDropped({patch}); + patchesDropped({patch}, desc); } } @@ -180,6 +193,11 @@ namespace jucePluginEditorLib::patchManager 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) @@ -225,7 +243,8 @@ namespace jucePluginEditorLib::patchManager _g.setFont(fnt); } - const juce::String t(m_text); + + 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); } @@ -253,6 +272,33 @@ namespace jucePluginEditorLib::patchManager 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) @@ -262,7 +308,7 @@ namespace jucePluginEditorLib::patchManager m_searchHandle = pluginLib::patchDB::g_invalidSearchHandle; } - void TreeItem::patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches) + 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 @@ -42,7 +42,7 @@ namespace jucePluginEditorLib::patchManager 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); + virtual void patchesDropped(const std::vector<pluginLib::patchDB::PatchPtr>& _patches, const SavePatchDesc* _savePatchDesc = nullptr); bool hasSearch() const; @@ -68,11 +68,16 @@ namespace jucePluginEditorLib::patchManager 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; } @@ -92,5 +97,9 @@ namespace jucePluginEditorLib::patchManager 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/pluginEditor.cpp b/source/jucePluginEditorLib/pluginEditor.cpp @@ -17,6 +17,7 @@ namespace jucePluginEditorLib , m_binding(_binding) , m_skinFolder(std::move(_skinFolder)) { + showDisclaimer(); } Editor::~Editor() = default; @@ -162,6 +163,27 @@ namespace jucePluginEditorLib 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 @@ -58,6 +58,8 @@ namespace jucePluginEditorLib 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; diff --git a/source/jucePluginEditorLib/pluginEditorState.cpp b/source/jucePluginEditorLib/pluginEditorState.cpp @@ -10,6 +10,7 @@ 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) { } @@ -124,7 +125,7 @@ void PluginEditorState::loadSkin(const Skin& _skin) } } -void PluginEditorState::setGuiScale(int _scale) const +void PluginEditorState::setGuiScale(const int _scale) const { if(evSetGuiScale) evSetGuiScale(_scale); @@ -138,18 +139,23 @@ genericUI::Editor* PluginEditorState::getEditor() const 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; @@ -158,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) { @@ -186,32 +192,48 @@ void PluginEditorState::openMenu() } } - if(m_editor && 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_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();}); } } juce::PopupMenu scaleMenu; scaleMenu.addItem("50%", true, scale == 50, [this] { setGuiScale(50); }); + scaleMenu.addItem("65%", true, scale == 65, [this] { setGuiScale(65); }); scaleMenu.addItem("75%", true, scale == 75, [this] { setGuiScale(75); }); + scaleMenu.addItem("85%", true, scale == 85, [this] { setGuiScale(85); }); scaleMenu.addItem("100%", true, scale == 100, [this] { setGuiScale(100); }); scaleMenu.addItem("125%", true, scale == 125, [this] { setGuiScale(125); }); scaleMenu.addItem("150%", true, scale == 150, [this] { setGuiScale(150); }); + scaleMenu.addItem("175%", true, scale == 175, [this] { setGuiScale(175); }); scaleMenu.addItem("200%", true, scale == 200, [this] { setGuiScale(200); }); + 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); @@ -304,7 +326,7 @@ void PluginEditorState::exportCurrentSkin() const if(!editor) return; - const auto res = editor->exportToFolder(synthLib::getModulePath() + "skins/"); + const auto res = editor->exportToFolder(synthLib::getModulePath() + m_skinFolderName + '/'); if(!res.empty()) { diff --git a/source/jucePluginEditorLib/pluginEditorState.h b/source/jucePluginEditorLib/pluginEditorState.h @@ -94,5 +94,6 @@ namespace jucePluginEditorLib 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 @@ -20,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(); @@ -38,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) @@ -59,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) 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 @@ -7,13 +7,18 @@ 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)) @@ -47,38 +52,14 @@ namespace jucePluginEditorLib return new EditorWindow(*this, *m_editorState, getConfig()); } - void Processor::loadCustomData(const std::vector<uint8_t>& _sourceBuffer) + void Processor::destroyEditorState() { - // Compatibility with old Vavra versions that only wrote gain parameters - if(_sourceBuffer.size() == sizeof(float) * 2 + sizeof(uint32_t)) - { - pluginLib::Processor::loadCustomData(_sourceBuffer); - return; - } - - pluginLib::Processor::loadCustomData(_sourceBuffer); - - synthLib::BinaryStream s(_sourceBuffer); - - synthLib::ChunkReader cr(s); - cr.add("EDST", 1, [this](synthLib::BinaryStream& _binaryStream, unsigned _version) - { - _binaryStream.read(m_editorStateData); - }); - - // if there is no chunk in the data, it's an old non-Vavra chunk that only carries the editor state - if(!cr.tryRead() || cr.numRead() == 0) - { - m_editorStateData = _sourceBuffer; - } - - if(m_editorState) - m_editorState->setPerInstanceConfig(m_editorStateData); + m_editorState.reset(); } - void Processor::saveCustomData(std::vector<uint8_t>& _targetBuffer) + void Processor::saveChunkData(synthLib::BinaryStream& s) { - pluginLib::Processor::saveCustomData(_targetBuffer); + pluginLib::Processor::saveChunkData(s); if(m_editorState) { @@ -88,13 +69,30 @@ namespace jucePluginEditorLib if(!m_editorStateData.empty()) { - synthLib::BinaryStream s; - { - synthLib::ChunkWriter cw(s, "EDST", 1); - s.write(m_editorStateData); - } - - s.toVector(_targetBuffer, true); + 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 @@ -9,7 +9,8 @@ namespace jucePluginEditorLib 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; } @@ -20,9 +21,11 @@ namespace jucePluginEditorLib juce::AudioProcessorEditor* createEditor() override; virtual PluginEditorState* createEditorState() = 0; + void destroyEditorState(); - void loadCustomData(const std::vector<uint8_t>& _sourceBuffer) override; - void saveCustomData(std::vector<uint8_t>& _targetBuffer) override; + 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; diff --git a/source/jucePluginLib/CMakeLists.txt b/source/jucePluginLib/CMakeLists.txt @@ -13,6 +13,7 @@ set(SOURCES parameterlink.cpp parameterlink.h parameterregion.cpp parameterregion.h processor.cpp processor.h + softknob.cpp softknob.h types.h ) @@ -38,3 +39,4 @@ 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 { @@ -430,7 +456,7 @@ namespace pluginLib return m_lockedRegions.find(_id) != m_lockedRegions.end(); } - std::unordered_set<std::string> Controller::getLockedParameters() const + std::unordered_set<std::string> Controller::getLockedParameterNames() const { if(m_lockedRegions.empty()) return {}; @@ -451,6 +477,39 @@ namespace pluginLib 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,8 @@ #include <string> +#include "softknob.h" + namespace juce { class AudioProcessor; @@ -24,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; @@ -64,10 +66,16 @@ namespace pluginLib bool unlockRegion(const std::string& _id); const std::set<std::string>& getLockedRegions() const; bool isRegionLocked(const std::string& _id); - std::unordered_set<std::string> getLockedParameters() const; - + 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); @@ -115,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 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/parameterbinding.cpp b/source/jucePluginLib/parameterbinding.cpp @@ -27,6 +27,11 @@ namespace pluginLib m_param->setValueNotifyingHost(m_param->convertTo0to1(static_cast<float>(m_slider->getValue())), Parameter::ChangedBy::ControlChange); } + void ParameterBinding::MouseListener::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); diff --git a/source/jucePluginLib/parameterbinding.h b/source/jucePluginLib/parameterbinding.h @@ -25,6 +25,7 @@ namespace pluginLib void mouseDown(const juce::MouseEvent& event) override; void mouseUp(const juce::MouseEvent& event) override; void mouseDrag(const juce::MouseEvent& event) override; + void mouseWheelMove(const juce::MouseEvent& event, const juce::MouseWheelDetails& wheel) override; void mouseDoubleClick(const juce::MouseEvent& event) override; private: 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 @@ -53,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,6 +335,56 @@ 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); @@ -339,7 +399,7 @@ namespace pluginLib if(!res.empty()) { LOG("ParameterDescription parsing issues:\n" << res); - assert(false); + assert(false && "failed to parse parameter descriptions"); } return res; @@ -469,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; diff --git a/source/jucePluginLib/parameterdescriptions.h b/source/jucePluginLib/parameterdescriptions.h @@ -9,8 +9,6 @@ #include "parameterlink.h" #include "parameterregion.h" -#include "juce_core/juce_core.h" - namespace juce { class var; @@ -39,6 +37,8 @@ namespace pluginLib 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); 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.h b/source/jucePluginLib/parameterregion.h @@ -16,6 +16,11 @@ namespace pluginLib 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; diff --git a/source/jucePluginLib/patchdb/datasource.cpp b/source/jucePluginLib/patchdb/datasource.cpp @@ -5,6 +5,9 @@ #include <memory> #include "patch.h" +#include "patchmodifications.h" + +#include "../../synthLib/binarystream.h" namespace pluginLib::patchDB { @@ -131,6 +134,16 @@ namespace pluginLib::patchDB 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; @@ -144,6 +157,51 @@ namespace pluginLib::patchDB 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) { } diff --git a/source/jucePluginLib/patchdb/datasource.h b/source/jucePluginLib/patchdb/datasource.h @@ -4,8 +4,15 @@ #include "patchdbtypes.h" +namespace synthLib +{ + class BinaryStream; +} + namespace pluginLib::patchDB { + struct PatchKey; + struct DataSource { SourceType type = SourceType::Invalid; @@ -77,6 +84,8 @@ namespace pluginLib::patchDB return !(*this == _ds); } + PatchPtr getPatch(const PatchKey& _key) const; + bool operator < (const DataSource& _ds) const { // if (parent < _ds.parent) return true; @@ -105,6 +114,9 @@ namespace pluginLib::patchDB } 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> diff --git a/source/jucePluginLib/patchdb/db.cpp b/source/jucePluginLib/patchdb/db.cpp @@ -9,11 +9,14 @@ #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) @@ -34,6 +37,7 @@ namespace pluginLib::patchDB 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) { } @@ -41,7 +45,14 @@ namespace pluginLib::patchDB 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) @@ -62,7 +73,12 @@ namespace pluginLib::patchDB sysexBuffer.insert(sysexBuffer.end(), patchSysex.begin(), patchSysex.end()); } - return _file.replaceWithData(sysexBuffer.data(), sysexBuffer.size()); + 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) @@ -210,6 +226,15 @@ namespace pluginLib::patchDB }); } + 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); @@ -335,6 +360,9 @@ namespace pluginLib::patchDB 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); @@ -364,12 +392,12 @@ namespace pluginLib::patchDB return nullptr; } - void DB::copyPatchesTo(const DataSourceNodePtr& _ds, const std::vector<PatchPtr>& _patches, int _insertRow/* = -1*/) + 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] + runOnLoaderThread([this, _ds, _patches, _insertRow, _successCallback] { { std::shared_lock lockDs(m_dataSourcesMutex); @@ -416,6 +444,14 @@ namespace pluginLib::patchDB createConsecutiveProgramNumbers(_ds); + if(_successCallback) + { + runOnUiThread([_successCallback, newPatches] + { + _successCallback(newPatches); + }); + } + saveJson(); }); } @@ -540,8 +576,6 @@ namespace pluginLib::patchDB if(patch->source.expired()) continue; - const auto key = PatchKey(*patch); - auto mods = patch->modifications; if(!mods) @@ -729,7 +763,8 @@ namespace pluginLib::patchDB runOnLoaderThread([this] { - loadJson(); + if(!g_cacheEnabled || !loadCache()) + loadJson(); }); } @@ -1127,7 +1162,15 @@ namespace pluginLib::patchDB } 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); @@ -1174,7 +1217,7 @@ namespace pluginLib::patchDB return res; } - bool DB::createConsecutiveProgramNumbers(const DataSourceNodePtr& _ds) + bool DB::createConsecutiveProgramNumbers(const DataSourceNodePtr& _ds) const { std::unique_lock lockPatches(m_patchesMutex); return _ds->createConsecutiveProgramNumbers(); @@ -1266,7 +1309,7 @@ namespace pluginLib::patchDB if(colors) { std::unordered_map<Tag, Color> newTags; - for (auto itCol : colors->getProperties()) + for (const auto& itCol : colors->getProperties()) { const auto tag = itCol.name.toString().toStdString(); const auto col = static_cast<juce::int64>(itCol.value); @@ -1377,6 +1420,8 @@ namespace pluginLib::patchDB bool DB::saveJson() { + m_cacheDirty = true; + if (!m_jsonFileName.hasWriteAccess()) { pushError("No write access to file:\n" + m_jsonFileName.getFullPathName().toStdString()); @@ -1585,7 +1630,7 @@ namespace pluginLib::patchDB return m_settingsDir.getChildFile(filename + ".syx"); } - bool DB::saveLocalStorage() const + bool DB::saveLocalStorage() { std::map<DataSourceNodePtr, std::set<PatchPtr>> localStoragePatches; @@ -1632,4 +1677,335 @@ namespace pluginLib::patchDB 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 @@ -32,6 +32,7 @@ namespace pluginLib::patchDB 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) { @@ -61,7 +62,8 @@ namespace pluginLib::patchDB std::shared_ptr<Search> getSearch(SearchHandle _handle); std::shared_ptr<Search> getSearch(const DataSource& _dataSource); - void copyPatchesTo(const DataSourceNodePtr& _ds, const std::vector<PatchPtr>& _patches, int _insertRow = -1); + void 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); @@ -73,7 +75,7 @@ namespace pluginLib::patchDB bool isLoading() const { return m_loading; } bool isScanning() const { return !m_loader.empty(); } - static bool writePatchesToFile(const juce::File& _file, const std::vector<PatchPtr>& _patches); + bool writePatchesToFile(const juce::File& _file, const std::vector<PatchPtr>& _patches); protected: DataSourceNodePtr addDataSource(const DataSource& _ds, bool _save); @@ -113,7 +115,7 @@ namespace pluginLib::patchDB void updateSearches(const std::vector<PatchPtr>& _patches); bool removePatchesFromSearches(const std::vector<PatchPtr>& _keys); - bool createConsecutiveProgramNumbers(const DataSourceNodePtr& _ds); + bool createConsecutiveProgramNumbers(const DataSourceNodePtr& _ds) const; Color getTagColorInternal(TagType _type, const Tag& _tag) const; @@ -128,13 +130,17 @@ namespace pluginLib::patchDB juce::File getJsonFile(const DataSource& _ds) const; juce::File getLocalStorageFile(const DataSource& _ds) const; - bool saveLocalStorage() 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; @@ -161,5 +167,6 @@ namespace pluginLib::patchDB // state bool m_loading = true; + bool m_cacheDirty = false; }; } diff --git a/source/jucePluginLib/patchdb/patch.cpp b/source/jucePluginLib/patchdb/patch.cpp @@ -8,6 +8,8 @@ #include "juce_core/juce_core.h" +#include "../../synthLib/binarystream.h" + namespace pluginLib::patchDB { std::pair<PatchPtr, PatchModificationsPtr> Patch::createCopy(const DataSourceNodePtr& _ds) const @@ -43,6 +45,60 @@ namespace pluginLib::patchDB 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) diff --git a/source/jucePluginLib/patchdb/patch.h b/source/jucePluginLib/patchdb/patch.h @@ -9,6 +9,11 @@ #include "tags.h" #include "patchdbtypes.h" +namespace synthLib +{ + class BinaryStream; +} + namespace pluginLib::patchDB { using PatchHash = std::array<uint8_t, 16>; @@ -28,6 +33,15 @@ namespace pluginLib::patchDB 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; diff --git a/source/jucePluginLib/patchdb/patchdbtypes.h b/source/jucePluginLib/patchdb/patchdbtypes.h @@ -79,4 +79,19 @@ namespace pluginLib::patchDB 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/patchmodifications.cpp b/source/jucePluginLib/patchdb/patchmodifications.cpp @@ -2,7 +2,9 @@ #include "patch.h" -#include <juce_audio_processors/juce_audio_processors.h> +#include "juce_core/juce_core.h" + +#include "../../synthLib/binarystream.h" namespace pluginLib::patchDB { @@ -106,4 +108,25 @@ namespace pluginLib::patchDB { 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 @@ -20,6 +20,9 @@ namespace pluginLib::patchDB bool empty() const; + void write(synthLib::BinaryStream& _outStream) const; + bool read(synthLib::BinaryStream& _binaryStream); + std::weak_ptr<Patch> patch; TypedTags tags; std::string name; diff --git a/source/jucePluginLib/patchdb/tags.cpp b/source/jucePluginLib/patchdb/tags.cpp @@ -2,8 +2,40 @@ #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()) @@ -11,13 +43,13 @@ namespace pluginLib::patchDB if(m_removed.size() != _t.m_removed.size()) return false; - for (auto e : m_added) + for (const auto& e : m_added) { if(_t.m_added.find(e) == _t.m_added.end()) return false; } - for (auto e : m_removed) + for (const auto& e : m_removed) { if(_t.m_removed.find(e) == _t.m_removed.end()) return false; @@ -210,4 +242,40 @@ namespace pluginLib::patchDB } 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 @@ -6,6 +6,11 @@ #include "patchdbtypes.h" +namespace synthLib +{ + class BinaryStream; +} + namespace juce { class DynamicObject; @@ -67,6 +72,9 @@ namespace pluginLib::patchDB 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: @@ -90,6 +98,10 @@ namespace pluginLib::patchDB 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 @@ -5,6 +5,8 @@ #include "../synthLib/deviceException.h" #include "../synthLib/os.h" #include "../synthLib/binarystream.h" +#include "../synthLib/midiBufferParser.h" + #include "dsp56kEmu/fastmath.h" #include "dsp56kEmu/logging.h" @@ -17,9 +19,9 @@ namespace synthLib namespace pluginLib { constexpr char g_saveMagic[] = "DSP56300"; - constexpr uint32_t g_saveVersion = 1; + 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) @@ -190,10 +207,30 @@ 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 @@ -207,44 +244,69 @@ namespace pluginLib s.write(m_dspClockPercent); } - s.toVector(_targetBuffer, true); + if(m_preferredDeviceSamplerate > 0) + { + synthLib::ChunkWriter cw(s, "DSSR", 1); + s.write(m_preferredDeviceSamplerate); + } } - void Processor::loadCustomData(const std::vector<uint8_t>& _sourceBuffer) + bool Processor::loadCustomData(const std::vector<uint8_t>& _sourceBuffer) { - auto readGain = [this](synthLib::BinaryStream& _s) - { - const auto version = _s.read<uint32_t>(); - if (version != 1) - return; - m_inputGain = _s.read<float>(); - m_outputGain = _s.read<float>(); - }; + 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; + return true; } synthLib::BinaryStream s(_sourceBuffer); synthLib::ChunkReader cr(s); - cr.add("GAIN", 1, [readGain](synthLib::BinaryStream& _binaryStream, uint32_t _version) + 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) + _cr.add("DSPC", 1, [this](synthLib::BinaryStream& _binaryStream, uint32_t _version) { auto p = _binaryStream.read<uint32_t>(); p = dsp56k::clamp<uint32_t>(p, 50, 200); setDspClockPercent(p); }); - cr.tryRead(); + _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) @@ -271,12 +333,43 @@ namespace pluginLib 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(); @@ -288,6 +381,22 @@ 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) { @@ -295,14 +404,10 @@ namespace pluginLib // You could do that either as raw data, or use the XML or ValueTree classes // as intermediaries to make it easy to save and load complex data. #if !SYNTHLIB_DEMO_MODE - std::vector<uint8_t> buffer; - getPlugin().getState(buffer, synthLib::StateTypeGlobal); - 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); @@ -337,6 +442,165 @@ namespace pluginLib 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) @@ -361,12 +625,17 @@ 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); + + if(version == 1) + { + ss.read(buffer); + getPlugin().setState(buffer); + } + ss.read(buffer); if(!buffer.empty()) 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,10 +55,14 @@ 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 loadCustomData(const std::vector<uint8_t>& _sourceBuffer); + 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) { @@ -79,15 +94,32 @@ namespace pluginLib 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; } + + 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); @@ -118,8 +150,11 @@ namespace pluginLib 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/juceUiLib/CMakeLists.txt b/source/juceUiLib/CMakeLists.txt @@ -30,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/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 @@ -242,11 +242,16 @@ 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); diff --git a/source/juceUiLib/editor.h b/source/juceUiLib/editor.h @@ -2,7 +2,7 @@ #include <string> -#include <juce_audio_processors/juce_audio_processors.h> +#include "juce_gui_basics/juce_gui_basics.h" #include "button.h" #include "uiObject.h" @@ -80,6 +80,7 @@ namespace genericUI static void setEnabled(juce::Component& _component, bool _enable); virtual void setCurrentPart(uint8_t _part); + void updateKeyValueConditions(const std::string& _key, const std::string& _value) const; juce::TooltipWindow& getTooltipWindow() { return m_tooltipWindow; } 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/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/uiObject.cpp b/source/juceUiLib/uiObject.cpp @@ -1,7 +1,5 @@ #include "uiObject.h" -#include <juce_audio_processors/juce_audio_processors.h> - #include <utility> #include "editor.h" @@ -69,18 +67,18 @@ 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); } @@ -95,7 +93,7 @@ namespace genericUI ch->registerTemplates(_editor); } - void UiObject::apply(Editor& _editor, juce::Component& _target) + void UiObject::apply(const Editor& _editor, juce::Component& _target) { const auto x = getPropertyInt("x"); const auto y = getPropertyInt("y"); @@ -325,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) { @@ -366,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 v = _editor.getInterface().getParameterValue(index, 0); + const auto paramName = getProperty("enableOnParameter"); + + if(!paramName.empty()) + { + const auto paramIndex = _editor.getInterface().getParameterIndexByName(paramName); + + if(paramIndex < 0) + throw std::runtime_error("Parameter named " + paramName + " not found"); - if(!v) - throw std::runtime_error("Parameter named " + paramName + " not found"); + const auto v = _editor.getInterface().getParameterValue(paramIndex, 0); - m_condition.reset(new Condition(_target, v, static_cast<uint32_t>(index), values)); + 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(key.empty()) + throw std::runtime_error("Unknown condition type, neither 'enableOnParameter' nor 'enableOnKey' specified"); + + m_condition.reset(new ConditionByKeyValue(_target, std::move(key), std::move(values))); + } } bool UiObject::parse(juce::DynamicObject* _obj) diff --git a/source/juceUiLib/uiObject.h b/source/juceUiLib/uiObject.h @@ -39,10 +39,10 @@ namespace genericUI 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); @@ -73,11 +73,13 @@ namespace genericUI 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, 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); diff --git a/source/juceUiLib/uiObjectStyle.cpp b/source/juceUiLib/uiObjectStyle.cpp @@ -42,22 +42,7 @@ namespace genericUI auto parseColor = [&_object](juce::Colour& _target, const std::string& _prop) { const auto color = _object.getProperty(_prop); - - uint32_t r,g,b,a; - - if(color.size() == 8) - { - sscanf(color.c_str(), "%02x%02x%02x%02x", &r, &g, &b, &a); - } - else if(color.size() == 6) - { - sscanf(color.c_str(), "%02x%02x%02x", &r, &g, &b); - a = 255; - } - else - return; - - _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"); @@ -111,11 +96,33 @@ namespace genericUI if (m_fontFile.empty()) return {}; - auto font = juce::Font(m_editor.getFont(m_fontFile).getTypeface()); + 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) + { + 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()) diff --git a/source/juceUiLib/uiObjectStyle.h b/source/juceUiLib/uiObjectStyle.h @@ -1,6 +1,6 @@ #pragma once -#include <juce_audio_processors/juce_audio_processors.h> +#include "juce_gui_basics/juce_gui_basics.h" #include <optional> @@ -26,6 +26,8 @@ namespace genericUI 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; 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/synthLib/CMakeLists.txt b/source/synthLib/CMakeLists.txt @@ -10,13 +10,15 @@ 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 @@ -38,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/audiobuffer.h b/source/synthLib/audiobuffer.h @@ -1,4 +1,5 @@ #pragma once +#include <cstddef> #include <vector> #include "audioTypes.h" 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,24 +1,156 @@ #pragma once +#include <cassert> #include <cstdint> #include <functional> -#include <iosfwd> #include <sstream> #include <vector> #include <cstring> namespace synthLib { - class BinaryStream final : std::stringstream + class StreamBuffer { public: - using SizeType = uint32_t; + 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); } @@ -26,7 +158,7 @@ namespace synthLib // tools // - void toVector(std::vector<uint8_t>& _buffer, bool _append = false) + void toVector(std::vector<uint8_t>& _buffer, const bool _append = false) { const auto size = tellp(); if(size <= 0) @@ -42,12 +174,12 @@ namespace synthLib { const auto currentSize = _buffer.size(); _buffer.resize(currentSize + size); - std::stringstream::read(reinterpret_cast<char*>(&_buffer[currentSize]), size); + Base::read(&_buffer[currentSize], size); } else { _buffer.resize(size); - std::stringstream::read(reinterpret_cast<char*>(_buffer.data()), size); + Base::read(_buffer.data(), size); } } @@ -63,46 +195,18 @@ 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 (uint32_t)const_cast<BinaryStream&>(*this).tellp(); - } - - uint32_t getReadPos() const - { - return (uint32_t)const_cast<BinaryStream&>(*this).tellg(); - } - - void setWritePos(const uint32_t _pos) - { - seekp(_pos); - } - - void setReadPos(const uint32_t _pos) - { - seekg(_pos); - } - - bool endOfStream() const - { - return eof() || (getReadPos() == size()); - } + 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(); } - SizeType size() const - { - const auto readPos = getReadPos(); - auto& s = const_cast<BinaryStream&>(*this); - s.seekg(0, std::ios_base::end); - const auto size = s.tellg(); - s.seekg(readPos); - return (SizeType)size; - } + void setWritePos(const uint32_t _pos) { seekp(_pos); } + void setReadPos(const uint32_t _pos) { seekg(_pos); } // ___________________________________ // write @@ -110,7 +214,7 @@ namespace synthLib 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) @@ -118,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) @@ -150,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; } @@ -165,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(); } @@ -174,7 +278,7 @@ 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; } @@ -203,6 +307,17 @@ namespace synthLib _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 // @@ -215,6 +330,25 @@ namespace synthLib } }; + 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: @@ -229,6 +363,12 @@ namespace synthLib 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(); @@ -249,71 +389,59 @@ namespace synthLib using SizeType = ChunkWriter::SizeType; using ChunkCallback = std::function<void(BinaryStream&, uint32_t)>; // data, version - struct Chunk + struct ChunkCallbackData { - char fourcc[5]; + char fourCC[5]; uint32_t expectedVersion; ChunkCallback callback; }; - ChunkReader(BinaryStream& _stream) : m_stream(_stream) + 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) { - Chunk c; - strcpy(c.fourcc, _4Cc); + ChunkCallbackData c; + strcpy(c.fourCC, _4Cc); c.expectedVersion = _version; c.callback = _callback; supportedChunks.emplace_back(std::move(c)); } - void read() + void read(const uint32_t _count = 0) { - while(!m_stream.endOfStream()) - { - char fourCC[5]; - m_stream.read4CC(fourCC); - const auto version = m_stream.read<uint32_t>(); - const auto length = m_stream.read<SizeType>(); + uint32_t count = 0; - bool hasReadChunk = false; + while(!m_stream.endOfStream() && (!_count || ++count <= _count)) + { + Chunk chunk; + chunk.read(m_stream); ++m_numChunks; - for (const auto& chunk : supportedChunks) + for (const auto& chunkData : supportedChunks) { - if(0 != strcmp(chunk.fourcc, fourCC)) + if(0 != strcmp(chunkData.fourCC, chunk.fourCC)) continue; - if(version > chunk.expectedVersion) + if(chunk.version > chunkData.expectedVersion) break; - std::vector<uint8_t> chunkData; - chunkData.reserve(length); - for(size_t i=0; i<length; ++i) - chunkData.push_back(m_stream.read<uint8_t>()); - - hasReadChunk = true; ++m_numRead; - BinaryStream s(chunkData); - chunk.callback(s, version); + chunkData.callback(chunk.data, chunk.version); break; } - - if(!hasReadChunk) - m_stream.setReadPos(m_stream.getReadPos() + length); } } - bool tryRead() + bool tryRead(const uint32_t _count = 0) { const auto pos = m_stream.getReadPos(); try { - read(); + read(_count); return true; } catch(std::range_error&) @@ -335,7 +463,7 @@ namespace synthLib private: BinaryStream& m_stream; - std::vector<Chunk> supportedChunks; + std::vector<ChunkCallbackData> supportedChunks; uint32_t m_numRead = 0; uint32_t m_numChunks = 0; }; 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 @@ -24,7 +24,23 @@ 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 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/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/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 @@ -159,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) @@ -190,9 +199,25 @@ 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 = fopen(_file.c_str(), "rb"); + FILE* hFile = openFile(_file, "rb"); if (!hFile) return 0; @@ -310,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); @@ -325,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; @@ -343,8 +368,26 @@ namespace synthLib 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,19 @@ 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); @@ -36,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(); } @@ -84,6 +110,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 { @@ -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 @@ -21,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; @@ -32,6 +36,9 @@ 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); @@ -58,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; @@ -75,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/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,135 +166,41 @@ std::string ConsoleApp::getSingleNameAsFilename() const return "virusEmu_" + audioFilename + ".wav"; } -void ConsoleApp::audioCallback(uint32_t audioCallbackCount) +void ConsoleApp::audioCallback(const uint32_t _audioCallbackCount) { - uc->process(); + 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)); @@ -311,7 +212,7 @@ void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampl 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*) @@ -337,17 +238,18 @@ void ConsoleApp::run(const std::string& _audioOutputFilename, uint32_t _maxSampl 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; @@ -357,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/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 @@ -9,43 +9,47 @@ #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(); @@ -56,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 @@ -73,9 +101,21 @@ 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 @@ -100,10 +140,13 @@ namespace virusLib 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; @@ -269,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) @@ -286,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; @@ -340,7 +383,7 @@ namespace virusLib 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(); @@ -354,31 +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->getPeriphX().getEsaiClock().setSpeedPercent(_percent); + bool res = m_dsp->getEsxiClock().setSpeedPercent(_percent); if(m_dsp2) - res &= m_dsp2->getPeriphX().getEsaiClock().setSpeedPercent(_percent); + res &= m_dsp2->getEsxiClock().setSpeedPercent(_percent); return res; } uint32_t Device::getDspClockPercent() const { - return !m_dsp ? 0 : m_dsp->getPeriphX().getEsaiClock().getSpeedPercent(); + return !m_dsp ? 0 : m_dsp->getEsxiClock().getSpeedPercent(); } uint64_t Device::getDspClockHz() const { - return !m_dsp ? 0 : m_dsp->getPeriphX().getEsaiClock().getSpeedInHz(); + 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,21 +1,31 @@ #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; @@ -35,26 +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 @@ -13,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; } @@ -32,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) { 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/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) { @@ -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; }; diff --git a/source/virusLib/microcontroller.h b/source/virusLib/microcontroller.h @@ -16,6 +16,7 @@ namespace virusLib { + struct FrontpanelState; class DspSingle; class DemoPlayback; @@ -27,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); @@ -61,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 @@ -17,11 +17,11 @@ namespace virusLib return load(sysex); } - bool MidiFileToRomData::load(const std::vector<uint8_t>& _fileData) + bool MidiFileToRomData::load(const std::vector<uint8_t>& _fileData, bool _isMidiFileData/* = false*/) { std::vector<std::vector<uint8_t>> packets; - synthLib::MidiToSysex::splitMultipleSysex(packets, _fileData); + synthLib::MidiToSysex::splitMultipleSysex(packets, _fileData, _isMidiFileData); return add(packets); } diff --git a/source/virusLib/midiFileToRomData.h b/source/virusLib/midiFileToRomData.h @@ -16,7 +16,7 @@ namespace virusLib } bool load(const std::string& _filename); - bool load(const std::vector<uint8_t>& _fileData); + 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,115 +9,24 @@ #include "../synthLib/os.h" -#include "midiFileToRomData.h" - #include <cstring> // memcpy #include "dsp56kEmu/memory.h" -#ifdef _WIN32 -#define NOMINMAX -#include <Windows.h> -#endif - 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(); -} - -std::string ROMFile::findROM() -{ - return synthLib::findROM(getRomSizeModelABC()); + if(initialize()) + return; + m_romFileData.clear(); + bootRom.size = 0; } -bool ROMFile::loadROMData(std::string& _loadedFile, std::vector<uint8_t>& _loadedData, const size_t _expectedSizeMin, const size_t _expectedSizeMax) +ROMFile ROMFile::invalid() { - // 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() @@ -167,7 +75,7 @@ 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 { @@ -191,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; @@ -213,7 +125,7 @@ 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++) @@ -228,7 +140,7 @@ std::thread ROMFile::bootDSP(dsp56k::DSP& dsp, dsp56k::Peripherals56362& periph) // Attach command stream std::thread feedCommandStream([&]() { - periph.getHDI08().writeRX(m_commandStream); + _hdi08.writeRX(m_commandStream); }); // Initialize the DSP @@ -236,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/temp/cmake_win64/gearmulator.sln.DotSettings b/temp/cmake_win64/gearmulator.sln.DotSettings @@ -5,6 +5,7 @@ <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>