diff --git a/.vscode/launch.json b/.vscode/launch.json index de4a777..c6e4187 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,8 @@ // Resolved by CMake Tools: "program": "${command:cmake.launchTargetPath}", "args": [ - "[ProfileThumbnails]" + "hw:M2", + "hw:M2" ], "stopAtEntry": false, "cwd": "${workspaceFolder}", diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f9ecbd..f4448b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,13 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.5.92 + VERSION 1.5.93 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) execute_process( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE ) -set (DISPLAY_VERSION "PiPedal v1.5.92-Experimental") +set (DISPLAY_VERSION "PiPedal v1.5.93-Experimental") set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (CMAKE_INSTALL_PREFIX "/usr/") diff --git a/README.md b/README.md index e8f35fc..b2416e1 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ -Download: v1.5.92 +Download: v1.5.93 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html). -#### NEW version 1.5.92 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE! +#### NEW version 1.5.93 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE!   diff --git a/build-prod.sh b/build-prod.sh index 5758e83..0cf9ae4 100755 --- a/build-prod.sh +++ b/build-prod.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Run a CMake build. +# Run a Clean Production CMake build. set -e @@ -14,3 +14,5 @@ cd .. time cmake --build ./build --target all --config Release -- -j 3 +./makePackage.sh + diff --git a/default_presets/presets/Default+Bank.bank b/default_presets/presets/Default+Bank.bank index 496d9a4..fe47723 100644 --- a/default_presets/presets/Default+Bank.bank +++ b/default_presets/presets/Default+Bank.bank @@ -1 +1 @@ -{"name": "","nextInstanceId": 12,"selectedPreset": 7,"presets": [{"instanceId": 7,"preset": {"name": "Heavy","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -23.1093884},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 49,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -60},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 100},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-delay","isEnabled": false,"controlValues": [{"key": "delay","value": 340},{"key": "level","value": 27},{"key": "feedback","value": 39.1666679}],"pluginName": "TooB Delay","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 1},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "model","value": 18},{"key": "gain","value": 2.41666675},{"key": "master","value": -4.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 2,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/HT40_Overdrive.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/HT40_Overdrive.json\"}"},"title": "","useModUi": false},{"instanceId": 31,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "dryWet","value": 0.25},{"key": "roomSize","value": 0.733333349},{"key": "damping","value": 0.574999988},{"key": "bypass","value": 1},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 51,"uri": "http://two-play.com/plugins/toob-parametric-eq","isEnabled": true,"controlValues": [{"key": "loCut","value": 50.7010994},{"key": "hiCut","value": 21},{"key": "lfLevel","value": 0},{"key": "lfC","value": 120},{"key": "lmfLevel","value": 0},{"key": "lmfC","value": 400},{"key": "lmfQ","value": 1.5},{"key": "hmfLevel","value": 0},{"key": "hmfC","value": 2},{"key": "hmfQ","value": 1.5},{"key": "hfLevel","value": -3.25},{"key": "hfC","value": 3.875},{"key": "gain","value": 0}],"pluginName": "TooB Parametric EQ","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false}],"nextInstanceId": 52,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 41}},{"instanceId": 8,"preset": {"name": "Sparkling Clean","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -23.1093884},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 55,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -65},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 100},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 53,"uri": "http://two-play.com/plugins/toob-phaser","isEnabled": true,"controlValues": [{"key": "rate","value": 0.424131542},{"key": "dryWet","value": 0.400000006}],"pluginName": "TooB Phaser","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-delay","isEnabled": false,"controlValues": [{"key": "delay","value": 340},{"key": "level","value": 36.6666679},{"key": "feedback","value": 33.1666679}],"pluginName": "TooB Delay","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -2.5},{"key": "bass","value": 0.5},{"key": "mid","value": 0.625},{"key": "treble","value": 0.5},{"key": "model","value": 19},{"key": "gain","value": 0},{"key": "master","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/MesaBoogie_Studio22_Clean.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ToobMlModels/MesaBoogie_Studio22_Clean.json\"}"},"title": "","useModUi": false},{"instanceId": 51,"uri": "http://two-play.com/plugins/toob-convolution-reverb-stereo","isEnabled": true,"controlValues": [{"key": "bypass","value": 1},{"key": "time","value": 30},{"key": "direct_mix","value": -6},{"key": "reverb_mix","value": -4},{"key": "width","value": 1},{"key": "pan","value": 0},{"key": "predelay","value": -1},{"key": "predelay_new","value": 1},{"key": "stretch","value": 1},{"key": "decay","value": 0},{"key": "tails","value": 1},{"key": "loading_state","value": 0}],"pluginName": "TooB Convolution Reverb (Stereo)","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-impulse#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ReverbImpulseFiles/Genesis 6 Studio Live Room.wav"}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-impulse#impulseFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ReverbImpulseFiles/Genesis 6 Studio Live Room.wav\"}"},"title": "","useModUi": false}],"nextInstanceId": 55,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 12,"preset": {"name": "Clean-Lead Split","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -54},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 66,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -96},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 31.622776},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 54,"uri": "uri://two-play/pipedal/pedalboard#Split","isEnabled": true,"controlValues": [{"key": "splitType","value": 0},{"key": "select","value": 1},{"key": "mix","value": 0},{"key": "panL","value": 0},{"key": "volL","value": -3},{"key": "panR","value": 0},{"key": "volR","value": -3}],"pluginName": "","topChain": [{"instanceId": 72,"uri": "http://two-play.com/plugins/toob-chorus","isEnabled": true,"controlValues": [{"key": "rate","value": 0.5},{"key": "depth","value": 0.5},{"key": "dryWet","value": 0.575000048}],"pluginName": "TooB CE-2 Chorus","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 58,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 0},{"key": "trimOut","value": -35},{"key": "model","value": -1},{"key": "gain","value": 2.25},{"key": "master","value": 0},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13},{"key": "gainEnable","value": 0}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/BluesJR.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/BluesJR.json\"}"},"title": "Blues Jr","useModUi": false},{"instanceId": 64,"uri": "http://two-play.com/plugins/toob-cab-ir","isEnabled": true,"controlValues": [{"key": "reverb_mix","value": -6},{"key": "reverb_mix2","value": 0},{"key": "reverb_mix3","value": 0},{"key": "time","value": 1.5},{"key": "direct_mix","value": -40},{"key": "predelay","value": 0},{"key": "loading_state","value": 0}],"pluginName": "TooB Cab IR","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-cab-ir#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/Modern US 001.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile2": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/Modern US 002.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile3": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#String","value": ""}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-cab-ir#impulseFile": "{\"otype_\": \"Path\",\"value\": \"CabIR/Modern US 001.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile2": "{\"otype_\": \"Path\",\"value\": \"CabIR/Modern US 002.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile3": "{\"otype_\": \"Path\",\"value\": \"\"}"},"title": "","useModUi": false},{"instanceId": 87,"uri": "http://two-play.com/plugins/toob-volume","isEnabled": true,"controlValues": [{"key": "vol","value": -2.25}],"pluginName": "TooB Volume","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 69,"uri": "http://two-play.com/plugins/toob-convolution-reverb-stereo","isEnabled": true,"controlValues": [{"key": "bypass","value": 1},{"key": "time","value": 30},{"key": "direct_mix","value": -0.666666687},{"key": "reverb_mix","value": -1.39999998},{"key": "width","value": 1},{"key": "pan","value": 0},{"key": "predelay","value": -1},{"key": "predelay_new","value": 1},{"key": "stretch","value": 1.27456057},{"key": "decay","value": 0},{"key": "tails","value": 1},{"key": "loading_state","value": 0}],"pluginName": "TooB Convolution Reverb (Stereo)","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{"http://two-play.com/plugins/toob-impulse#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ReverbImpulseFiles/St. Margaret's Church.wav"}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-impulse#impulseFile": "{\"otype_\": \"Path\",\"value\": \"ReverbImpulseFiles/St. Margaret's Church.wav\"}"},"title": "","useModUi": false}],"bottomChain": [{"instanceId": 81,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -10.5},{"key": "trimOut","value": -35},{"key": "model","value": -1},{"key": "gain","value": 8.83333302},{"key": "master","value": 0},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13},{"key": "gainEnable","value": 0}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 2,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/Proteus_Tone_Packs/AmpPack1/BlackstarHT40_AmpHighGain.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/Proteus_Tone_Packs/AmpPack1/BlackstarHT40_AmpHighGain.json\"}"},"title": "HT40","useModUi": false},{"instanceId": 62,"uri": "http://two-play.com/plugins/toob-cab-ir","isEnabled": true,"controlValues": [{"key": "reverb_mix","value": -18.5},{"key": "reverb_mix2","value": -5.5},{"key": "reverb_mix3","value": 0},{"key": "time","value": 1.5},{"key": "direct_mix","value": -13.5},{"key": "predelay","value": 0},{"key": "loading_state","value": 0}],"pluginName": "TooB Cab IR","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-cab-ir#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/Modern US 002.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile2": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/Modern US 004.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile3": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#String","value": ""}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-cab-ir#impulseFile": "{\"otype_\": \"Path\",\"value\": \"CabIR/Modern US 002.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile2": "{\"otype_\": \"Path\",\"value\": \"CabIR/Modern US 004.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile3": "{\"otype_\": \"Path\",\"value\": \"\"}"},"title": "","useModUi": false},{"instanceId": 85,"uri": "http://two-play.com/plugins/toob-volume","isEnabled": true,"controlValues": [{"key": "vol","value": 3.75}],"pluginName": "TooB Volume","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 83,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "bypass","value": 1},{"key": "dryWet","value": 0.383333325},{"key": "roomSize","value": 0.683333337},{"key": "damping","value": 0},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false}],"midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false}],"nextInstanceId": 88,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 81}},{"instanceId": 9,"preset": {"name": "Screaming Lead","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -54},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 17,"uri": "http://two-play.com/plugins/toob-input_stage","isEnabled": true,"controlValues": [{"key": "trim","value": -15.75},{"key": "locut","value": 196.5},{"key": "bright","value": 0},{"key": "brightf","value": 1300},{"key": "hicut","value": 13000},{"key": "gate_t","value": -66.5},{"key": "boost","value": 0}],"pluginName": "TooB Input Stage","midiBindings": [{"channel": -1,"symbol": "__bypass","bindingType": 2,"note": 72,"control": 4,"minControlValue": 0,"maxControlValue": 127,"minValue": 0,"maxValue": 1,"rotaryScale": 1,"linearControlType": 0,"switchControlType": 0}],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-delay","isEnabled": true,"controlValues": [{"key": "delay","value": 340},{"key": "level","value": 18.666666},{"key": "feedback","value": 13.3333349}],"pluginName": "TooB Delay","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 42,"uri": "http://two-play.com/plugins/toob-chorus","isEnabled": false,"controlValues": [{"key": "rate","value": 0.291666657},{"key": "depth","value": 0.375},{"key": "dryWet","value": 1}],"pluginName": "TooB CE-2 Chorus","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 47,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 12.5},{"key": "model","value": 29},{"key": "gain","value": 3},{"key": "master","value": -0.5},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/TS9_FullD.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ToobMlModels/TS9_FullD.json\"}"},"title": "","useModUi": false},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -4},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "model","value": 12},{"key": "gain","value": 10},{"key": "master","value": -5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/HT40_Overdrive.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ToobMlModels/HT40_Overdrive.json\"}"},"title": "","useModUi": false},{"instanceId": 51,"uri": "http://two-play.com/plugins/toob-flanger-stereo","isEnabled": true,"controlValues": [{"key": "manual","value": 0.5},{"key": "depth","value": 0.5},{"key": "rate","value": 0.5},{"key": "lfo","value": 0},{"key": "res","value": 0.5},{"key": "dryWet","value": 0.508333325}],"pluginName": "TooB BF-2 Stereo Flanger","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 53,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "bypass","value": 1},{"key": "dryWet","value": 0.150000006},{"key": "roomSize","value": 0.649999976},{"key": "damping","value": 0},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false}],"nextInstanceId": 53,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 10,"preset": {"name": "Light Crunch","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -23.1093884},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 49,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -102},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 100},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 42,"uri": "http://two-play.com/plugins/toob-chorus","isEnabled": false,"controlValues": [{"key": "rate","value": 0.291666657},{"key": "depth","value": 0.375},{"key": "dryWet","value": 1}],"pluginName": "TooB CE-2 Chorus","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-delay","isEnabled": true,"controlValues": [{"key": "delay","value": 340},{"key": "level","value": 27},{"key": "feedback","value": 39}],"pluginName": "TooB Delay","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -0.5},{"key": "bass","value": 0.650000036},{"key": "mid","value": 0.5},{"key": "treble","value": 0.266666681},{"key": "model","value": 17},{"key": "gain","value": 7.75},{"key": "master","value": -4},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/ProCoRat.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ToobMlModels/ProCoRat.json\"}"},"title": "","useModUi": false},{"instanceId": 51,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -3},{"key": "trimOut","value": -35},{"key": "model","value": -1},{"key": "gain","value": 3},{"key": "master","value": -6},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13},{"key": "gainEnable","value": 0}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/FenderPrinceton_highLevel.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ToobMlModels/FenderPrinceton_highLevel.json\"}"},"title": "","useModUi": false},{"instanceId": 58,"uri": "http://two-play.com/plugins/toob-nam","isEnabled": true,"controlValues": [{"key": "inputGain","value": -12.666667},{"key": "inputGainOut","value": -35},{"key": "outputGain","value": 0},{"key": "gate","value": -120},{"key": "gateOut","value": 0},{"key": "buffer","value": 0},{"key": "toneStack","value": 3},{"key": "bass","value": 5},{"key": "mid","value": 5},{"key": "treble","value": 5},{"key": "inputCalibrationMode","value": 1},{"key": "calibration","value": -6},{"key": "outputCalibration","value": 0},{"key": "version","value": 1}],"pluginName": "TooB Neural Amp Modeler","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-nam#modelFile": "{\"otype_\": \"Path\",\"value\": \"\"}"},"title": "","useModUi": false},{"instanceId": 53,"uri": "http://two-play.com/plugins/toob-parametric-eq","isEnabled": true,"controlValues": [{"key": "loCut","value": 27.2704601},{"key": "hiCut","value": 21},{"key": "lfLevel","value": 0},{"key": "lfC","value": 120},{"key": "lmfLevel","value": -6.5},{"key": "lmfC","value": 551.471313},{"key": "lmfQ","value": 0.850504637},{"key": "hmfLevel","value": -7.5},{"key": "hmfC","value": 2.5746665},{"key": "hmfQ","value": 1.5},{"key": "hfLevel","value": -6.5},{"key": "hfC","value": 2.25},{"key": "gain","value": 0}],"pluginName": "TooB Parametric EQ","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 31,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "dryWet","value": 0.416666687},{"key": "roomSize","value": 0.625},{"key": "damping","value": 0.574999988},{"key": "bypass","value": 1},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false}],"nextInstanceId": 58,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 11,"preset": {"name": "Jazz","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -23.1093884},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 45,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -53},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 100},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-chorus","isEnabled": false,"controlValues": [{"key": "rate","value": 0.208333328},{"key": "depth","value": 0.5},{"key": "dryWet","value": 1}],"pluginName": "TooB CE-2 Chorus","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 47,"uri": "http://two-play.com/plugins/toob-parametric-eq","isEnabled": true,"controlValues": [{"key": "loCut","value": 41.8936462},{"key": "hiCut","value": 21},{"key": "lfLevel","value": 0},{"key": "lfC","value": 120},{"key": "lmfLevel","value": -5.5},{"key": "lmfC","value": 238.107834},{"key": "lmfQ","value": 0.837899804},{"key": "hmfLevel","value": 0},{"key": "hmfC","value": 2},{"key": "hmfQ","value": 1.5},{"key": "hfLevel","value": -12},{"key": "hfC","value": 1},{"key": "gain","value": 5.83333349}],"pluginName": "TooB Parametric EQ","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -13},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "model","value": 0},{"key": "gain","value": 4.66666651},{"key": "master","value": 5},{"key": "sag","value": 0.550000012},{"key": "sagd","value": 0.900000036},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 2,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/FenderPrinceton_clean.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/FenderPrinceton_clean.json\"}"},"title": "","useModUi": false},{"instanceId": 31,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "dryWet","value": 0.416666657},{"key": "roomSize","value": 0.625},{"key": "damping","value": 0.574999988},{"key": "bypass","value": 1},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false}],"nextInstanceId": 48,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}}]} \ No newline at end of file +{"name": "","nextInstanceId": 13,"selectedPreset": 7,"presets": [{"instanceId": 7,"preset": {"name": "Heavy","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -23.1093884},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 49,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -60},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 100},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-delay","isEnabled": false,"controlValues": [{"key": "delay","value": 340},{"key": "level","value": 27},{"key": "feedback","value": 39.1666679}],"pluginName": "TooB Delay","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 1},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "model","value": 18},{"key": "gain","value": 2.41666675},{"key": "master","value": -4.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 2,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/BossDistortionDs1_highGain.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/BossDistortionDs1_highGain.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 31,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "dryWet","value": 0.25},{"key": "roomSize","value": 0.733333349},{"key": "damping","value": 0.574999988},{"key": "bypass","value": 1},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 51,"uri": "http://two-play.com/plugins/toob-parametric-eq","isEnabled": true,"controlValues": [{"key": "loCut","value": 50.7010994},{"key": "hiCut","value": 21},{"key": "lfLevel","value": 0},{"key": "lfC","value": 120},{"key": "lmfLevel","value": 0},{"key": "lmfC","value": 400},{"key": "lmfQ","value": 1.5},{"key": "hmfLevel","value": 0},{"key": "hmfC","value": 2},{"key": "hmfQ","value": 1.5},{"key": "hfLevel","value": -3.25},{"key": "hfC","value": 3.875},{"key": "gain","value": 0}],"pluginName": "TooB Parametric EQ","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""}],"nextInstanceId": 54,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 8,"preset": {"name": "Sparkling Clean","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -23.1093884},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 55,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -65},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 100},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 53,"uri": "http://two-play.com/plugins/toob-phaser","isEnabled": true,"controlValues": [{"key": "rate","value": 0.424131542},{"key": "dryWet","value": 0.400000006}],"pluginName": "TooB Phaser","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-delay","isEnabled": false,"controlValues": [{"key": "delay","value": 340},{"key": "level","value": 36.6666679},{"key": "feedback","value": 33.1666679}],"pluginName": "TooB Delay","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -2.5},{"key": "bass","value": 0.5},{"key": "mid","value": 0.625},{"key": "treble","value": 0.5},{"key": "model","value": 19},{"key": "gain","value": 0},{"key": "master","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/MesaBoogie_Studio22_Clean.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ToobMlModels/MesaBoogie_Studio22_Clean.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 51,"uri": "http://two-play.com/plugins/toob-convolution-reverb-stereo","isEnabled": true,"controlValues": [{"key": "bypass","value": 1},{"key": "time","value": 30},{"key": "direct_mix","value": -6},{"key": "reverb_mix","value": -4},{"key": "width","value": 1},{"key": "pan","value": 0},{"key": "predelay","value": -1},{"key": "predelay_new","value": 1},{"key": "stretch","value": 1},{"key": "decay","value": 0},{"key": "tails","value": 1},{"key": "loading_state","value": 0}],"pluginName": "TooB Convolution Reverb (Stereo)","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-impulse#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ReverbImpulseFiles/Genesis 6 Studio Live Room.wav"}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-impulse#impulseFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ReverbImpulseFiles/Genesis 6 Studio Live Room.wav\"}"},"title": "","useModUi": false,"iconColor": ""}],"nextInstanceId": 55,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 12,"preset": {"name": "Clean-Lead Split","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -54},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 66,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -96},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 31.622776},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 54,"uri": "uri://two-play/pipedal/pedalboard#Split","isEnabled": true,"controlValues": [{"key": "splitType","value": 0},{"key": "select","value": 1},{"key": "mix","value": 0},{"key": "panL","value": 0},{"key": "volL","value": -3},{"key": "panR","value": 0},{"key": "volR","value": -3}],"pluginName": "","topChain": [{"instanceId": 72,"uri": "http://two-play.com/plugins/toob-chorus","isEnabled": true,"controlValues": [{"key": "rate","value": 0.5},{"key": "depth","value": 0.5},{"key": "dryWet","value": 0.575000048}],"pluginName": "TooB CE-2 Chorus","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 58,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 0},{"key": "trimOut","value": -35},{"key": "model","value": -1},{"key": "gain","value": 2.25},{"key": "master","value": 0},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13},{"key": "gainEnable","value": 0}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/BluesJR.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/BluesJR.json\"}"},"title": "Blues Jr","useModUi": false,"iconColor": ""},{"instanceId": 64,"uri": "http://two-play.com/plugins/toob-cab-ir","isEnabled": true,"controlValues": [{"key": "reverb_mix","value": -6},{"key": "reverb_mix2","value": 0},{"key": "reverb_mix3","value": 0},{"key": "time","value": 1.5},{"key": "direct_mix","value": -40},{"key": "predelay","value": 0},{"key": "loading_state","value": 0}],"pluginName": "TooB Cab IR","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-cab-ir#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/Modern US 001.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile2": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/Modern US 002.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile3": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#String","value": ""}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-cab-ir#impulseFile": "{\"otype_\": \"Path\",\"value\": \"CabIR/Modern US 001.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile2": "{\"otype_\": \"Path\",\"value\": \"CabIR/Modern US 002.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile3": "{\"otype_\": \"Path\",\"value\": \"\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 87,"uri": "http://two-play.com/plugins/toob-volume","isEnabled": true,"controlValues": [{"key": "vol","value": -7.5}],"pluginName": "TooB Volume","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 69,"uri": "http://two-play.com/plugins/toob-convolution-reverb-stereo","isEnabled": true,"controlValues": [{"key": "bypass","value": 1},{"key": "time","value": 30},{"key": "direct_mix","value": -0.666666687},{"key": "reverb_mix","value": -1.39999998},{"key": "width","value": 1},{"key": "pan","value": 0},{"key": "predelay","value": -1},{"key": "predelay_new","value": 1},{"key": "stretch","value": 1.27456057},{"key": "decay","value": 0},{"key": "tails","value": 1},{"key": "loading_state","value": 0}],"pluginName": "TooB Convolution Reverb (Stereo)","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{"http://two-play.com/plugins/toob-impulse#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ReverbImpulseFiles/St. Margaret's Church.wav"}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-impulse#impulseFile": "{\"otype_\": \"Path\",\"value\": \"ReverbImpulseFiles/St. Margaret's Church.wav\"}"},"title": "","useModUi": false,"iconColor": ""}],"bottomChain": [{"instanceId": 81,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 0},{"key": "trimOut","value": -35},{"key": "model","value": -1},{"key": "gain","value": 8.83333302},{"key": "master","value": 0},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13},{"key": "gainEnable","value": 0}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 4,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/DumbleClone_highGain.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/DumbleClone_highGain.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 62,"uri": "http://two-play.com/plugins/toob-cab-ir","isEnabled": true,"controlValues": [{"key": "reverb_mix","value": -18.5},{"key": "reverb_mix2","value": -5.5},{"key": "reverb_mix3","value": 0},{"key": "time","value": 1.5},{"key": "direct_mix","value": -13.5},{"key": "predelay","value": 0},{"key": "loading_state","value": 0}],"pluginName": "TooB Cab IR","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-cab-ir#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/Modern US 002.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile2": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/Modern US 004.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile3": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#String","value": ""}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-cab-ir#impulseFile": "{\"otype_\": \"Path\",\"value\": \"CabIR/Modern US 002.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile2": "{\"otype_\": \"Path\",\"value\": \"CabIR/Modern US 004.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile3": "{\"otype_\": \"Path\",\"value\": \"\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 85,"uri": "http://two-play.com/plugins/toob-volume","isEnabled": true,"controlValues": [{"key": "vol","value": -3}],"pluginName": "TooB Volume","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 83,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "bypass","value": 1},{"key": "dryWet","value": 0.383333325},{"key": "roomSize","value": 0.683333337},{"key": "damping","value": 0},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""}],"midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""}],"nextInstanceId": 90,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 9,"preset": {"name": "Screaming Lead","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -54},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 17,"uri": "http://two-play.com/plugins/toob-input_stage","isEnabled": true,"controlValues": [{"key": "trim","value": -15.75},{"key": "locut","value": 196.5},{"key": "bright","value": 0},{"key": "brightf","value": 1300},{"key": "hicut","value": 13000},{"key": "gate_t","value": -66.5},{"key": "boost","value": 0}],"pluginName": "TooB Input Stage","midiBindings": [{"channel": -1,"symbol": "__bypass","bindingType": 2,"note": 72,"control": 4,"minControlValue": 0,"maxControlValue": 127,"minValue": 0,"maxValue": 1,"rotaryScale": 1,"linearControlType": 0,"switchControlType": 0}],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-delay","isEnabled": true,"controlValues": [{"key": "delay","value": 340},{"key": "level","value": 18.666666},{"key": "feedback","value": 13.3333349}],"pluginName": "TooB Delay","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 42,"uri": "http://two-play.com/plugins/toob-chorus","isEnabled": false,"controlValues": [{"key": "rate","value": 0.291666657},{"key": "depth","value": 0.375},{"key": "dryWet","value": 1}],"pluginName": "TooB CE-2 Chorus","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 47,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 12.5},{"key": "model","value": 29},{"key": "gain","value": 3},{"key": "master","value": -0.5},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/TS9_FullD.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/TS9_FullD.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -4},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "model","value": 12},{"key": "gain","value": 10},{"key": "master","value": -5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/HT40_Overdrive.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/HT40_Overdrive.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 51,"uri": "http://two-play.com/plugins/toob-flanger-stereo","isEnabled": true,"controlValues": [{"key": "manual","value": 0.5},{"key": "depth","value": 0.5},{"key": "rate","value": 0.5},{"key": "lfo","value": 0},{"key": "res","value": 0.5},{"key": "dryWet","value": 0.508333325}],"pluginName": "TooB BF-2 Stereo Flanger","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 53,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "bypass","value": 1},{"key": "dryWet","value": 0.150000006},{"key": "roomSize","value": 0.649999976},{"key": "damping","value": 0},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""}],"nextInstanceId": 56,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 10,"preset": {"name": "Light Crunch","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -23.1093884},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 49,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -102},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 100},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 42,"uri": "http://two-play.com/plugins/toob-chorus","isEnabled": false,"controlValues": [{"key": "rate","value": 0.291666657},{"key": "depth","value": 0.375},{"key": "dryWet","value": 1}],"pluginName": "TooB CE-2 Chorus","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-delay","isEnabled": true,"controlValues": [{"key": "delay","value": 340},{"key": "level","value": 27},{"key": "feedback","value": 39}],"pluginName": "TooB Delay","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -0.5},{"key": "bass","value": 0.650000036},{"key": "mid","value": 0.5},{"key": "treble","value": 0.266666681},{"key": "model","value": 17},{"key": "gain","value": 7.75},{"key": "master","value": -4},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/ProCoRat.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ToobMlModels/ProCoRat.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 51,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -3},{"key": "trimOut","value": -35},{"key": "model","value": -1},{"key": "gain","value": 3},{"key": "master","value": -6},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13},{"key": "gainEnable","value": 0}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/FenderPrinceton_highLevel.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"/var/pipedal/audio_uploads/ToobMlModels/FenderPrinceton_highLevel.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 58,"uri": "http://two-play.com/plugins/toob-nam","isEnabled": true,"controlValues": [{"key": "inputGain","value": -12.666667},{"key": "inputGainOut","value": -35},{"key": "outputGain","value": 0},{"key": "gate","value": -120},{"key": "gateOut","value": 0},{"key": "buffer","value": 0},{"key": "toneStack","value": 3},{"key": "bass","value": 5},{"key": "mid","value": 5},{"key": "treble","value": 5},{"key": "inputCalibrationMode","value": 1},{"key": "calibration","value": -6},{"key": "outputCalibration","value": 0},{"key": "version","value": 1}],"pluginName": "TooB Neural Amp Modeler","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-nam#modelFile": "{\"otype_\": \"Path\",\"value\": \"\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 53,"uri": "http://two-play.com/plugins/toob-parametric-eq","isEnabled": true,"controlValues": [{"key": "loCut","value": 27.2704601},{"key": "hiCut","value": 21},{"key": "lfLevel","value": 0},{"key": "lfC","value": 120},{"key": "lmfLevel","value": -6.5},{"key": "lmfC","value": 551.471313},{"key": "lmfQ","value": 0.850504637},{"key": "hmfLevel","value": -7.5},{"key": "hmfC","value": 2.5746665},{"key": "hmfQ","value": 1.5},{"key": "hfLevel","value": -6.5},{"key": "hfC","value": 2.25},{"key": "gain","value": 0}],"pluginName": "TooB Parametric EQ","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 31,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "dryWet","value": 0.416666687},{"key": "roomSize","value": 0.625},{"key": "damping","value": 0.574999988},{"key": "bypass","value": 1},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""}],"nextInstanceId": 58,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 11,"preset": {"name": "Jazz","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -23.1093884},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 45,"uri": "http://two-play.com/plugins/toob-noise-gate","isEnabled": true,"controlValues": [{"key": "threshold","value": -53},{"key": "hysteresis","value": -6},{"key": "trigger_led","value": 0},{"key": "reduction","value": -60},{"key": "attack","value": 1},{"key": "hold","value": 100},{"key": "release","value": 330},{"key": "gate_level","value": -60}],"pluginName": "TooB Noise Gate","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 41,"uri": "http://two-play.com/plugins/toob-chorus","isEnabled": false,"controlValues": [{"key": "rate","value": 0.208333328},{"key": "depth","value": 0.5},{"key": "dryWet","value": 1}],"pluginName": "TooB CE-2 Chorus","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 47,"uri": "http://two-play.com/plugins/toob-parametric-eq","isEnabled": true,"controlValues": [{"key": "loCut","value": 41.8936462},{"key": "hiCut","value": 21},{"key": "lfLevel","value": 0},{"key": "lfC","value": 120},{"key": "lmfLevel","value": -5.5},{"key": "lmfC","value": 238.107834},{"key": "lmfQ","value": 0.837899804},{"key": "hmfLevel","value": 0},{"key": "hmfC","value": 2},{"key": "hmfQ","value": 1.5},{"key": "hfLevel","value": -12},{"key": "hfC","value": 1},{"key": "gain","value": 5.83333349}],"pluginName": "TooB Parametric EQ","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 39,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": -13},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "model","value": 0},{"key": "gain","value": 4.66666651},{"key": "master","value": 5},{"key": "sag","value": 0.550000012},{"key": "sagd","value": 0.900000036},{"key": "sagf","value": 13}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 2,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/FenderPrinceton_clean.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/FenderPrinceton_clean.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 31,"uri": "http://two-play.com/plugins/toob-freeverb","isEnabled": true,"controlValues": [{"key": "dryWet","value": 0.416666657},{"key": "roomSize","value": 0.625},{"key": "damping","value": 0.574999988},{"key": "bypass","value": 1},{"key": "tails","value": 1}],"pluginName": "TooB Freeverb","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""}],"nextInstanceId": 48,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}},{"instanceId": 13,"preset": {"name": "Stereo Vibro","input_volume_db": 0,"output_volume_db": 0,"items": [{"instanceId": 16,"uri": "http://two-play.com/plugins/toob-tuner","isEnabled": true,"controlValues": [{"key": "REFFREQ","value": 440},{"key": "THRESHOLD","value": -54},{"key": "MUTE","value": 0}],"pluginName": "TooB Tuner","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 61,"uri": "http://two-play.com/plugins/toob-tremolo","isEnabled": true,"controlValues": [{"key": "rate","value": 5},{"key": "depth","value": 0.566666663},{"key": "harmonic","value": 1},{"key": "shape","value": 0}],"pluginName": "TooB Tremolo (Stereo)","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 64,"uri": "uri://two-play/pipedal/pedalboard#Split","isEnabled": true,"controlValues": [{"key": "splitType","value": 2},{"key": "select","value": 0},{"key": "mix","value": 0},{"key": "panL","value": -1},{"key": "volL","value": 0},{"key": "panR","value": 1},{"key": "volR","value": -1}],"pluginName": "","topChain": [{"instanceId": 67,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 0},{"key": "trimOut","value": -35},{"key": "model","value": -1},{"key": "gain","value": 8.16666698},{"key": "master","value": 0},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13},{"key": "gainEnable","value": 0}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 2,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/BluesJR.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/BluesJR.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 71,"uri": "http://two-play.com/plugins/toob-cab-ir","isEnabled": true,"controlValues": [{"key": "reverb_mix","value": -8.5},{"key": "reverb_mix2","value": -4.5},{"key": "reverb_mix3","value": -3},{"key": "time","value": 1.5},{"key": "direct_mix","value": -40},{"key": "predelay","value": 0},{"key": "loading_state","value": 0}],"pluginName": "TooB Cab IR","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-cab-ir#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/80s UK 001.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile2": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/80s UK 002.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile3": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#String","value": ""}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-cab-ir#impulseFile": "{\"otype_\": \"Path\",\"value\": \"CabIR/80s UK 001.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile2": "{\"otype_\": \"Path\",\"value\": \"CabIR/80s UK 002.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile3": "{\"otype_\": \"Path\",\"value\": \"\"}"},"title": "","useModUi": false,"iconColor": ""}],"bottomChain": [{"instanceId": 68,"uri": "http://two-play.com/plugins/toob-ml","isEnabled": true,"controlValues": [{"key": "trim","value": 0},{"key": "trimOut","value": -35},{"key": "model","value": -1},{"key": "gain","value": 4},{"key": "master","value": -2},{"key": "bass","value": 0.5},{"key": "mid","value": 0.5},{"key": "treble","value": 0.5},{"key": "sag","value": 0},{"key": "sagd","value": 0},{"key": "sagf","value": 13},{"key": "gainEnable","value": 0}],"pluginName": "TooB ML Amplifier","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [true,{"http://two-play.com/plugins/toob-ml#modelFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "ToobMlModels/BluesJR.json"},"http://two-play.com/plugins/toob-ml#version": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Float","value": 1}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-ml#modelFile": "{\"otype_\": \"Path\",\"value\": \"ToobMlModels/BluesJR.json\"}"},"title": "","useModUi": false,"iconColor": ""},{"instanceId": 73,"uri": "http://two-play.com/plugins/toob-cab-ir","isEnabled": true,"controlValues": [{"key": "reverb_mix","value": -3},{"key": "reverb_mix2","value": -3},{"key": "reverb_mix3","value": -3},{"key": "time","value": 1.5},{"key": "direct_mix","value": -40},{"key": "predelay","value": 0},{"key": "loading_state","value": 0}],"pluginName": "TooB Cab IR","midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 1,"lv2State": [true,{"http://two-play.com/plugins/toob-cab-ir#impulseFile": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#Path","value": "CabIR/80s UK 002.wav"},"http://two-play.com/plugins/toob-cab-ir#impulseFile2": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#String","value": ""},"http://two-play.com/plugins/toob-cab-ir#impulseFile3": {"flags": 3,"atomType": "http://lv2plug.in/ns/ext/atom#String","value": ""}}],"lilvPresetUri": "","pathProperties": {"http://two-play.com/plugins/toob-cab-ir#impulseFile": "{\"otype_\": \"Path\",\"value\": \"CabIR/80s UK 002.wav\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile2": "{\"otype_\": \"Path\",\"value\": \"\"}","http://two-play.com/plugins/toob-cab-ir#impulseFile3": "{\"otype_\": \"Path\",\"value\": \"\"}"},"title": "","useModUi": false,"iconColor": ""}],"midiBindings": [ ],"midiChannelBinding": null,"stateUpdateCount": 0,"lv2State": [false,{}],"lilvPresetUri": "","pathProperties": {},"title": "","useModUi": false,"iconColor": ""}],"nextInstanceId": 73,"snapshots": [ ],"selectedSnapshot": -1,"selectedPlugin": 16}}]} \ No newline at end of file diff --git a/default_presets/presets/banks.versionInfo b/default_presets/presets/banks.versionInfo new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/default_presets/presets/banks.versionInfo @@ -0,0 +1 @@ +2 diff --git a/docs/Installing.md b/docs/Installing.md index a3ad872..25bd308 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,18 +13,18 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v1.5.92](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_arm64.deb) -- [Ubuntu 24.04 through 25.04 (aarch64) v1.5.92](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_arm64.deb) -- [Ubuntu 24.04 through 25.04 (amd64) v1.5.92](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v1.5.93](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_arm64.deb) +- [Ubuntu 24.04 through 25.04 (aarch64) v1.5.93](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_arm64.deb) +- [Ubuntu 24.04 through 25.04 (amd64) v1.5.93](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_amd64.deb) -Version 1.5.92 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: +Version 1.5.93 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: ``` sudo apt update sudo apt upgrade cd ~/Downloads - sudo apt-get install ./pipedal_1.5.92_arm64.deb + sudo apt-get install ./pipedal_1.5.93_arm64.deb ``` You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 7dd3972..cc7141c 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,26 @@ # Release Notes +## PiPedal 1.5.93 Release + +### Major Features in This Build + +- **New TooB Tremolo plugin**. Normal and harmonic tremolo in mono and stereo versions. Harmonic tremolo is a tremolo variant that modulates the amplitude of high-frequency and low-frequency content in opposite phase. It follows the lead of many classic and modern Fender amps, which also provide harmonic tremolo. Stereo harmonic tremolo is particularly smooth and pleasant. + +- **Colored plugin icons**. Tap on the title of the plugin in the middle toolbar to edit the display name of the plugin and choose a color for the icon. + +- **Tools for copying presets between banks**. You can now import presets from another bank using the Presets menu found at the top of the main screen, or you can import presets from another bank, or export presets from the current bank to another bank, using menu commands found in the "Manage Presets" dialog. + +- **Multi-select in the Manage Presets dialog** makes it easier to perform bulk exports to another bank or delete multiple presets. To select multiple presets when using a mouse, press the Ctrl key and click on presets to toggle selection, or press the Shift key and click on a preset to select all presets from the last selected to the newly selected preset. On touch devices, long-press on a preset (without dragging) to start multi-select action mode (an Android touch-UI convention), then tap on individual presets to toggle selection. + +### Bug fixes + +- several bugs relating to the new Factory Presets bank: preset amp model files were not getting properly installed; one preset used an amp model that is not included in factory installed presets. And a new preset showing off the new TooB Tremolo plugin has been added. + +- pipedal_config will no longer accept the --port option without also specifying the --install option. (It used to silently fail). + +- pipedal_latency_test now provides instructions on how to enable missing real-time scheduling permissions for non-root users. + + ## PiPedal 1.5.92 Release ### Major Features in This Build diff --git a/docs/download.md b/docs/download.md index 2d1f9be..c54df26 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,9 +4,9 @@ Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v1.5.92 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_arm64.deb) -- [Ubuntu 24.x, 25.04 (aarch64) v1.5.92 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_arm64.deb) -- [Ubuntu 24.x, 25.04 (amd64) v1.5.92 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v1.5.93 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_arm64.deb) +- [Ubuntu 24.x, 25.04 (aarch64) v1.5.93 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_arm64.deb) +- [Ubuntu 24.x, 25.04 (amd64) v1.5.93 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.93/pipedal_1.5.93_amd64.deb) Install the package by running @@ -14,7 +14,7 @@ Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.5.92_arm64.deb + sudo apt-get install ./pipedal_1.5.93_arm64.deb ``` You MUST use `apt-get` to install the package. `apt install` will NOT install the package correctly. The message about missing permissions given by `apt-get` is expected, and can be safely ignored. diff --git a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl index d88d73a..4e5ea66 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabIR.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabIR.ttl @@ -93,7 +93,7 @@ cabir:impulseFile3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ TooB Cab IR is a convolution-based guitar cabinet impulse response simulator. diff --git a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl index 2789e8d..69a7c06 100644 --- a/lv2/aarch64/ToobAmp.lv2/CabSim.ttl +++ b/lv2/aarch64/ToobAmp.lv2/CabSim.ttl @@ -50,7 +50,7 @@ toob:frequencyResponseVector doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; mod:brand "TooB"; mod:label "TooB CabSim"; diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl index 7794f2c..6c10ed4 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverb.ttl @@ -53,7 +53,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ Toob Convolution Reverb Stereo uses convolution reverb impulse/response files in order to produce highly diff --git a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl index b754268..e70825c 100644 --- a/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ConvolutionReverbStereo.ttl @@ -51,7 +51,7 @@ toobimpulse:impulseFile doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ diff --git a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl index 229bc9a..233e430 100644 --- a/lv2/aarch64/ToobAmp.lv2/InputStage.ttl +++ b/lv2/aarch64/ToobAmp.lv2/InputStage.ttl @@ -66,7 +66,7 @@ inputStage:filterGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; mod:brand "TooB"; mod:label "TooB Input"; diff --git a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl index 375bcb9..b3cfdae 100644 --- a/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl +++ b/lv2/aarch64/ToobAmp.lv2/PowerStage2.ttl @@ -68,7 +68,7 @@ pstage:stage3 doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; mod:brand "TooB"; mod:label "Power Stage"; diff --git a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl index 5ae2b18..e5b91cd 100644 --- a/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl +++ b/lv2/aarch64/ToobAmp.lv2/SpectrumAnalyzer.ttl @@ -57,7 +57,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment "TooB spectrum analyzer" ; mod:brand "TooB"; diff --git a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl index c306598..9d869ea 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToneStack.ttl @@ -57,7 +57,7 @@ tonestack:eqGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/Toob3BandEq.ttl b/lv2/aarch64/ToobAmp.lv2/Toob3BandEq.ttl index 45a1d4f..aa2e407 100644 --- a/lv2/aarch64/ToobAmp.lv2/Toob3BandEq.ttl +++ b/lv2/aarch64/ToobAmp.lv2/Toob3BandEq.ttl @@ -52,7 +52,7 @@ toob:frequencyResponseVector doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so index a409b12..32a1c16 100755 Binary files a/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so and b/lv2/aarch64/ToobAmp.lv2/ToobAmpUI.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl index 7217dfc..67a19a7 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobChorus.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ Emulation of a Boss CE-2 Chorus. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl index 99e3e5f..61082ef 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobDelay.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ A straightforward no-frills digital delay. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl index 9e74eaf..2bb53b5 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlanger.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl index 933a93d..e3fda58 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFlangerStereo.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ Digital emulation of a Boss BF-2 Flanger. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl index d0cdf79..05c18ed 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobFreeverb.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl b/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl index 763cba0..70d41fd 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobGraphicEq.ttl @@ -41,7 +41,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals. """ ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl b/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl index de45efc..940876b 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobLooperFour.ttl @@ -92,7 +92,7 @@ myprefix:output_group doap:license ; doap:maintainer ; lv2:minorVersion 2 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl b/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl index 8053e48..af0e8e3 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobLooperOne.ttl @@ -78,7 +78,7 @@ myprefix:output_group doap:license ; doap:maintainer ; lv2:minorVersion 2 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl index 4d176c1..c48acc7 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobML.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobML.ttl @@ -67,7 +67,7 @@ toobml:sagGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented using neural-network-based machine learning models of real amplifiers. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl b/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl index c22d803..8109f12 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobMix.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ Remix a stereo input signal. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl index 8cd1756..e048033 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobNeuralAmpModeler.ttl @@ -72,7 +72,7 @@ toobNam:calibrationGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ TooB Neural Amp Modeler is a neural network based Amp Simulator. It uses .nam model files that are generated by training nueral networks on audio recordings of actual guitar amps, effects pedals and other equipment. .nam files contain data from the trained models, which can be loaded into diff --git a/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl b/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl index b3f4b8c..329ad48 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobNoiseGate.ttl @@ -54,7 +54,7 @@ noisegate:envelope_group doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ A noise gate is an audio processing tool that controls the volume of an audio signal by allowing it to pass through only when it exceeds a set threshold. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobParametricEq.ttl b/lv2/aarch64/ToobAmp.lv2/ToobParametricEq.ttl index 2c49908..9bcff31 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobParametricEq.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobParametricEq.ttl @@ -79,7 +79,7 @@ parametric_eq:hfGroup a lv2:Plugin , - lv2:EQPlugin ; + lv2:ParaEQPlugin ; doap:name "TooB Parametric EQ" , "TooB Parametric EQ"@en-gb ; @@ -87,7 +87,7 @@ parametric_eq:hfGroup doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; uiext:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl b/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl index 438c2c3..dabee14 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobPhaser.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ A loose emulation of an MXR® Phase 90 Phaser. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl b/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl index 125c7ed..ed8802c 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobPlayer.ttl @@ -60,7 +60,7 @@ toobPlayer:seek doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ Play audio files. Audio files can optionally be loooped by pressing the "Set loop" button. diff --git a/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl b/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl index 6014e8a..669b5e5 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobRecordMono.ttl @@ -51,7 +51,7 @@ recordPrefix:audioFile doap:license ; doap:maintainer ; lv2:minorVersion 2 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl index c42d2b6..5b8fcea 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobRecordStereo.ttl @@ -88,7 +88,7 @@ myprefix:loop3_group doap:license ; doap:maintainer ; lv2:minorVersion 2 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; ui:ui ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTremolo.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTremolo.ttl new file mode 100644 index 0000000..6bc4522 --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/ToobTremolo.ttl @@ -0,0 +1,199 @@ +@prefix doap: . +@prefix lv2: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . +@prefix urid: . +@prefix atom: . +@prefix midi: . +@prefix epp: . +@prefix uiext: . +@prefix idpy: . +@prefix foaf: . +@prefix mod: . +@prefix param: . +@prefix work: . +@prefix pg: . + +@prefix atom: . +@prefix patch: . +@prefix rdfs: . +@prefix state: . +@prefix urid: . +@prefix xsd: . +@prefix toobTremolo: . +@prefix pipedal_ui: . + + + + a foaf:Person ; + foaf:name "Robin Davies" ; + foaf:mbox ; + foaf:homepage . + + + a lv2:Plugin , + lv2:ModulatorPlugin ; + doap:name "TooB Tremolo (Stereo)" , + "TooB Tremolo (Stereo)"@en-us + ; + uiext:ui ; + doap:license ; + doap:maintainer ; + lv2:minorVersion 0 ; + lv2:microVersion 71 ; + rdfs:comment """ +A tremolo effect, equivalent to "vibrato" on vintage Fender tube amps. + +TooB Tremolo supports both normal tremolo and harmonic tremolo. When the Harmonic switch is turned on, +the tremolo effect is applied +with opposite phase to the low and high-frequency portions of the input signal. This replicates the harmonic +tremolo feature found on classic Fender amps like the Bandmaster and Vibroverb. Harmonic tremolo is favored for +sounding smoother than normal tremolo, and can introduce a pleasant and subtle effect that sounds a bit like a gentle phaser. +""" ; + + mod:brand "TooB"; + mod:label "Tremolo"; + lv2:optionalFeature lv2:hardRTCapable; + + lv2:port + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 0 ; + lv2:symbol "rate" ; + lv2:name "Rate"; + + lv2:default 5.0 ; + lv2:minimum 0.25 ; + lv2:maximum 10.0 ; + units:unit units:hz ; + rdfs:comment "Rate"; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 1 ; + lv2:symbol "depth" ; + lv2:name "Depth"; + + lv2:default 0.5 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Depth"; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 2 ; + lv2:portProperty lv2:toggled ; + lv2:symbol "harmonic" ; + lv2:name "Harmonic"; + + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Harmonic Tremelo"; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 3 ; + lv2:symbol "shape" ; + lv2:name "Shape"; + + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Controls the shape of the LFO"; + ], + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 4 ; + lv2:symbol "inl" ; + lv2:name "In L" + ], + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 5 ; + lv2:symbol "inr" ; + lv2:name "In R" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 6 ; + lv2:symbol "outl" ; + lv2:name "Out L" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 7 ; + lv2:symbol "outr" ; + lv2:name "OutR" + ], + + [ + a atom:AtomPort , + lv2:InputPort; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + atom:supports patch:Message ; + + lv2:index 8 ; + lv2:symbol "control" ; + lv2:name "Control" ; + rdfs:comment "Control" ; + ] , [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 9; + lv2:symbol "notify" ; + lv2:name "Notify" ; + rdfs:comment "Notification" ; + ] + + . + + + a uiext:X11UI ; + lv2:binary ; + + lv2:extensionData uiext::idle ; + lv2:extensionData uiext:resize ; + lv2:extensionData uiext:idleInterface; + lv2:requiredFeature uiext:idleInterface ; + . + + + pipedal_ui:ui . + + + + + a pipedal_ui:ui ; + pipedal_ui:frequencyPlot + [ + lv2:index 4 ; + pipedal_ui:patchProperty ; + pipedal_ui:xLeft 0 ; + pipedal_ui:xRight 1 ; + pipedal_ui:yTop 1.1; + pipedal_ui:yBottom -0.1 ; + pipedal_ui:xLog 0 ; + pipedal_ui:yDb 0 ; + pipedal_ui:width 120 ; + ] + . + diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTremoloMono.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTremoloMono.ttl new file mode 100644 index 0000000..400eda8 --- /dev/null +++ b/lv2/aarch64/ToobAmp.lv2/ToobTremoloMono.ttl @@ -0,0 +1,184 @@ +@prefix doap: . +@prefix lv2: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . +@prefix urid: . +@prefix atom: . +@prefix midi: . +@prefix epp: . +@prefix uiext: . +@prefix idpy: . +@prefix foaf: . +@prefix mod: . +@prefix param: . +@prefix work: . +@prefix pg: . + +@prefix atom: . +@prefix patch: . +@prefix rdfs: . +@prefix state: . +@prefix urid: . +@prefix xsd: . +@prefix toobTremeloMono: . +@prefix pipedal_ui: . + + + + a foaf:Person ; + foaf:name "Robin Davies" ; + foaf:mbox ; + foaf:homepage . + + + a lv2:Plugin , + lv2:ModulatorPlugin ; + doap:name "TooB Tremolo (Mono)" , + "TooB Tremolo (Mono)"@en-us + ; + uiext:ui ; + doap:license ; + doap:maintainer ; + lv2:minorVersion 0 ; + lv2:microVersion 71 ; + rdfs:comment """ +A tremolo effect, equivalent to "vibrato" on vintage Fender tube amps. + +TooB Tremolo supports both normal tremolo and harmonic tremolo. When the Harmonic switch is turned on, +the tremolo effect is applied +with opposite phase to the low and high-frequency portions of the input signal. This replicates the harmonic +tremolo feature found on classic Fender amps like the Bandmaster and Vibroverb. Harmonic tremolo is favored for +sounding smoother than normal tremolo, and can introduce a pleasant and subtle effect that sounds a bit like a gentle phaser. +""" ; + + mod:brand "TooB"; + mod:label "Tremolo"; + lv2:optionalFeature lv2:hardRTCapable; + + lv2:port + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 0 ; + lv2:symbol "rate" ; + lv2:name "Rate"; + + lv2:default 5.0 ; + lv2:minimum 0.25 ; + lv2:maximum 10.0 ; + units:unit units:hz ; + rdfs:comment "Rate"; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 1 ; + lv2:symbol "depth" ; + lv2:name "Depth"; + + lv2:default 0.5 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Depth"; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 2 ; + lv2:portProperty lv2:toggled ; + lv2:symbol "harmonic" ; + lv2:name "Harmonic"; + + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Harmonic Tremelo"; + ], + [ + a lv2:InputPort , + lv2:ControlPort ; + + lv2:index 3 ; + lv2:symbol "shape" ; + lv2:name "Shape"; + + lv2:default 0.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + rdfs:comment "Controls the shape of the LFO"; + ], + [ + a lv2:AudioPort , + lv2:InputPort ; + lv2:index 4 ; + lv2:symbol "inl" ; + lv2:name "In L" + ], + [ + a lv2:AudioPort , + lv2:OutputPort ; + lv2:index 5 ; + lv2:symbol "outl" ; + lv2:name "Out L" + ], + [ + a atom:AtomPort , + lv2:InputPort; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + atom:supports patch:Message ; + + lv2:index 6 ; + lv2:symbol "control" ; + lv2:name "Control" ; + rdfs:comment "Control" ; + ] , [ + a atom:AtomPort , + lv2:OutputPort ; + atom:bufferType atom:Sequence ; + # atom:supports patch:Message; + lv2:designation lv2:control ; + lv2:index 7; + lv2:symbol "notify" ; + lv2:name "Notify" ; + rdfs:comment "Notification" ; + ] + + . + + + a uiext:X11UI ; + lv2:binary ; + + lv2:extensionData uiext::idle ; + lv2:extensionData uiext:resize ; + lv2:extensionData uiext:idleInterface; + lv2:requiredFeature uiext:idleInterface ; + . + + + pipedal_ui:ui . + + + + + a pipedal_ui:ui ; + pipedal_ui:frequencyPlot + [ + lv2:index 4 ; + pipedal_ui:patchProperty ; + pipedal_ui:xLeft 0 ; + pipedal_ui:xRight 1 ; + pipedal_ui:yTop 1.1; + pipedal_ui:yBottom -0.1 ; + pipedal_ui:xLog 0 ; + pipedal_ui:yDb 0 ; + pipedal_ui:width 120 ; + ] + . + diff --git a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl index 791cc20..9b095b0 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobTuner.ttl @@ -40,7 +40,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ TooB Tuner is a chromatic guitar tuner. """ ; diff --git a/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl b/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl index f6bc3be..ba43de3 100644 --- a/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl +++ b/lv2/aarch64/ToobAmp.lv2/ToobVolume.ttl @@ -39,7 +39,7 @@ doap:license ; doap:maintainer ; lv2:minorVersion 0 ; - lv2:microVersion 70 ; + lv2:microVersion 71 ; rdfs:comment """ Volume control. diff --git a/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a72.so b/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a72.so index ba1a57b..2f516c9 100755 Binary files a/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a72.so and b/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a72.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a76.so b/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a76.so index cb02a43..01d4764 100755 Binary files a/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a76.so and b/lv2/aarch64/ToobAmp.lv2/bin/ToobAmp-a76.so differ diff --git a/lv2/aarch64/ToobAmp.lv2/impulseFiles/CabIR/.Licenses.ToobAmp.lv2.txt b/lv2/aarch64/ToobAmp.lv2/impulseFiles/CabIR/.Licenses.ToobAmp.lv2.txt deleted file mode 100644 index 1c98d02..0000000 --- a/lv2/aarch64/ToobAmp.lv2/impulseFiles/CabIR/.Licenses.ToobAmp.lv2.txt +++ /dev/null @@ -1,26 +0,0 @@ -The following files have been graciously provided by Kristoffer Ekstrand (akak Adventure Kid), -and are licensed under a CC-BY-4.0 license: - -https://creativecommons.org/licenses/by/4.0/ - -80s UK 001.wav -80s UK 002.wav -80s US 001.wav -80s US 002.wav -Modern UK 001.wav -Modern UK 002.wav -Modern US 001.wav -Modern US 002.wav -Modern US 003.wav -Modern US 004.wav -Radio 001.wav -Speaker 001.wav -Speaker 002.wav -Vintage UK 001.wav -Vintage UK 002.wav -Vintage US 001.wav -Vintage US 002.wav - -By: Adventure Kid Research & Technology, Kristoffer Ekstrand. - -Source: https://www.adventurekid.se/akrt/free-reverb-impulse-responses/ diff --git a/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/.Licenses.ToobAmp.lv2.txt b/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/.Licenses.ToobAmp.lv2.txt deleted file mode 100644 index f8e43f3..0000000 --- a/lv2/aarch64/ToobAmp.lv2/impulseFiles/reverb/.Licenses.ToobAmp.lv2.txt +++ /dev/null @@ -1,52 +0,0 @@ -Toob Convolution Reverb's impulse files were provided by the OpenAir project. -https://www.openair.hosted.york.ac.uk/?page_id=571, under CC-By-4.0 licenses. - - -Arthur Sykes Rymer Auditorium.wav ---------------------------------------- -By: www.openairlib.net, University of York - -This work is provided under a Creative Commons Attribution 4.0 International License. -https://creativecommons.org/licenses/by/4.0/ - -Source: https://www.openair.hosted.york.ac.uk/?page_id=425 - - -Jack Lyons Hall, University of York.wav ---------------------------------------- -By: www.openairlib.net, Audiolab University of York, Alex Duffell, Aishwarya Sridhar, Zhong Li - -This work is provided under a Creative Commons Attribution 4.0 International License. -https://creativecommons.org/licenses/by/4.0/ - -Source: https://www.openair.hosted.york.ac.uk/?page_id=571 - - -Koli National Park - Winter.wav ---------------------------------------- -By: www.openairlib.net, Audio Lab University of York, Andrew Chadwick, Simon Shelley - -This work is provided under a Creative Commons Attribution 4.0 International License. -https://creativecommons.org/licenses/by/4.0/ - -Source: https://www.openair.hosted.york.ac.uk/?page_id=584 - - -St. Margaret’s Church.wav ---------------------------------------- -By: www.openairlib.net, AudioLab University of York, www.ncem.co.uk - -This work is provided under a Creative Commons Attribution 4.0 International License. -https://creativecommons.org/licenses/by/4.0/ - -Source: https://www.openair.hosted.york.ac.uk/?page_id=702 - - -Genesis 6 Studio Live Room.wav ---------------------------------------- -By: www.openairlib.net - -This work is provided under a Creative Commons Attribution 4.0 International License. -https://creativecommons.org/licenses/by/4.0/ - -Source: https://www.openair.hosted.york.ac.uk/?page_id=483 diff --git a/lv2/aarch64/ToobAmp.lv2/manifest.ttl b/lv2/aarch64/ToobAmp.lv2/manifest.ttl index d8b6af9..c92dffd 100644 --- a/lv2/aarch64/ToobAmp.lv2/manifest.ttl +++ b/lv2/aarch64/ToobAmp.lv2/manifest.ttl @@ -106,6 +106,14 @@ lv2:binary ; rdfs:seeAlso . + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + a lv2:Plugin ; lv2:binary ; rdfs:seeAlso . diff --git a/lv2/aarch64/ToobAmp.lv2/models/tones/README.md b/lv2/aarch64/ToobAmp.lv2/models/tones/README.md deleted file mode 100644 index 0d15c17..0000000 --- a/lv2/aarch64/ToobAmp.lv2/models/tones/README.md +++ /dev/null @@ -1 +0,0 @@ -# NeuralPi Tones diff --git a/lv2/x86_64/toobamp_1.2.70_amd64.deb b/lv2/x86_64/toobamp_1.2.71_amd64.deb similarity index 59% rename from lv2/x86_64/toobamp_1.2.70_amd64.deb rename to lv2/x86_64/toobamp_1.2.71_amd64.deb index 6d318df..2c424c0 100644 Binary files a/lv2/x86_64/toobamp_1.2.70_amd64.deb and b/lv2/x86_64/toobamp_1.2.71_amd64.deb differ diff --git a/src/Banks.hpp b/src/Banks.hpp index e8d25e8..4522426 100644 --- a/src/Banks.hpp +++ b/src/Banks.hpp @@ -23,316 +23,348 @@ #include "Pedalboard.hpp" #include "PiPedalException.hpp" -namespace pipedal { +namespace pipedal +{ -#define GETTER_SETTER_REF(name) \ - decltype(name##_)& name() { return name##_;} \ - const decltype(name##_)& name() const { return name##_;} \ +#define GETTER_SETTER_REF(name) \ + decltype(name##_) &name() { return name##_; } \ + const decltype(name##_) &name() const { return name##_; } \ void name(const decltype(name##_) &value) { name##_ = value; } -#define GETTER_SETTER_VEC(name) \ - decltype(name##_)& name() { return name##_;} \ - const decltype(name##_)& name() const { return name##_;} +#define GETTER_SETTER_VEC(name) \ + decltype(name##_) &name() { return name##_; } \ + const decltype(name##_) &name() const { return name##_; } - -#define GETTER_SETTER(name) \ - decltype(name##_) name() const { return name##_;} \ +#define GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ void name(decltype(name##_) value) { name##_ = value; } + class PresetIndexEntry + { + int64_t instanceId_ = 0; + std::string name_; -class PresetIndexEntry { - int64_t instanceId_ = 0; - std::string name_; -public: - GETTER_SETTER(instanceId); - GETTER_SETTER(name); - DECLARE_JSON_MAP(PresetIndexEntry); -}; - -class PresetIndex { - int64_t selectedInstanceId_ = -1; - bool presetChanged_ = false; - std::vector presets_; -public: - bool GetPresetName(int64_t instanceId, std::string*pResult) { - for (size_t i = 0; i < presets_.size(); ++i) + public: + PresetIndexEntry() {} + PresetIndexEntry(uint64_t instanceId, const std::string &name) + : instanceId_(instanceId), name_(name) { - if (presets_[i].instanceId() == instanceId) + } + + GETTER_SETTER(instanceId); + GETTER_SETTER(name); + DECLARE_JSON_MAP(PresetIndexEntry); + }; + + class PresetIndex + { + int64_t selectedInstanceId_ = -1; + bool presetChanged_ = false; + std::vector presets_; + + public: + bool GetPresetName(int64_t instanceId, std::string *pResult) + { + for (size_t i = 0; i < presets_.size(); ++i) { - *pResult = presets_[i].name(); - return true; - } - } - return false; - } - GETTER_SETTER(selectedInstanceId); - GETTER_SETTER_VEC(presets); - GETTER_SETTER(presetChanged); - - DECLARE_JSON_MAP(PresetIndex); -}; - - -class BankFileEntry { - int64_t instanceId_; - Pedalboard preset_; -public: - GETTER_SETTER(instanceId); - GETTER_SETTER_REF(preset); - - DECLARE_JSON_MAP(BankFileEntry); -}; -class BankFile { - std::string name_; - int64_t nextInstanceId_ = 0; - int64_t selectedPreset_ = -1; - std::vector> presets_; - -public: - GETTER_SETTER(name); - GETTER_SETTER(nextInstanceId); - GETTER_SETTER(selectedPreset); - GETTER_SETTER_VEC(presets); - - void clear() - { - nextInstanceId_ = 0; - presets_.clear(); - selectedPreset_ = -1; - } - - void move(size_t from, size_t to) - { - if (from >= this->presets_.size()) { - throw std::invalid_argument("Argument out of range."); - } - if (to >= this->presets_.size()) { - throw std::invalid_argument("Argument out of range."); - } - std::unique_ptr t = std::move(this->presets_[from]); - presets_.erase(presets_.begin()+from); - presets_.insert(presets_.begin()+to,std::move(t)); - } - void updateNextIndex() { - int64_t t = 0; - for (size_t i = 0; i < this->presets_.size(); ++i) - { - int64_t instanceId = this->presets_[i]->instanceId(); - if (instanceId > t) t = instanceId; - - } - this->nextInstanceId_ = t; - } - int64_t addPreset(const Pedalboard&preset, int64_t afterItem = -1) - { - if (hasName(preset.name())) - { - throw PiPedalStateException("A preset by that name already exists."); - } - std::unique_ptr entry = std::make_unique(); - entry->instanceId(++nextInstanceId_); - entry->preset(preset); - int64_t instanceId = entry->instanceId(); - if (afterItem == -1) - { - this->presets_.push_back(std::move(entry)); - } else { - for (auto it = this->presets_.begin(); it != this->presets_.end(); ++it) - { - if ((*it)->instanceId() == afterItem) + if (presets_[i].instanceId() == instanceId) { - ++it; - this->presets_.insert(it,std::move(entry)); - break; + *pResult = presets_[i].name(); + return true; } } + return false; } - return instanceId; - } - bool hasItem(int64_t instanceId) - { - for (size_t i = 0; i < presets_.size(); ++i) - { - if (presets_[i]->instanceId() == instanceId) - { - return true; - } - } - return false; - } - bool hasName(const std::string &name) { - for (size_t i = 0; i < presets_.size(); ++i) - { - if (presets_[i]->preset().name() == name) - { - return true; - } - } - return false; - } - bool renamePreset(int64_t instanceId, const std::string&name) - { - if (hasName(name)) - { - throw PiPedalStateException("A preset by that name already exists."); - } - for (size_t i = 0; i < presets_.size(); ++i) - { - if (presets_[i]->instanceId() ==instanceId) - { - presets_[i]->preset().name(name); - return true; - } - } - return false; + GETTER_SETTER(selectedInstanceId); + GETTER_SETTER_VEC(presets); + GETTER_SETTER(presetChanged); - } + DECLARE_JSON_MAP(PresetIndex); + }; - BankFileEntry &getItem(int64_t instanceId) + class BankFileEntry { - for (size_t i = 0; i < presets_.size(); ++i) + int64_t instanceId_; + Pedalboard preset_; + + public: + GETTER_SETTER(instanceId); + GETTER_SETTER_REF(preset); + + DECLARE_JSON_MAP(BankFileEntry); + }; + class BankFile + { + std::string name_; + int64_t nextInstanceId_ = 0; + int64_t selectedPreset_ = -1; + std::vector> presets_; + + public: + GETTER_SETTER(name); + GETTER_SETTER(nextInstanceId); + GETTER_SETTER(selectedPreset); + GETTER_SETTER_VEC(presets); + + void clear() { - if (presets_[i]->instanceId() == instanceId) - { - return *(presets_[i].get()); - } + nextInstanceId_ = 0; + presets_.clear(); + selectedPreset_ = -1; } - throw PiPedalArgumentException("Instance not found."); - } - int64_t deletePreset(int64_t instanceId) { - for (size_t i = 0; i < presets_.size(); ++i) + + void move(size_t from, size_t to) { - if (presets_[i]->instanceId() == instanceId) + if (from >= this->presets_.size()) { - presets_.erase(presets_.begin()+i); - int64_t newSelection; - if (i < presets_.size()) + throw std::invalid_argument("Argument out of range."); + } + if (to >= this->presets_.size()) + { + throw std::invalid_argument("Argument out of range."); + } + std::unique_ptr t = std::move(this->presets_[from]); + presets_.erase(presets_.begin() + from); + presets_.insert(presets_.begin() + to, std::move(t)); + } + void updateNextIndex() + { + int64_t t = 0; + for (size_t i = 0; i < this->presets_.size(); ++i) + { + int64_t instanceId = this->presets_[i]->instanceId(); + if (instanceId > t) + t = instanceId; + } + this->nextInstanceId_ = t; + } + int64_t addPreset(const Pedalboard &preset, int64_t afterItem = -1) + { + if (hasName(preset.name())) + { + throw PiPedalStateException("A preset by that name already exists."); + } + std::unique_ptr entry = std::make_unique(); + entry->instanceId(++nextInstanceId_); + entry->preset(preset); + int64_t instanceId = entry->instanceId(); + if (afterItem == -1) + { + this->presets_.push_back(std::move(entry)); + } + else + { + for (auto it = this->presets_.begin(); it != this->presets_.end(); ++it) { - newSelection = presets_[i]->instanceId(); - } else if (presets_.size() > 1) { - newSelection = presets_[0]->instanceId(); - } else { - // zero length? We can never have a zero-length bank. - // Add a default preset and make it the selected preset. - Pedalboard pedalboard = Pedalboard::MakeDefault(); - this->addPreset(pedalboard); - newSelection = presets_[0]->instanceId(); + if ((*it)->instanceId() == afterItem) + { + ++it; + this->presets_.insert(it, std::move(entry)); + break; + } } - if (instanceId == this->selectedPreset_) + } + return instanceId; + } + bool hasItem(int64_t instanceId) + { + for (size_t i = 0; i < presets_.size(); ++i) + { + if (presets_[i]->instanceId() == instanceId) { - this->selectedPreset_ = newSelection; + return true; } - return newSelection; } + return false; } - throw PiPedalStateException("Preset not found."); - } - - DECLARE_JSON_MAP(BankFile); -}; - -class BankIndexEntry { - int64_t instanceId_ = 0; - std::string name_; -public: - GETTER_SETTER(instanceId); - GETTER_SETTER_REF(name); - - DECLARE_JSON_MAP(BankIndexEntry); -}; - -class BankIndex { - int64_t selectedBank_ = 0; - int64_t nextInstanceId_ = 0; - std::vector entries_; -public: - GETTER_SETTER(selectedBank); - GETTER_SETTER_VEC(entries); - - DECLARE_JSON_MAP(BankIndex); - - bool hasName(const std::string&name) const { - for (size_t i = 0; i < this->entries_.size(); ++i) + bool hasName(const std::string &name) { - if (this->entries_[i].name() == name) return true; - } - return false; - } - - void move(size_t from, size_t to) - { - if (from >= this->entries_.size()) { - throw std::invalid_argument("Argument out of range."); - } - if (to >= this->entries_.size()) { - throw std::invalid_argument("Argument out of range."); - } - BankIndexEntry t = std::move(this->entries_[from]); - entries_.erase(entries_.begin()+from); - entries_.insert(entries_.begin()+to,std::move(t)); - } - - void clear() { - entries_.clear(); - selectedBank_ = 0; - } - int64_t addBank(int64_t afterId,const std::string& name) - { - BankIndexEntry bank; - bank.name(name); - bank.instanceId(++nextInstanceId_); - - for (size_t i = 0; i < this->entries_.size(); ++i) - { - if (entries_[i].instanceId() == afterId) + for (size_t i = 0; i < presets_.size(); ++i) { - entries_.insert(entries_.begin()+(i+1), bank); - return bank.instanceId(); + if (presets_[i]->preset().name() == name) + { + return true; + } } + return false; } - // else at the end. - entries_.push_back(bank); - return bank.instanceId(); - } - BankIndexEntry* getEntryByName(const std::string &name) - { - for (size_t i = 0; i < entries_.size(); ++i) + bool renamePreset(int64_t instanceId, const std::string &name) { - if (entries_[i].name() == name) + if (hasName(name)) { - return &entries_[i]; + throw PiPedalStateException("A preset by that name already exists."); } - } - return nullptr; - } - BankIndexEntry&getBankIndexEntry(int64_t instanceId) - { - for (size_t i = 0; i < entries_.size(); ++i) - { - if (entries_[i].instanceId() == instanceId) + for (size_t i = 0; i < presets_.size(); ++i) { - return entries_[i]; + if (presets_[i]->instanceId() == instanceId) + { + presets_[i]->preset().name(name); + return true; + } } + return false; } - throw PiPedalArgumentException("Bank not found."); - } - const BankIndexEntry&getBankIndexEntry(int64_t instanceId) const - { - for (size_t i = 0; i < entries_.size(); ++i) - { - if (entries_[i].instanceId() == instanceId) - { - return entries_[i]; - } - } - throw PiPedalArgumentException("Bank not found."); - } -}; + BankFileEntry &getItem(int64_t instanceId) + { + for (size_t i = 0; i < presets_.size(); ++i) + { + if (presets_[i]->instanceId() == instanceId) + { + return *(presets_[i].get()); + } + } + throw PiPedalArgumentException("Instance not found."); + } + int64_t deletePreset(int64_t instanceId) + { + for (size_t i = 0; i < presets_.size(); ++i) + { + if (presets_[i]->instanceId() == instanceId) + { + presets_.erase(presets_.begin() + i); + int64_t newSelection; + if (i < presets_.size()) + { + newSelection = presets_[i]->instanceId(); + } + else if (i == presets_.size() && i != 0) { + newSelection = presets_[i-1]->instanceId(); + + } + else if (presets_.size() >= 1) + { + newSelection = presets_[0]->instanceId(); + } + else + { + // zero length? We can never have a zero-length bank. + // Add a default preset and make it the selected preset. + Pedalboard pedalboard = Pedalboard::MakeDefault(); + this->addPreset(pedalboard); + newSelection = presets_[0]->instanceId(); + } + if (instanceId == this->selectedPreset_) + { + this->selectedPreset_ = newSelection; + } + return newSelection; + } + } + throw PiPedalStateException("Preset not found."); + } + + DECLARE_JSON_MAP(BankFile); + }; + + class BankIndexEntry + { + int64_t instanceId_ = 0; + std::string name_; + + public: + GETTER_SETTER(instanceId); + GETTER_SETTER_REF(name); + + DECLARE_JSON_MAP(BankIndexEntry); + }; + + class BankIndex + { + int64_t selectedBank_ = 0; + int64_t nextInstanceId_ = 0; + std::vector entries_; + + public: + GETTER_SETTER(selectedBank); + GETTER_SETTER_VEC(entries); + + DECLARE_JSON_MAP(BankIndex); + + bool hasName(const std::string &name) const + { + for (size_t i = 0; i < this->entries_.size(); ++i) + { + if (this->entries_[i].name() == name) + return true; + } + return false; + } + + void move(size_t from, size_t to) + { + if (from >= this->entries_.size()) + { + throw std::invalid_argument("Argument out of range."); + } + if (to >= this->entries_.size()) + { + throw std::invalid_argument("Argument out of range."); + } + BankIndexEntry t = std::move(this->entries_[from]); + entries_.erase(entries_.begin() + from); + entries_.insert(entries_.begin() + to, std::move(t)); + } + + void clear() + { + entries_.clear(); + selectedBank_ = 0; + } + int64_t addBank(int64_t afterId, const std::string &name) + { + BankIndexEntry bank; + bank.name(name); + bank.instanceId(++nextInstanceId_); + + for (size_t i = 0; i < this->entries_.size(); ++i) + { + if (entries_[i].instanceId() == afterId) + { + entries_.insert(entries_.begin() + (i + 1), bank); + return bank.instanceId(); + } + } + // else at the end. + entries_.push_back(bank); + return bank.instanceId(); + } + BankIndexEntry *getEntryByName(const std::string &name) + { + for (size_t i = 0; i < entries_.size(); ++i) + { + if (entries_[i].name() == name) + { + return &entries_[i]; + } + } + return nullptr; + } + BankIndexEntry &getBankIndexEntry(int64_t instanceId) + { + for (size_t i = 0; i < entries_.size(); ++i) + { + if (entries_[i].instanceId() == instanceId) + { + return entries_[i]; + } + } + throw PiPedalArgumentException("Bank not found."); + } + const BankIndexEntry &getBankIndexEntry(int64_t instanceId) const + { + for (size_t i = 0; i < entries_.size(); ++i) + { + if (entries_[i].instanceId() == instanceId) + { + return entries_[i]; + } + } + throw PiPedalArgumentException("Bank not found."); + } + }; #undef GETTER_SETTER #undef GETTER_SETTER_VEC #undef GETTER_SETTER_REF } - - diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index 214168c..704756f 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -562,7 +562,6 @@ void InstallAudioService() #endif } - static bool IsP2pServiceEnabled() { WifiDirectConfigSettings settings; @@ -742,7 +741,7 @@ void SetVarPermissions( catch (const std::exception &e) { // spurious errors on pulse config files. - // std::cout << "Error: failed to set permissions on file " << entry.path() << std::endl; + // std::cout << "Error: failed to set permissions on file " << entry.path() << std::endl; } } } @@ -926,26 +925,26 @@ void InstallPgpKey() } } - -static uint32_t autoSelectPorts[] = { +static uint32_t autoSelectPorts[] = { 80, - 81, // unofficial alternate - 8080, // unofficial alternate - 8081, // unofficial alternate - 8008, // unofficial alternate - 83 // Not official at all. - }; + 81, // unofficial alternate + 8080, // unofficial alternate + 8081, // unofficial alternate + 8008, // unofficial alternate + 83 // Not official at all. +}; - - -static bool isPortInUse(int port) { +static bool isPortInUse(int port) +{ int sockfd = socket(AF_INET, SOCK_STREAM, 0); - if (sockfd < 0) { + if (sockfd < 0) + { throw std::runtime_error(SS("Socket creation failed: " << strerror(errno))); } - Finally ff{[sockfd]() { - close(sockfd); - }}; + Finally ff{[sockfd]() + { + close(sockfd); + }}; struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; @@ -953,23 +952,27 @@ static bool isPortInUse(int port) { addr.sin_port = htons(port); // Try to bind to the port - int result = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)); + int result = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)); - if (result < 0) { - if (errno == EADDRINUSE) { - return true; // Port is in use - } else { + if (result < 0) + { + if (errno == EADDRINUSE) + { + return true; // Port is in use + } + else + { cout << "Warning: Unexpected error binding socket to port " << port << ": " << strerror(errno) << endl; return true; } } - return false; // Port is free + return false; // Port is free } - -static std::string AutoSelectPort() { - constexpr size_t MAX_PORT = sizeof(autoSelectPorts)/sizeof(autoSelectPorts[0]); +static std::string AutoSelectPort() +{ + constexpr size_t MAX_PORT = sizeof(autoSelectPorts) / sizeof(autoSelectPorts[0]); int32_t port = -1; for (size_t i = 0; i < MAX_PORT; ++i) @@ -980,29 +983,32 @@ static std::string AutoSelectPort() { break; } } - if (port == -1) { + if (port == -1) + { cout << "Warning: Can't find an available HTTP port. Setting to port 80 anyway." << endl; port = 80; - } else if (port != 80) + } + else if (port != 80) { - cout << "Warning: Port 80 is already in use. Using port " << port << " instead." << endl; + cout << "Warning: Port 80 is already in use. Using port " << port << " instead." << endl; } return SS(port); - } - bool SetWebServerPort(std::string portOption) { - try { + try + { auto nPos = portOption.find_last_of(':'); std::string strPort; if (nPos != std::string::npos) { - strPort = portOption.substr(nPos+1); - } else { + strPort = portOption.substr(nPos + 1); + } + else + { strPort = portOption; } std::istringstream ss(strPort); @@ -1021,7 +1027,8 @@ bool SetWebServerPort(std::string portOption) StartService(true); cout << "PiPedal web server is listening on port " << iPort << endl; - } catch (const std::exception&e) + } + catch (const std::exception &e) { cout << "Error: " << e.what() << endl; return false; @@ -1031,7 +1038,8 @@ bool SetWebServerPort(std::string portOption) void CheckPreemptDynamicConfig() { - try { + try + { BootConfig bootConfig; if (bootConfig.KernelType() == "PREEMPT_DYNAMIC") @@ -1044,13 +1052,12 @@ void CheckPreemptDynamicConfig() cout << endl; } } - - } catch(const std::exception& /* ignore */) + } + catch (const std::exception & /* ignore */) { } } - void Install(const fs::path &programPrefix, const std::string endpointAddress) { cout << "Configuring pipedal" << endl; @@ -1058,10 +1065,10 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) if (!UsingNetworkManager()) { cout << "Warning: The current OS is not using the NetworkManager network stack." << endl; - cout << " The PiPedal Auto-Hotspot feature will be disabled." << endl << endl; + cout << " The PiPedal Auto-Hotspot feature will be disabled." << endl + << endl; cout << " Consult PiPedal documentation to find out how to correct this " << endl; cout << " issue on Ubuntu Server." << endl; - } try { @@ -1301,10 +1308,11 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress) if (endpointAddress.starts_with(ADDR_ANY)) { cout << "PiPedal web server is listening on port " << endpointAddress.substr(ADDR_ANY.length()) << endl; - } else { + } + else + { cout << "PiPedal web server is listening on port " << endpointAddress << endl; } - } static std::string GetCurrentWebServicePort() @@ -1494,25 +1502,27 @@ void ListValidCountryCodes() if (ccs.size() == 0) { cout << "No regulatory domains found." << endl; - } else { - using pair = std::pair; + } + else + { + using pair = std::pair; std::vector list; - for (auto &cc: ccs) + for (auto &cc : ccs) { - list.push_back(pair(cc.first,cc.second)); + list.push_back(pair(cc.first, cc.second)); } auto collator = Locale::GetInstance()->GetCollator(); auto compare = [&collator]( - pair &left, - pair &right) + pair &left, + pair &right) { return collator->Compare( - left.second, right.second) < 0; + left.second, right.second) < 0; }; std::sort(list.begin(), list.end(), compare); - for (const auto &entry: list) + for (const auto &entry : list) { cout << entry.first << " - " << entry.second << endl; } @@ -1521,12 +1531,15 @@ void ListValidCountryCodes() static std::string ProcessPortOption(std::string portOption) { - if (portOption == "" || portOption == "0") // allow testing of port autodetect. + if (portOption == "" || portOption == "0") // allow testing of port autodetect. { std::string t = GetCurrentWebServicePort(); - if (portOption != "0") {// allows easy debugging of port AutoSelectPort. + if (portOption != "0") + { // allows easy debugging of port AutoSelectPort. portOption = t; - } else { + } + else + { portOption = ""; } if (portOption == "") @@ -1569,7 +1582,7 @@ int main(int argc, char **argv) bool noWifi = false; parser.AddOption("--nosudo", &nosudo); // strictly a debugging aid. Run without sudo, until we fail because of permissions. - parser.AddOption("--list-wifi-country-codes",&list_wifi_country_codes); + parser.AddOption("--list-wifi-country-codes", &list_wifi_country_codes); parser.AddOption("--install", &install); parser.AddOption("--uninstall", &uninstall); parser.AddOption("--stop", &stop); @@ -1589,15 +1602,17 @@ int main(int argc, char **argv) parser.AddOption("--alsa-devices", &alsaDevices); parser.AddOption("--alsa-device", &alsaDevice); parser.AddOption("--fix-permissions", &fix_permissions); + try { parser.Parse(argc, (const char **)argv); int actionCount = - (!alsaDevice.empty()) + alsaDevices + help + get_current_port + install + - uninstall + stop + start + enable + disable + enable_hotspot + - disable_hotspot + restart + enable_p2p + disable_p2p + list_p2p_channels + - fix_permissions + list_wifi_country_codes; + (!alsaDevice.empty()) + alsaDevices + help + get_current_port + install + + uninstall + stop + start + enable + disable + enable_hotspot + + disable_hotspot + restart + enable_p2p + disable_p2p + list_p2p_channels + + fix_permissions + list_wifi_country_codes; + if (actionCount == 0 && portOption.length() != 0) { ++actionCount; @@ -1615,6 +1630,12 @@ int main(int argc, char **argv) throw std::runtime_error("No action provided."); } + + if (portOption.length() != 0 && !install) + { + throw std::runtime_error("The --port option can only be specified in conjunction with the --install option."); + } + if ((!enable_p2p) && (!enable_hotspot) && (!list_p2p_channels)) { if (parser.Arguments().size() != 0) @@ -1744,7 +1765,6 @@ int main(int argc, char **argv) Install(prefix, portOption); FileSystemSync(); - } catch (const std::exception &e) { diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 590b27d..89324c9 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -153,12 +153,13 @@ bool Pedalboard::SetControlValue(int64_t pedalItemId, const std::string &symbol, return item->SetControlValue(symbol,value); } -bool Pedalboard::SetItemTitle(int64_t pedalItemId, const std::string &title) +bool Pedalboard::SetItemTitle(int64_t pedalItemId, const std::string &title, const std::string&iconColor) { PedalboardItem*item = GetItem(pedalItemId); if (!item) return false; - if (item->title() == title) return false; // no change. + if (item->title() == title && item->iconColor() == iconColor) return false; // no change. item->title(title); + item->iconColor(iconColor); return true; } @@ -532,6 +533,7 @@ JSON_MAP_BEGIN(PedalboardItem) JSON_MAP_REFERENCE(PedalboardItem,pathProperties) JSON_MAP_REFERENCE(PedalboardItem,title) JSON_MAP_REFERENCE(PedalboardItem,useModUi) + JSON_MAP_REFERENCE(PedalboardItem,iconColor) JSON_MAP_END() diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 4363d3c..995ddcb 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -106,6 +106,7 @@ public: std::map pathProperties_; std::string title_; bool useModUi_ = false; + std::string iconColor_; // non persistent state. PropertyMap patchProperties; @@ -135,6 +136,7 @@ public: GETTER_SETTER(stateUpdateCount) GETTER_SETTER_REF(lv2State) GETTER_SETTER_REF(title) + GETTER_SETTER_REF(iconColor) GETTER_SETTER(useModUi) Lv2PluginState&lv2State() { return lv2State_; } // non-const version. @@ -232,7 +234,7 @@ public: static constexpr int64_t INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume. static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output volume. bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value); - bool SetItemTitle(int64_t pedalItemId, const std::string &title); + bool SetItemTitle(int64_t pedalItemId, const std::string &title, const std::string&iconColor); bool SetItemEnabled(int64_t pedalItemId, bool enabled); bool SetItemUseModUi(int64_t pedalItemId, bool enabled); void SetCurrentSnapshotModified(bool modified); diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index e1616ac..01887e5 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -33,6 +33,7 @@ #include #include #include +#include using namespace pipedal; @@ -59,7 +60,7 @@ void PrintHelp() pp << Indent(0) << "Syntax\n\n"; pp << Indent(2) << "pipedal_latency_test [] []\n\n"; pp << "where is the name of an ALSA capture device and is the name of a playback device. " - "If is omitted, the input device will be used for both capture and playback. Typically the device names start with 'hw:'.\n\n"; + "If is omitted, the input device will be used for both capture and playback. Typically the device names start with 'hw:'.\n\n"; pp << Indent(0) << "Options\n\n"; pp << Indent(15); @@ -85,13 +86,13 @@ void PrintHelp() << "PiPedal Latency Tester measures internal buffer delays as well as operating system and " << "signal delays in hardware peripherals. Latency figures will therefore be somewhat higher than " << "most reported latency figures which typically only include internal buffer delays.\n\n"; - - pp - << "The tests run over a variety of buffer sizes. A nominal compute load is provided in order to put some " - "stress on the audio system.\n\n" - << "You may need to stop the pipedald audio service in order to access the ALSA device:\n\n" - << Indent(6) << "sudo systemctl stop pipedald\n\n"; + pp + << "The tests run over a variety of buffer sizes. A nominal compute load is provided in order to put some " + "stress on the audio system.\n\n" + + << "You may need to stop the pipedald audio service in order to access the ALSA device:\n\n" + << Indent(6) << "sudo systemctl stop pipedald\n\n"; pp << Indent(0) << "Examples\n\n"; pp << Indent(2) << "pipedal_latency_test --list\n\n"; @@ -123,7 +124,6 @@ void ListDevices() } } - using ChannelsT = std::vector; class AlsaTester : private AudioDriverHost @@ -169,12 +169,13 @@ public: delete[] inputBuffers; delete[] outputBuffers; } - std::vector SelectChannels(const std::vector&available, const std::vector& selection) + std::vector SelectChannels(const std::vector &available, const std::vector &selection) { - if (selection.size() == 0) return available; + if (selection.size() == 0) + return available; std::vector result; - for (int sel: selection) + for (int sel : selection) { if (sel < 0 || sel >= available.size()) { @@ -195,14 +196,13 @@ public: JackConfiguration jackConfiguration; jackConfiguration.AlsaInitialize(serverSettings); - auto & availableInputs = jackConfiguration.inputAudioPorts(); - auto & availableOutputs = jackConfiguration.outputAudioPorts(); - + auto &availableInputs = jackConfiguration.inputAudioPorts(); + auto &availableOutputs = jackConfiguration.outputAudioPorts(); std::vector inputAudioPorts, outputAudioPorts; - inputAudioPorts = SelectChannels(availableInputs,this->inputChannels); - outputAudioPorts = SelectChannels(availableOutputs,this->outputChannels); + inputAudioPorts = SelectChannels(availableInputs, this->inputChannels); + outputAudioPorts = SelectChannels(availableOutputs, this->outputChannels); JackChannelSelection channelSelection( inputAudioPorts, outputAudioPorts, @@ -360,7 +360,6 @@ public: } virtual void OnAudioTerminated() { - } virtual void OnProcess(size_t nFrames) { @@ -413,7 +412,7 @@ TestResult RunLatencyTest( const ChannelsT &outputChannels, uint32_t sampleRate, int bufferSize, int buffers) { - AlsaTester tester(inputDeviceId, outputDeviceId, inputChannels, outputChannels, sampleRate, bufferSize, buffers); + AlsaTester tester(inputDeviceId, outputDeviceId, inputChannels, outputChannels, sampleRate, bufferSize, buffers); return tester.Test(); } @@ -432,6 +431,28 @@ static std::string overheadDisplay(float value) return s.str(); } +static bool testRealtimePriorityPrivileges() +{ + + struct sched_param currentParam; + int currentPolicy = sched_getscheduler(0); + if (currentPolicy == -1) { + return false; + } + if (sched_getparam(0, ¤tParam) != 0) { + return false; + } + struct sched_param param; + param.sched_priority = 85; + if (sched_setscheduler(0, SCHED_RR, ¶m) != 0) + { + return false; + } + // Restore normal priority + sched_setscheduler(0, currentPolicy, ¤tParam); + return true; +} + void RunLatencyTest( const std::string &inputDeviceId, const std::string &outputDeviceId, @@ -439,8 +460,20 @@ void RunLatencyTest( const ChannelsT &outputChannels, uint32_t sampleRate) { + PrettyPrinter pp; - pp << "Input: " << inputDeviceId << " Output: " << outputDeviceId << " Rate: " << sampleRate << "\n\n"; + + if (!testRealtimePriorityPrivileges()) + { + pp << "Unable to enable realtime scheduling. Add your user id to the pipedal_d group to fix this problem:" << "\n\n"; + pp.AddIndent(4); + pp << "sudo usermod -a -G pipedal_d " << "\n\n"; + pp.AddIndent(-4); + pp << "You will need to log out or reboot your system in order for the change to take effect.\n\n"; + throw std::runtime_error("Unable to set relatime thread priority."); + } + + pp << "Input: " << inputDeviceId << " Output: " << outputDeviceId << " Rate: " << sampleRate << "\n\n"; const int SIZE_COLUMN_WIDTH = 8; const int BUFFERS_COLUMN_WIDTH = 20; @@ -467,7 +500,7 @@ void RunLatencyTest( for (auto bufferCount : bufferCounts) { - auto result = RunLatencyTest(inputDeviceId, outputDeviceId, inputChannels,outputChannels, sampleRate, bufferSize, bufferCount); + auto result = RunLatencyTest(inputDeviceId, outputDeviceId, inputChannels, outputChannels, sampleRate, bufferSize, bufferCount); pp.Column(column); column += BUFFERS_COLUMN_WIDTH; @@ -495,8 +528,7 @@ void RunLatencyTest( } } - -ChannelsT ParseChannels(const std::string&channels) +ChannelsT ParseChannels(const std::string &channels) { ChannelsT result; std::stringstream s(channels); @@ -504,9 +536,11 @@ ChannelsT ParseChannels(const std::string&channels) while (true) { int c = s.peek(); - if (c == -1) break; + if (c == -1) + break; - if (c == ',') { + if (c == ',') + { s.get(); c = s.peek(); } @@ -518,14 +552,12 @@ ChannelsT ParseChannels(const std::string&channels) while (s.peek() >= '0' && s.peek() <= '9') { c = s.get(); - v = v*10 + c-'0'; + v = v * 10 + c - '0'; } result.push_back(v); } return result; - - } int main(int argc, const char **argv) @@ -567,7 +599,7 @@ std: inputChannels = ParseChannels(strInputChannels); outputChannels = ParseChannels(strOutputChannels); - std::string inDev = parser.Arguments()[0]; + std::string inDev = parser.Arguments()[0]; std::string outDev = parser.Arguments().size() == 2 ? parser.Arguments()[1] : inDev; RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate); } diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index b36ba98..831de12 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -887,7 +887,7 @@ int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string & return presetId; } -int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &name, int64_t saveAfterInstanceId) +int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId,const std::string &name, int64_t saveAfterInstanceId) { std::lock_guard guard{mutex}; @@ -897,7 +897,7 @@ int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &n UpdateVst3Settings(pedalboard); pedalboard.name(name); - int64_t result = storage.SaveCurrentPresetAs(pedalboard, name, saveAfterInstanceId); + int64_t result = storage.SaveCurrentPresetAs(pedalboard, bankInstanceId, name, saveAfterInstanceId); FirePresetsChanged(clientId); return result; } @@ -1174,12 +1174,12 @@ int64_t PiPedalModel::DeleteBank(int64_t clientId, int64_t instanceId) return newSelection; } -int64_t PiPedalModel::DeletePreset(int64_t clientId, int64_t instanceId) +int64_t PiPedalModel::DeletePresets(int64_t clientId, const std::vector &presetInstanceIds) { std::lock_guard guard{mutex}; int64_t oldSelection = storage.GetCurrentPresetId(); - int64_t newSelection = storage.DeletePreset(instanceId); - this->FirePresetsChanged(clientId); // fire now. + int64_t newSelection = storage.DeletePresets(presetInstanceIds); + this->FirePresetsChanged(clientId); // fire BEFORE we load a new preset. if (oldSelection != newSelection) { this->LoadPreset( @@ -1745,15 +1745,18 @@ void PiPedalModel::SendSetPatchProperty( { std::shared_ptr pluginInfo = GetPluginInfo(pedalboardItem->uri_); auto pipedalUi = pluginInfo->piPedalUI(); - auto fileProperty = pipedalUi->GetFileProperty(propertyUri); - if (fileProperty) + if (pipedalUi) { + auto fileProperty = pipedalUi->GetFileProperty(propertyUri); + if (fileProperty) + { - json_variant abstractPath = pluginHost.AbstractPath(value); - std::string atomString = abstractPath.to_string(); - pedalboardItem->pathProperties_[propertyUri] = atomString; + json_variant abstractPath = pluginHost.AbstractPath(value); + std::string atomString = abstractPath.to_string(); + pedalboardItem->pathProperties_[propertyUri] = atomString; + } + this->SetPresetChanged(clientId, true); } - this->SetPresetChanged(clientId, true); } LV2_Atom *atomValue = atomConverter.ToAtom(value); @@ -3226,10 +3229,10 @@ void PiPedalModel::MoveAudioFile( AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory); dir->MoveAudioFile(directory, fromPosition, toPosition); } -void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title) +void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string &colorKey) { std::lock_guard lock(mutex); - if (!this->pedalboard.SetItemTitle(instanceId, title)) + if (!this->pedalboard.SetItemTitle(instanceId, title, colorKey)) { return; } @@ -3254,3 +3257,28 @@ bool PiPedalModel::HasTone3000Auth() const { return storage.GetTone3000Auth() != ""; } + +std::vector PiPedalModel::RequestBankPresets(int64_t bankInstanceId) +{ + std::lock_guard lock(mutex); + + return storage.RequestBankPresets(bankInstanceId); + +} + +int64_t PiPedalModel::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets) +{ + std::lock_guard lock(mutex); + uint64_t lastAdded = storage.ImportPresetsFromBank(bankInstanceId, presets); + + FirePresetsChanged(-1); + return lastAdded; + +} +int64_t PiPedalModel::CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets) +{ + std::lock_guard lock(mutex); + uint64_t lastAdded = storage.CopyPresetsToBank(bankInstanceId, presets); + return lastAdded; + +} diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index ebf3eae..897f524 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -378,13 +378,14 @@ namespace pipedal int64_t UploadPreset(const BankFile &bankFile, int64_t uploadAfter = -1); void UploadPluginPresets(const PluginPresets &pluginPresets); void SaveCurrentPreset(int64_t clientId); - int64_t SaveCurrentPresetAs(int64_t clientId, const std::string &name, int64_t saveAfterInstanceId = -1); + int64_t SaveCurrentPresetAs(int64_t clientId, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId = -1); int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name); + void LoadPreset(int64_t clientId, int64_t instanceId); bool UpdatePresets(int64_t clientId, const PresetIndex &presets); void UpdatePluginPresets(const PluginUiPresets &pluginPresets); - int64_t DeletePreset(int64_t clientId, int64_t instanceId); + int64_t DeletePresets(int64_t clientId, const std::vector &presetInstanceIds); bool RenamePreset(int64_t clientId, int64_t instanceId, const std::string &name); int64_t CopyPreset(int64_t clientId, int64_t fromIndex, int64_t toIndex); uint64_t CopyPluginPreset(const std::string &pluginUri, uint64_t presetId); @@ -486,13 +487,16 @@ namespace pipedal int32_t from, int32_t to); - void SetPedalboardItemTitle(int64_t instanceId, const std::string &title); + void SetPedalboardItemTitle(int64_t instanceId, const std::string &title, const std::string&iconColor); void SetTone3000Auth(const std::string &apiKey); bool HasTone3000Auth() const; void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId); + std::vector RequestBankPresets(int64_t bankInstanceId); + int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); + int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 83d1bfe..a3bf46b 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -46,6 +46,38 @@ using namespace std; using namespace pipedal; +class CopyPresetsToBankBody { + public: + int64_t bankInstanceId_; + std::vector presets_; + DECLARE_JSON_MAP(CopyPresetsToBankBody); +}; +JSON_MAP_BEGIN(CopyPresetsToBankBody) +JSON_MAP_REFERENCE(CopyPresetsToBankBody, bankInstanceId) +JSON_MAP_REFERENCE(CopyPresetsToBankBody, presets) +JSON_MAP_END() + + +class ImportPresetsFromBankBody { + public: + int64_t bankInstanceId_; + std::vector presets_; + DECLARE_JSON_MAP(ImportPresetsFromBankBody); +}; +JSON_MAP_BEGIN(ImportPresetsFromBankBody) +JSON_MAP_REFERENCE(ImportPresetsFromBankBody, bankInstanceId) +JSON_MAP_REFERENCE(ImportPresetsFromBankBody, presets) +JSON_MAP_END() + +class RequestBankPresetsBody { +public: + int64_t bankInstanceId_; + DECLARE_JSON_MAP(RequestBankPresetsBody); +}; +JSON_MAP_BEGIN(RequestBankPresetsBody) +JSON_MAP_REFERENCE(RequestBankPresetsBody, bankInstanceId) +JSON_MAP_END() + class PathPatchPropertyChangedBody { public: @@ -179,12 +211,14 @@ class SetPedalboardItemTitleBody public: uint64_t instanceId_; std::string title_; + std::string colorKey_; DECLARE_JSON_MAP(SetPedalboardItemTitleBody); }; JSON_MAP_BEGIN(SetPedalboardItemTitleBody) JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, instanceId) JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, title) +JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, colorKey) JSON_MAP_END() @@ -334,6 +368,7 @@ class SaveCurrentPresetAsBody { public: int64_t clientId_ = -1; + int64_t bankInstanceId_ = -1; std::string name_; int64_t saveAfterInstanceId_ = -1; @@ -341,6 +376,7 @@ public: }; JSON_MAP_BEGIN(SaveCurrentPresetAsBody) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, clientId) +JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, bankInstanceId) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, name) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, saveAfterInstanceId) JSON_MAP_END() @@ -1312,7 +1348,7 @@ public: { SaveCurrentPresetAsBody body; pReader->read(&body); - int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.name_, body.saveAfterInstanceId_); + int64_t result = this->model.SaveCurrentPresetAs(this->clientId, body.bankInstanceId_,body.name_, body.saveAfterInstanceId_); Reply(replyTo, "saveCurrentPresetsAs", result); } else if (message == "setSelectedPedalboardPlugin") @@ -1445,12 +1481,12 @@ public: model.RequestShutdown(true); this->Reply(replyTo, "restart"); } - else if (message == "deletePresetItem") + else if (message == "deletePresetItems") { - int64_t instanceId = 0; - pReader->read(&instanceId); - int64_t result = model.DeletePreset(this->clientId, instanceId); - this->Reply(replyTo, "deletePresetItem", result); + std::vector items; + pReader->read(&items); + int64_t result = model.DeletePresets(this->clientId, items); + this->Reply(replyTo, "deletePresetItems", result); } else if (message == "deleteBankItem") { @@ -1569,7 +1605,7 @@ public: { SetPedalboardItemTitleBody body; pReader->read(&body); - model.SetPedalboardItemTitle(body.instanceId_, body.title_); + model.SetPedalboardItemTitle(body.instanceId_, body.title_, body.colorKey_); } else if (message == "getPatchProperty") { @@ -1792,6 +1828,25 @@ public: { std::vector result = model.GetAlsaSequencerPorts(); this->Reply(replyTo,"getAlsaSequencerPorts", result); + } else if (message == "requestBankPresets") { + + RequestBankPresetsBody args; + pReader->read(&args); + auto result = this->model.RequestBankPresets(args.bankInstanceId_); + this->Reply(replyTo,"requestBankPresets",result); + + } + else if (message == "importPresetsFromBank") { + ImportPresetsFromBankBody args; + pReader->read(&args); + auto result = this->model.ImportPresetsFromBank(args.bankInstanceId_, args.presets_); + this->Reply(replyTo,"importPresetsFromBank",result); + } + else if (message == "copyPresetsToBank") { + CopyPresetsToBankBody args; + pReader->read(&args); + auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_); + this->Reply(replyTo,"copyPresetsToBank",result); } else { diff --git a/src/PiPedalUI.cpp b/src/PiPedalUI.cpp index a635073..e04fd65 100644 --- a/src/PiPedalUI.cpp +++ b/src/PiPedalUI.cpp @@ -538,7 +538,8 @@ UiFrequencyPlot::UiFrequencyPlot(PluginHost*pHost, const LilvNode*node, this->xRight_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__xRight,22000); this->yTop_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__yTop,5); this->yBottom_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__yBottom,-35); - this->xLog_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__xLog,-35); + this->xLog_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__xLog,1); + this->yDb_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__yDb,1); this->width_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__width,176); } @@ -727,6 +728,7 @@ JSON_MAP_REFERENCE(UiFrequencyPlot, portGroup) JSON_MAP_REFERENCE(UiFrequencyPlot, xLeft) JSON_MAP_REFERENCE(UiFrequencyPlot, xRight) JSON_MAP_REFERENCE(UiFrequencyPlot, xLog) +JSON_MAP_REFERENCE(UiFrequencyPlot, yDb) JSON_MAP_REFERENCE(UiFrequencyPlot, yTop) JSON_MAP_REFERENCE(UiFrequencyPlot, yBottom) JSON_MAP_REFERENCE(UiFrequencyPlot, width) diff --git a/src/PiPedalUI.hpp b/src/PiPedalUI.hpp index 53f274d..2b532ba 100644 --- a/src/PiPedalUI.hpp +++ b/src/PiPedalUI.hpp @@ -63,6 +63,7 @@ #define PIPEDAL_UI__xLeft PIPEDAL_UI_PREFIX "xLeft" #define PIPEDAL_UI__xRight PIPEDAL_UI_PREFIX "xRight" #define PIPEDAL_UI__xLog PIPEDAL_UI_PREFIX "xLog" +#define PIPEDAL_UI__yDb PIPEDAL_UI_PREFIX "yDb" #define PIPEDAL_UI__yTop PIPEDAL_UI_PREFIX "yTop" #define PIPEDAL_UI__yBottom PIPEDAL_UI_PREFIX "yBottom" #define PIPEDAL_UI__width PIPEDAL_UI_PREFIX "width" @@ -196,6 +197,7 @@ namespace pipedal float yTop_ = 5; float yBottom_ = -30; bool xLog_ = true; + bool yDb_ = true; float width_ = 60; public: @@ -213,6 +215,7 @@ namespace pipedal float yTop() const { return yTop_; } float yBottom() const { return yBottom_; } float width() const { return width_; } + bool yDb() const { return yDb_; } public: DECLARE_JSON_MAP(UiFrequencyPlot); diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 8aff3dd..d4d7bd2 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -165,6 +165,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld) pipedalUI__yTop = lilv_new_uri(pWorld, PIPEDAL_UI__yTop); pipedalUI__yBottom = lilv_new_uri(pWorld, PIPEDAL_UI__yBottom); pipedalUI__xLog = lilv_new_uri(pWorld, PIPEDAL_UI__xLog); + pipedalUI__yDb = lilv_new_uri(pWorld, PIPEDAL_UI__yDb); pipedalUI__width = lilv_new_uri(pWorld, PIPEDAL_UI__width); pipedalUI__graphicEq = lilv_new_uri(pWorld, PIPEDAL_UI__graphicEq); diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index c0edb66..2712bad 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -810,6 +810,7 @@ namespace pipedal AutoLilvNode pipedalUI__xLeft; AutoLilvNode pipedalUI__xRight; AutoLilvNode pipedalUI__xLog; + AutoLilvNode pipedalUI__yDb; AutoLilvNode pipedalUI__yTop; AutoLilvNode pipedalUI__yBottom; AutoLilvNode pipedalUI__width; diff --git a/src/Storage.cpp b/src/Storage.cpp index d5b65b1..b6979ad 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -225,7 +225,6 @@ static void CopyDirectory(const std::filesystem::path &source, const std::filesy } } - void Storage::MaybeCopyDefaultPresets() { auto presetsDirectory = this->GetPresetsDirectory(); @@ -239,13 +238,17 @@ void Storage::MaybeCopyDefaultPresets() } static void removeFileNoThrow(const std::filesystem::path &path) -{ - try { +{ + try + { fs::remove(path); - } catch (const std::exception&) + } + catch (const std::exception &) { } } + + void Storage::UpgradeFactoryPresets() { auto presetsDirectory = this->GetPresetsDirectory(); @@ -520,7 +523,6 @@ void Storage::SavePluginPresetIndex() } } - void Storage::SaveBankIndex() { pipedal::ofstream_synced os; @@ -653,17 +655,162 @@ void Storage::SaveCurrentPreset(const Pedalboard &pedalboard) item.preset(pedalboard); SaveCurrentBank(); } -int64_t Storage::SaveCurrentPresetAs(const Pedalboard &pedalboard, const std::string &name, int64_t saveAfterInstanceId) +int64_t Storage::SaveCurrentPresetAs(const Pedalboard &pedalboard, int64_t bankInstanceId, const std::string &name, int64_t saveAfterInstanceId) { Pedalboard newPedalboard = pedalboard; newPedalboard.name(name); - int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId); - currentBank.selectedPreset(newInstanceId); - SaveCurrentBank(); - return newInstanceId; + if (bankInstanceId == this->bankIndex.selectedBank()) + { + int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId); + currentBank.selectedPreset(newInstanceId); + SaveCurrentBank(); + return newInstanceId; + } + else + { + auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); + + try + { + BankFile bankFile; + LoadBankFile(indexEntry.name(), &(bankFile)); + int64_t newInstanceId = bankFile.addPreset(newPedalboard, -1); + SaveBankFile(indexEntry.name(), bankFile); + return -1; + } + catch (const std::exception &e) + { + throw std::logic_error(SS("Bank file corrupted. " << e.what() << "(" << GetBankFileName(indexEntry.name()) << ")")); + } + } } +static std::string stripNumericSuffix(const std::string &name) { + // remove (digit*) from the end of the string. + // Find the last '(' character + size_t pos = name.find_last_of('('); + if (pos == std::string::npos) { + return name; + } + + // Check if everything between '(' and ')' is digits + size_t len = name.length()-1; + if (name[len] != ')') + { + return name; + } + bool allDigits = true; + for (size_t i = pos + 1; i < len - 1; ++i) { + if (name[i] < '0' || name[i] > '9') { + allDigits = false; + break; + } + } + + if (!allDigits) { + return name; + } + + // Remove trailing spaces before the '(' + while (pos > 0 && name[pos - 1] == ' ') { + --pos; + } + + return name.substr(0, pos); +} +static std::string makeUniqueName(const std::string &name, const std::set&existingNames) +{ + if (!existingNames.contains(name)) + { + return name; + } + std::string baseName = stripNumericSuffix(name); + + size_t i = 2; + while (true) { + std::string newName = SS(baseName << " (" << i << ")"); + if (!existingNames.contains(newName)) { + return newName; + } + ++i; + } +} +int64_t Storage::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets) +{ + if (bankIndex.selectedBank() == bankInstanceId) { + throw std::runtime_error("Can't import to self."); + } + std::set presetsSet { presets.begin(), presets.end()}; + auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); + + std::set existingNames; + + for (auto&preset: this->currentBank.presets()) { + existingNames.insert(preset->preset().name()); + } + BankFile bankFile; + LoadBankFile(indexEntry.name(),&bankFile); + int64_t lastPresetId = -1; + for (auto &presetEntry: bankFile.presets()) { + if (presetsSet.contains(presetEntry->instanceId())) { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + existingNames.insert(uniqueName); + Pedalboard t = presetEntry->preset(); + t.name(uniqueName); + lastPresetId = this->currentBank.addPreset(t); + } + } + SaveCurrentBank(); + return lastPresetId; +} +int64_t Storage::CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets) +{ + if (bankIndex.selectedBank() == bankInstanceId) { + throw std::runtime_error("Can't copy to self."); + } + + auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); + BankFile bankFile; + LoadBankFile(indexEntry.name(),&bankFile); + + std::set presetsSet { presets.begin(), presets.end()}; + + std::set existingNames; + + for (auto&preset: bankFile.presets()) { + existingNames.insert(preset->preset().name()); + } + for (auto &presetEntry: this->currentBank.presets()) { + if (presetsSet.contains(presetEntry->instanceId())) { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + existingNames.insert(uniqueName); + Pedalboard t = presetEntry->preset(); + t.name(uniqueName); + bankFile.addPreset(t); + } + } + SaveBankFile(indexEntry.name(),bankFile); + return -1; +} + +std::vector Storage::RequestBankPresets(int64_t bankInstanceId) +{ + auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); + + std::vector result; + BankFile bankFile; + LoadBankFile(indexEntry.name(),&bankFile); + for (auto &preset: bankFile.presets()) { + result.push_back( + PresetIndexEntry(preset->instanceId(),preset->preset().name()) + ); + } + return result; +} + + + void Storage::SetPresetIndex(const PresetIndex &presets) { // painful because we must move unique_ptrs. @@ -734,9 +881,12 @@ Pedalboard Storage::GetPreset(int64_t instanceId) const throw PiPedalException("Not found."); } -int64_t Storage::DeletePreset(int64_t presetId) +int64_t Storage::DeletePresets(const std::vector &presetInstanceIds) { - int64_t newSelection = currentBank.deletePreset(presetId); + int64_t newSelection = currentBank.selectedPreset(); + for (auto presetId: presetInstanceIds) { + newSelection = currentBank.deletePreset(presetId); + } SaveCurrentBank(); return newSelection; } @@ -826,25 +976,30 @@ int64_t Storage::CreateNewPreset() } } newPedalboard.name(name); - return this->currentBank.addPreset(newPedalboard, -1); + auto t = this->currentBank.addPreset(newPedalboard, -1); + SaveCurrentBank(); + return t; } int64_t Storage::CopyPreset(int64_t fromId, int64_t toId) { auto &fromItem = this->currentBank.getItem(fromId); + int64_t result; if (toId == -1) { Pedalboard newPedalboard = fromItem.preset(); std::string name = GetPresetCopyName(fromItem.preset().name()); newPedalboard.name(name); - return this->currentBank.addPreset(newPedalboard, fromId); + result = this->currentBank.addPreset(newPedalboard, fromId); } else { auto &toItem = this->currentBank.getItem(toId); toItem.preset(fromItem.preset()); - return toId; + result = toId; } + SaveCurrentBank(); + return result; } void Storage::SetJackChannelSelection(const JackChannelSelection &channelSelection) @@ -2861,6 +3016,7 @@ std::string Storage::FromAbstractPathJson(const std::string &pathJson) return v.to_string(); } + JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) diff --git a/src/Storage.hpp b/src/Storage.hpp index e31212c..765bc6f 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -149,7 +149,7 @@ public: int64_t GetBankByMidiBankNumber(uint8_t bankNumber); const Pedalboard& GetCurrentPreset(); void SaveCurrentPreset(const Pedalboard&pedalboard); - int64_t SaveCurrentPresetAs(const Pedalboard&pedalboard, const std::string&namne,int64_t saveAfterInstanceId = -1); + int64_t SaveCurrentPresetAs(const Pedalboard&pedalboard, int64_t bankInstanceId,const std::string&namne,int64_t saveAfterInstanceId = -1); int64_t GetCurrentPresetId() const; void GetPresetIndex(PresetIndex*pResult); @@ -162,7 +162,7 @@ public: bool LoadPreset(int64_t presetId); - int64_t DeletePreset(int64_t presetId); + int64_t DeletePresets(const std::vector& presetInstanceIds); bool RenamePreset(int64_t presetId, const std::string&name); int64_t CopyPreset(int64_t fromId, int64_t toId = -1); int64_t CreateNewPreset(); @@ -268,6 +268,11 @@ public: void LoadTone3000Auth(); void SetTone3000Auth(const std::string&apiKey); std::string GetTone3000Auth() const; + + std::vector RequestBankPresets(int64_t bankInstanceId); + int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); + int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets); + }; diff --git a/src/makeReleaseMain.cpp b/src/makeReleaseMain.cpp index 1459955..5e1e149 100644 --- a/src/makeReleaseMain.cpp +++ b/src/makeReleaseMain.cpp @@ -43,10 +43,11 @@ std::string psystem(const std::string &command) { throw std::runtime_error("popen() failed!"); } - Finally ff{[pipe]() { - pclose(pipe); - }}; - while (fgets(buffer.data(), buffer.size()-1, pipe) != nullptr) + Finally ff{[pipe]() + { + pclose(pipe); + }}; + while (fgets(buffer.data(), buffer.size() - 1, pipe) != nullptr) { result += buffer.data(); } @@ -68,7 +69,8 @@ void SignPackage() { packagePath = SS("build/pipedal_" << PROJECT_VER << "_amd64.deb"); packagePath = fs::absolute(packagePath); - if (!fs::exists(packagePath)) { + if (!fs::exists(packagePath)) + { packagePath = SS("build/pipedal_" << PROJECT_VER << "_*.deb"); throw std::runtime_error(SS("File does not exist: " << packagePath)); } @@ -82,8 +84,8 @@ void SignPackage() std::string signCmd = SS("/usr/bin/gpg --pinentry-mode loopback --yes --default-key " << UPDATE_GPG_FINGERPRINT2 - << " --armor --output " << packagePath << ".asc" - << " --detach-sign " << packagePath.c_str()); + << " --armor --output " << packagePath << ".asc" + << " --detach-sign " << packagePath.c_str()); int result = system(signCmd.c_str()); if (result != EXIT_SUCCESS) { @@ -226,6 +228,8 @@ public: bool draft = false, prerelease = false; std::vector assets; std::string published_at; + double release_downloads = 0; + double average_daily_downloads = 0; }; static std::string timePointToISO8601(const std::chrono::system_clock::time_point &tp) @@ -233,11 +237,33 @@ static std::string timePointToISO8601(const std::chrono::system_clock::time_poin auto tt = std::chrono::system_clock::to_time_t(tp); std::tm tm = *std::gmtime(&tt); std::stringstream ss; - ss << std::put_time(&tm,"%Y-%m-%dT%H:%M:%S") << "Z"; + ss << std::put_time(&tm, "%Y-%m-%dT%H:%M:%S") << "Z"; return ss.str(); - } +static std::chrono::system_clock::time_point iso8601ToTimePoint(const std::string&date) +{ + std::istringstream ss(date); + std::tm tm = {}; + char zulu; + ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S") >> zulu; + if (ss.fail()) { + throw std::runtime_error("Failed to parse ISO8601 date: " + date); + } + return std::chrono::system_clock::from_time_t(std::mktime(&tm)); +} + + + + +static bool endsWith(const std::string &value, const std::string &end) +{ + if (end.length() > value.length()) + { + return false; + } + return value.compare(value.length() - end.length(), end.length(), end) == 0; +} void GetDownloadCounts(bool gplotDownloads) { // error handling and temporary file use is different enough that it justifies @@ -321,15 +347,65 @@ void GetDownloadCounts(bool gplotDownloads) return left.published_at < right.published_at; // date ascending }); - for (size_t i = 0; i < releases.size()-1; ++i) + uint64_t cumulativeCount = 0; + + for (auto i = releases.begin(); i != releases.end(); /**/) { - releases[i].published_at = releases[i+1].published_at; + if (i->name.find("Experimental") != string::npos + || i->name.find("Retracted") != string::npos + || i->name.find("Testing") != string::npos + ) + { + i = releases.erase(i); + } + else + { + ++i; + } + } + for (auto &release : releases) + { + size_t releaseDownloads = 0; + for (const auto &asset : release.assets) + { + if (asset.name.ends_with(".deb")) + { + releaseDownloads += asset.downloads; + } + } + release.release_downloads = releaseDownloads; + } + + + + for (size_t i = 0; i < releases.size(); ++i) + { + std::chrono::system_clock::time_point nextTime; + if (i == releases.size()-1) + { + nextTime = std::chrono::system_clock::now(); + } else { + nextTime = iso8601ToTimePoint(releases.at(i+1).published_at); + } + auto thisTime = iso8601ToTimePoint(releases.at(i).published_at); + + auto seconds = std::chrono::duration_cast(nextTime-thisTime).count(); + auto days = seconds/(60.0*60*24); + + + releases[i].average_daily_downloads = releases[i].release_downloads/ + (days); + } + + for (size_t i = 0; i < releases.size() - 1; ++i) + { + releases[i].published_at = releases[i + 1].published_at; } if (releases.size() >= 1) { - releases[releases.size()-1].published_at = timePointToISO8601(std::chrono::system_clock::now()); + releases[releases.size() - 1].published_at = timePointToISO8601(std::chrono::system_clock::now()); } - uint64_t cumulativeCount = 0; + for (const auto &release : releases) { for (const auto &asset : release.assets) @@ -339,7 +415,10 @@ void GetDownloadCounts(bool gplotDownloads) cumulativeCount += asset.downloads; } } - cout << release.published_at << " " << cumulativeCount << endl; + cout << release.published_at << " " << cumulativeCount + << " " << release.average_daily_downloads + << " " << "\"" << release.name << "\"" + << endl; } } else @@ -351,14 +430,49 @@ void GetDownloadCounts(bool gplotDownloads) { return left.published_at > right.published_at; // latest date first. }); + + size_t total = 0; + size_t amdTotal = 0; + size_t amdAscTotal = 0; + size_t aarch64Total = 0; + size_t aarch64AscTotal = 0; + size_t otherTotal = 0; + for (const auto &release : releases) { - cout << release.name << endl; + cout << release.name << " " << release.published_at << endl; for (const auto &asset : release.assets) { + total += asset.downloads; + if (endsWith(asset.name, "arm64.deb")) + { + aarch64Total += asset.downloads; + } + else if (endsWith(asset.name, "amd64.deb")) + { + amdTotal += asset.downloads; + } + else if (endsWith(asset.name, "arm64.deb.asc")) + { + aarch64AscTotal += asset.downloads; + } + else if (endsWith(asset.name, "amd64.deb.asc")) + { + amdAscTotal += asset.downloads; + } + else + { + otherTotal += asset.downloads; + } cout << " " << asset.name << ": " << asset.downloads << endl; } } + cout << endl + << "Total: " << total << endl; + + cout << "arm64:" << aarch64Total << " asc: " << aarch64AscTotal << endl; + cout << "amd64:" << amdTotal << " asc: " << amdAscTotal << endl; + cout << "other:" << otherTotal << endl; } } } diff --git a/todo.txt b/todo.txt index 0cdcf93..8423926 100644 --- a/todo.txt +++ b/todo.txt @@ -1,10 +1,4 @@ -double select on double-click/ok in file dialog. -sudo apt install libbz2-dev - -README, license files in file browser. - - ls Carla Project icons. diff --git a/vite/install.sh b/vite/install.sh new file mode 100755 index 0000000..3fbedb2 --- /dev/null +++ b/vite/install.sh @@ -0,0 +1,4 @@ +#!/bin/sudo /bin/bash +rm -rf /etc/pipedal/react +mkdir -p /etc/pipedal/react +cp -r dist/* /etc/pipedal/react/ diff --git a/vite/src/pipedal/App.tsx b/vite/src/pipedal/App.tsx index 396d281..202c11a 100644 --- a/vite/src/pipedal/App.tsx +++ b/vite/src/pipedal/App.tsx @@ -28,6 +28,7 @@ import { isDarkMode } from './DarkMode'; import Tone3000AuthComplete from './Tone3000AuthComplete'; import FontTest from './FontTest'; +import IconTest from './IconTest'; declare module '@mui/material/styles' { interface Theme { @@ -38,6 +39,12 @@ declare module '@mui/material/styles' { mainBackground?: React.CSSProperties['color']; toolbarColor?: React.CSSProperties['color']; } + interface Palette { + actionBar: Palette['primary']; + } + interface PaletteOptions { + actionBar: PaletteOptions['primary']; + } } @@ -142,7 +149,12 @@ const theme = createTheme( }, secondary: { main: "#FF6060" + }, + actionBar: { + main: '#130b22ff', + contrastText: '#FFFFFF' } + }, mainBackground: "#222", toolbarColor: '#222' @@ -208,8 +220,14 @@ const theme = createTheme( }, secondary: { main: "#FF6060" + }, + actionBar: { + main: '#130b22ff', + contrastText: '#FFFFFF' } + + }, mainBackground: "#FFFFFF", toolbarColor: '#FFFFFF' @@ -235,6 +253,11 @@ function isFontTest() { let param = url.searchParams.get("fontTest"); return (param !== null) } +function isIconTest() { + let url = new URL(window.location.href); + let param = url.searchParams.get("iconTest"); + return (param !== null) +} const App = (class extends React.Component { // Before the component mounts, we initialise our state @@ -258,6 +281,7 @@ const App = (class extends React.Component { { isTone3000Auth() && () || isFontTest() && () + || isIconTest() && () || () } diff --git a/vite/src/pipedal/CopyPresetsToBankDialog.tsx b/vite/src/pipedal/CopyPresetsToBankDialog.tsx new file mode 100644 index 0000000..07c9442 --- /dev/null +++ b/vite/src/pipedal/CopyPresetsToBankDialog.tsx @@ -0,0 +1,180 @@ +// Copyright (c) Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import Toolbar from "@mui/material/Toolbar"; +import IconButtonEx from "./IconButtonEx"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import Typography from "@mui/material/Typography"; +import Select from '@mui/material/Select'; +import Button from '@mui/material/Button'; +import InputLabel from '@mui/material/InputLabel'; +import DialogEx from './DialogEx'; +import DialogTitle from '@mui/material/DialogTitle'; +import MenuItem from '@mui/material/MenuItem'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import { PiPedalModel } from './PiPedalModel'; +import { BankIndexEntry } from './Banks'; + + +export interface CopyPresetsToBankDialogProps { + open: boolean, + onOk: (bankInstanceId: number) => void, + onClose: () => void +}; + +export interface CopyPresetsToBankDialogState { + selectedBank: number; + banks: BankIndexEntry[]; + fullScreen: boolean; +}; + +function setSavedDefaultBankSelection(bankInstanceId: number) { + localStorage.setItem('pipedal_copyTo_defaultBankSelection', bankInstanceId.toString()); +} +function getSavedDefaultBankSelection() { + let defaultValue = localStorage.getItem('pipedal_copyTo_defaultBankSelection'); + if (defaultValue) { + return parseInt(defaultValue); + } + return -1; +} +export default class CopyPresetsToBankDialog extends ResizeResponsiveComponent { + + model: PiPedalModel; + constructor(props: CopyPresetsToBankDialogProps) { + super(props); + this.model = PiPedalModel.getInstance(); + let banks = this.getBanks(); + let selectedBank = this.getDefaultBankSelection(banks); + this.state = { + banks: banks, + selectedBank: selectedBank, + fullScreen: false, + + }; + } + private getBanks() { + let bankIndex = this.model.banks.get(); + let result: BankIndexEntry[] = []; + for (let entry of bankIndex.entries) { + if (entry.instanceId === bankIndex.selectedBank) continue; + result.push(entry); + } + return result; + } + private getDefaultBankSelection(banks: BankIndexEntry[]) { + if (banks.length === 0) return -1; + let defaultSelection = getSavedDefaultBankSelection(); + for (let bank of banks) { + if (bank.instanceId === defaultSelection) return defaultSelection; + } + return banks[0].instanceId; + } + + + + componentDidMount() { + super.componentDidMount(); + } + componentWillUnmount() { + super.componentWillUnmount(); + } + + componentDidUpdate() { + } + + handleBankChanged(bankId: number) { + setSavedDefaultBankSelection(bankId); + this.setState({ selectedBank: bankId }); + } + + handleOk() { + this.props.onOk(this.state.selectedBank); + } + + render() { + let props = this.props; + let { open, onClose } = props; + + const handleClose = () => { + onClose(); + }; + return ( + { }} + > +
+ {!this.state.fullScreen && ( + + + { onClose(); }} + > + + + + + Copy Presets to Bank + + + + + )} + + + Bank + + + + + + + +
+
+ ); + } +} diff --git a/vite/src/pipedal/Draggable.tsx b/vite/src/pipedal/Draggable.tsx index ca2931e..7857efe 100644 --- a/vite/src/pipedal/Draggable.tsx +++ b/vite/src/pipedal/Draggable.tsx @@ -27,7 +27,7 @@ import { PiPedalStateError } from './PiPedalError'; import { css } from '@emotion/react'; -const SELECT_SCALE = 1.5; +const SELECT_SCALE = 1.2; const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_THRESHOLD = 48; @@ -58,6 +58,7 @@ export interface DraggableProps extends WithStyles { children?: ReactNode | ReactNode[]; draggable?: boolean; + style?: React.CSSProperties; } @@ -149,6 +150,7 @@ const Draggable = startClientY: number = 0; dragStarted: boolean = false; savedIndex: string = ""; + savedOpacity: string = ""; lastPointerDown: number = 0; originalBounds?: DOMRect; captureElement?: HTMLDivElement; @@ -244,6 +246,8 @@ const Draggable = this.pointerId = e.pointerId; this.pointerType = e.pointerType; this.savedIndex = e.currentTarget.style.zIndex; + this.savedOpacity = e.currentTarget.style.opacity; + e.currentTarget.style.opacity = "0.9"; if (this.pointerType !== "touch") { this.dragTarget.style.transform = "scale(" + SELECT_SCALE + ")"; this.dragTarget.style.zIndex = "3"; @@ -254,6 +258,7 @@ const Draggable = clearDragTransform(element: HTMLDivElement): void { element.style.transform = ""; element.style.zIndex = this.savedIndex; + element.style.opacity = this.savedOpacity; } onPointerCancel(e: PointerEvent) { @@ -486,7 +491,7 @@ const Draggable = render() { const classes = withStyles.getClasses(this.props); return ( -
void; + onLongPress?: (event: PointerEvent, itemIndex: number) => void; canDrag?: boolean; @@ -209,7 +209,7 @@ const DraggableGrid = } bringSelectedItemIntoView() { - if (this.state.indexToSelect && this.state.indexToSelect >= 0) + if (this.state.indexToSelect !== undefined && this.state.indexToSelect >= 0) { let grid = this.refGrid.current; if (grid) @@ -295,6 +295,7 @@ const DraggableGrid = startClientX: number = 0; startClientY: number = 0; dragStarted: boolean = false; + dragThresholdWasExceeded: boolean = false; savedIndex: string = ""; savedBackground: string = ""; lastPointerDown: number = 0; @@ -529,6 +530,11 @@ const DraggableGrid = if (this.startIndex !== this.currentIndex) { this.props.moveElement(this.startIndex, this.currentIndex); } + if (!this.dragThresholdWasExceeded) { + if (this.props.onLongPress) { + this.props.onLongPress(e,this.startIndex); + } + } } this.cancelDrag(); } @@ -693,6 +699,7 @@ const DraggableGrid = ///window.addEventListener("touchstart",this.handleTouchStart, {passive: false}); this.dragStarted = false; + this.dragThresholdWasExceeded = false; this.startX = e.clientX; this.startY = e.clientY; this.startClientX = e.clientX; @@ -756,8 +763,11 @@ const DraggableGrid = handlePointerMove(e: PointerEvent): boolean { if (this.isCapturedPointer(e)) { - if (!this.dragStarted && this.dragThresholdExceeded(e)) { - this.startDrag(); + if (this.dragThresholdExceeded(e)) { + if (!this.dragStarted) { + this.startDrag(); + } + this.dragThresholdWasExceeded = true; } if (this.dragStarted) { this.lastX = e.clientX; diff --git a/vite/src/pipedal/IconColorDropdownButton.tsx b/vite/src/pipedal/IconColorDropdownButton.tsx new file mode 100644 index 0000000..11436fd --- /dev/null +++ b/vite/src/pipedal/IconColorDropdownButton.tsx @@ -0,0 +1,155 @@ +/* + * MIT License + * + * Copyright (c) Robin E. R. Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React, { useState } from 'react'; +import ButtonBase from '@mui/material/ButtonBase'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import { useTheme } from '@mui/material/styles'; +import PluginIcon, { getIconColorSelections, getIconColor, IconColorSelect } from './PluginIcon'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import { PluginType } from './Lv2Plugin'; + + +export enum DropdownAlignment { + SE, SW +} + +interface ColorListDropdownButtonProps { + colorKey: string; + colorList?: IconColorSelect[]; + pluginType?: PluginType; + + onColorChange: (selection: IconColorSelect) => void; + dropdownAlignment: DropdownAlignment; +} + +const IconColorDropdownButton: React.FC = (props: ColorListDropdownButtonProps) => { + let { colorList, colorKey, onColorChange, dropdownAlignment } = props; + if (!colorList) colorList = getIconColorSelections(); + const theme = useTheme(); + + const [anchorEl, setAnchorEl] = useState(null); + + let selectedColor = getIconColor(colorKey); + if (selectedColor === undefined) { + selectedColor = theme.palette.text.secondary; + } + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleColorSelect = (selection: IconColorSelect) => { + handleClose(); + onColorChange(selection); + }; + return ( +
+ +
+ {props.pluginType ? ( +
+ +
+ ) : ( +
+ + )} + +
+ + + +
+ + handleColorSelect({ key: "default", value: undefined })} + style={{ + width: 48, height: 48, borderRadius: 6, margin: 4, + borderStyle: "solid", + borderWidth: colorKey === "default" ? 2 : 0.25, + borderColor: colorKey == "default" ? + theme.palette.text.primary + : theme.palette.text.secondary, + backgroundColor: theme.palette.text.secondary + }} + > + +
+ {colorList.map((color) => { + let selected = color.key === selectedColor; + return ( + handleColorSelect(color)} + style={{ + width: 48, height: 48, borderRadius: 6, margin: 4, + borderStyle: "solid", + borderWidth: selected ? 2 : 0.25, + borderColor: color.key === selectedColor ? + theme.palette.text.primary + : theme.palette.text.secondary, + backgroundColor: color.value + }} + > + ) + } + )} +
+
+
+
+ ); +}; + +export default IconColorDropdownButton; \ No newline at end of file diff --git a/vite/src/pipedal/IconTest.tsx b/vite/src/pipedal/IconTest.tsx new file mode 100644 index 0000000..0a40d24 --- /dev/null +++ b/vite/src/pipedal/IconTest.tsx @@ -0,0 +1,73 @@ + +import { useState } from "react"; +import { PluginType } from "./Lv2Plugin"; +import PluginIcon, { getIconColorSelections } from './PluginIcon'; +import IconColorDropdownButton, {DropdownAlignment} from "./IconColorDropdownButton"; + +function IconTest() { + const [selectedColor, setSelectedColor] = useState(undefined); + const [selectedKey, setSelectedKey] = useState(""); + return ( + <> +
+ { + getIconColorSelections().map((color) => { + return ( +
{ + setSelectedColor(color.value ); + }} + /> + ); + + }) + } + +
+
+ { + getIconColorSelections(false).map((color) => { + return ( +
{ setSelectedColor(color.value ); }} + /> + ); + + }) + } + +
+
+ + { + setSelectedColor(selection.value); + setSelectedKey(selection.key); + } + } + dropdownAlignment={DropdownAlignment.SE} /> +
+
+ { + Object.values(PluginType).map((pluginType) => ( +
+ +
+ )) + } +
+
+ { + Object.values(PluginType).map((pluginType) => ( +
+ +
+ )) + } +
+ + ); +} + +export default IconTest; \ No newline at end of file diff --git a/vite/src/pipedal/ImportPresetFromBankDialog.tsx b/vite/src/pipedal/ImportPresetFromBankDialog.tsx new file mode 100644 index 0000000..8327a8b --- /dev/null +++ b/vite/src/pipedal/ImportPresetFromBankDialog.tsx @@ -0,0 +1,282 @@ +// Copyright (c) Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import Divider from '@mui/material/Divider'; +import Toolbar from "@mui/material/Toolbar"; +import IconButtonEx from "./IconButtonEx"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import Typography from "@mui/material/Typography"; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Checkbox from '@mui/material/Checkbox'; +import Select from '@mui/material/Select'; +import Button from '@mui/material/Button'; +import InputLabel from '@mui/material/InputLabel'; +import DialogEx from './DialogEx'; +import DialogTitle from '@mui/material/DialogTitle'; +import MenuItem from '@mui/material/MenuItem'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import { PiPedalModel, PresetIndexEntry } from './PiPedalModel'; +import { BankIndexEntry } from './Banks'; + + +export interface ImportPresetFromBankDialogProps { + open: boolean, + onOk: (bankInstanceId: number, presets: number[]) => void, + onClose: () => void +}; + +export interface ImportPresetFromBankDialogState { + selectedBank: number; + banks: BankIndexEntry[]; + bankPresets: PresetIndexEntry[] | null; + selectedPresets: number[]; + + fullScreen: boolean; +}; + +function setSavedDefaultBankSelection(bankInstanceId: number) { + localStorage.setItem('pipedal_defaultBankSelection', bankInstanceId.toString()); +} +function getSavedDefaultBankSelection() { + let defaultValue = localStorage.getItem('pipedal_defaultBankSelection'); + if (defaultValue) { + return parseInt(defaultValue); + } + return -1; +} +export default class ImportPresetFromBankDialog extends ResizeResponsiveComponent { + + model: PiPedalModel; + constructor(props: ImportPresetFromBankDialogProps) { + super(props); + this.model = PiPedalModel.getInstance(); + let banks = this.getBanks(); + let selectedBank = this.getDefaultBankSelection(banks); + this.state = { + banks: banks, + selectedBank: selectedBank, + fullScreen: this.windowSize.height < 550, + bankPresets: null, + selectedPresets: [] + + + }; + } + private getBanks() { + let bankIndex = this.model.banks.get(); + let result: BankIndexEntry[] = []; + for (let entry of bankIndex.entries) { + if (entry.instanceId === bankIndex.selectedBank) continue; + result.push(entry); + } + return result; + } + private getDefaultBankSelection(banks: BankIndexEntry[]) { + if (banks.length === 0) return -1; + let defaultSelection = getSavedDefaultBankSelection(); + for (let bank of banks) { + if (bank.instanceId === defaultSelection) return defaultSelection; + } + return banks[0].instanceId; + } + + private mounted: boolean = false; + + + + onWindowSizeChanged(width: number, height: number): void { + this.setState({ fullScreen: height < 550 }) + } + + + requestPresets(bankInstanceId: number) { + this.model.requestBankPresets(bankInstanceId).then((presets) => { + if (!this.mounted) return; + this.setState({ bankPresets: presets, selectedPresets: [] }); + }).catch((error) => { + this.model.showAlert("Error requesting presets: " + error); + }); + } + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + this.requestPresets(this.state.selectedBank); + } + componentWillUnmount() { + super.componentWillUnmount(); + this.mounted = false; + } + + componentDidUpdate() { + } + checkForIllegalCharacters(filename: string) { + } + + handleBankChanged(bankId: number) { + setSavedDefaultBankSelection(bankId); + this.setState({ selectedBank: bankId }); + this.requestPresets(bankId); + } + + handleOk() { + if (this.state.selectedPresets.length === 0) return; + this.props.onOk(this.state.selectedBank, this.state.selectedPresets); + } + + render() { + let props = this.props; + let { open, onClose } = props; + + const handleClose = () => { + onClose(); + }; + return ( + { }} + > +
+ {!this.state.fullScreen ? ( + <> + + + { onClose(); }} + > + + + + + Import Presets from Bank + + + + + + Bank + + + + + ) : ( +
+ { onClose(); }} + > + + + +
+ Bank + + +
+
+ + + )} + + + + {this.state.bankPresets?.map((presetEntry) => { + const labelId = `checkbox-list-label-${presetEntry.instanceId}`; + + return ( + { + let selectedPresets = this.state.selectedPresets; + let index = selectedPresets.indexOf(presetEntry.instanceId); + if (index === -1) { + selectedPresets.push(presetEntry.instanceId); + } else { + selectedPresets.splice(index, 1); + } + this.setState({ selectedPresets: [...selectedPresets] }); + }}> + + + + + + ); + })} + + + + + + + +
+
+ ); + } +} diff --git a/vite/src/pipedal/Lv2Plugin.tsx b/vite/src/pipedal/Lv2Plugin.tsx index f63ced5..e7d4b09 100644 --- a/vite/src/pipedal/Lv2Plugin.tsx +++ b/vite/src/pipedal/Lv2Plugin.tsx @@ -223,6 +223,7 @@ export class UiFrequencyPlot { this.xLeft = input.xLeft; this.xRight = input.xRight; this.xLog = input.xLog; + this.yDb = input.yDb; this.yTop = input.yTop; this.yBottom = input.yBottom; this.width = input.width; @@ -242,6 +243,7 @@ export class UiFrequencyPlot { xLeft: number = -1; xRight: number = -1; xLog: boolean = true; + yDb: boolean = true; yTop: number = -1; yBottom: number = -1; width: number = -1; diff --git a/vite/src/pipedal/MainPage.tsx b/vite/src/pipedal/MainPage.tsx index e7ba524..56eca58 100644 --- a/vite/src/pipedal/MainPage.tsx +++ b/vite/src/pipedal/MainPage.tsx @@ -24,7 +24,7 @@ import { withStyles } from "tss-react/mui"; import IconButtonEx from './IconButtonEx'; import ButtonEx from './ButtonEx'; import ButtonBase from '@mui/material/ButtonBase'; -import RenameDialog from './RenameDialog'; +import PluginIcon, { getIconColor } from './PluginIcon'; import ToolTipEx from './ToolTipEx'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; @@ -64,9 +64,11 @@ import PipedalUiIcon from './svg/pp_ui.svg?react'; import SnapshotDialog from './SnapshotDialog'; import { css } from '@emotion/react'; import { setDefaultModGuiPreference } from './ModGuiHost'; +import PluginNameDialog from './PluginNameDialog'; const SPLIT_CONTROLBAR_THRESHHOLD = 750; +const SHOW_ICON_THRESHHOLD = 475; const DISPLAY_AUTHOR_THRESHHOLD = 750; const DISPLAY_AUTHOR_SPLIT_THRESHOLD = 500; const HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK = 500; @@ -128,6 +130,7 @@ interface MainState { splitControlBar: boolean; displayAuthor: boolean; horizontalScrollLayout: boolean; + canDisplayPluginIcon: boolean; showMidiBindingsDialog: boolean; screenHeight: number; displayNameDialogOpen: boolean; @@ -147,6 +150,9 @@ export const MainPage = getSplitToolbar() { return this.windowSize.width < SPLIT_CONTROLBAR_THRESHHOLD && this.windowSize.height >= HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK; } + getShowPluginIcon() { + return this.windowSize.width > SHOW_ICON_THRESHHOLD; + } getDisplayAuthor() { if (this.getSplitToolbar()) { return this.windowSize.width >= DISPLAY_AUTHOR_SPLIT_THRESHOLD; @@ -173,6 +179,7 @@ export const MainPage = addMenuAnchorEl: null, splitControlBar: this.getSplitToolbar(), displayAuthor: this.getDisplayAuthor(), + canDisplayPluginIcon: this.getShowPluginIcon(), horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK, showMidiBindingsDialog: false, screenHeight: this.windowSize.height, @@ -304,6 +311,7 @@ export const MainPage = this.setState({ splitControlBar: this.getSplitToolbar(), displayAuthor: this.getDisplayAuthor(), + canDisplayPluginIcon: this.getShowPluginIcon(), horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK, screenHeight: this.windowSize.height }); @@ -415,6 +423,7 @@ export const MainPage = return pedalboardItem.uri; } titleBar(pedalboardItem: PedalboardItem | null, canShowModUi: boolean): React.ReactNode { + let uiPlugin = pedalboardItem ? this.model.getUiPlugin(pedalboardItem?.uri) : null; let title = ""; let author = ""; let infoPluginUri = ""; @@ -489,21 +498,45 @@ export const MainPage = this.handleEditPluginDisplayName(); }} > -
- - {title} - {this.state.displayAuthor && ( - {author} - )} - +
+ {this.state.canDisplayPluginIcon && uiPlugin && ( +
+ +
+ )} +
+ + {title} + {this.state.displayAuthor && ( + {author} + )} + +
) : ( -
- {title} - {this.state.displayAuthor && ( - {author} +
+ {this.state.canDisplayPluginIcon && uiPlugin && ( +
+ +
)} +
+ + {title} + {this.state.displayAuthor && ( + {author} + )} + +
)} @@ -770,17 +803,14 @@ export const MainPage = this.onSnapshotDialogOk()} /> )} {(this.state.displayNameDialogOpen) && ( - this.setState({ displayNameDialogOpen: false })} - onOk={(newName: string) => { + onApply={(newName: string, color: string) => { if (pedalboardItem) { - this.model.setPedalboardItemTitle(pedalboardItem.instanceId, newName); + this.model.setPedalboardItemTitle(pedalboardItem.instanceId, newName, color); } - this.setState({ displayNameDialogOpen: false }); }} /> )} diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 25d9951..157a5e4 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -80,6 +80,7 @@ export class PedalboardItem implements Deserializable { this.lilvPresetUri = input.lilvPresetUri; this.pathProperties = input.pathProperties; this.useModUi = input.useModUi ?? false; + this.iconColor = input.iconColor??""; return this; } @@ -215,6 +216,7 @@ export class PedalboardItem implements Deserializable { lilvPresetUri: string = ""; pathProperties: {[Name: string]: string} = {}; useModUi: boolean = false; // true if this item should use the mod-ui. + iconColor: string = ""; }; export class SnapshotValue { diff --git a/vite/src/pipedal/PedalboardView.tsx b/vite/src/pipedal/PedalboardView.tsx index 8e3f29c..319cf2a 100644 --- a/vite/src/pipedal/PedalboardView.tsx +++ b/vite/src/pipedal/PedalboardView.tsx @@ -19,9 +19,9 @@ import React, { ReactNode, Component, SyntheticEvent } from 'react'; import { css } from '@emotion/react'; -import {createStyles} from './WithStyles'; +import { createStyles } from './WithStyles'; -import WithStyles, {withTheme} from './WithStyles'; +import WithStyles, { withTheme } from './WithStyles'; import { withStyles } from "tss-react/mui"; @@ -30,7 +30,7 @@ import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { PluginType } from './Lv2Plugin'; import ButtonBase from '@mui/material/ButtonBase'; import Typography from '@mui/material/Typography'; -import PluginIcon, { SelectIconUri } from './PluginIcon'; +import PluginIcon, { getIconColor, SelectIconUri } from './PluginIcon'; import { SelectHoverBackground } from './SelectHoverBackground'; import SvgPathBuilder from './SvgPathBuilder'; import Draggable from './Draggable' @@ -42,9 +42,9 @@ import { Pedalboard, PedalboardItem, PedalboardSplitItem, SplitType } from './Pedalboard'; -import MidiIcon from './svg/ic_midi.svg?react'; -import { midiChannelBindingControlFeatureEnabled } from './MidiChannelBinding'; - +// import MidiIcon from './svg/ic_midi.svg?react'; +// import { midiChannelBindingControlFeatureEnabled } from './MidiChannelBinding'; +// import CloseIcon from '@mui/icons-material/Close'; const START_CONTROL = Pedalboard.START_CONTROL; const END_CONTROL = Pedalboard.END_CONTROL; @@ -161,7 +161,24 @@ const pedalboardStyles = (theme: Theme) => createStyles({ marginBottom: (CELL_HEIGHT - FRAME_SIZE) / 2, width: FRAME_SIZE, height: FRAME_SIZE, - border: isDarkMode() ? "1pt #AAA solid" : "1pt #666 solid", + border: isDarkMode() ? "1pt #555 solid" : "1pt #666 solid", + borderRadius: 6 + }), + selectedIconFrame: css({ + + display: "flex", + alignItems: "center", + justifyContent: "center", + position: "relative", + + background: theme.palette.background.default, + marginLeft: (CELL_WIDTH - FRAME_SIZE) / 2, + marginRight: (CELL_WIDTH - FRAME_SIZE) / 2, + marginTop: (CELL_HEIGHT - FRAME_SIZE) / 2, + marginBottom: (CELL_HEIGHT - FRAME_SIZE) / 2, + width: FRAME_SIZE, + height: FRAME_SIZE, + border: isDarkMode() ? "1pt #FFF solid" : "1pt #333 solid", borderRadius: 6 }), borderlessIconFrame: css({ @@ -245,6 +262,7 @@ class PedalLayout { name: string = ""; pluginType: PluginType = PluginType.Plugin; iconUrl: string = ""; + iconColor: string = ""; bounds: Rect = new Rect(); @@ -333,6 +351,7 @@ class PedalLayout { pluginType = PluginType.NamPlugin; } this.iconUrl = SelectIconUri(pluginType); + this.iconColor = pedalItem.iconColor; this.name = uiPlugin.label; if (pedalItem.title !== "") { this.name = pedalItem.title; @@ -401,908 +420,932 @@ class LayoutParams { const PedalboardView = -withTheme( -withStyles( - class extends Component { - model: PiPedalModel; + withTheme( + withStyles( + class extends Component { + model: PiPedalModel; - frameRef: React.RefObject; - scrollRef: React.RefObject; - private bgColor: string; + frameRef: React.RefObject; + scrollRef: React.RefObject; + private bgColor: string; - constructor(props: PedalboardProps) { - super(props); - this.model = PiPedalModelFactory.getInstance(); + constructor(props: PedalboardProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); - this.bgColor = props.theme.palette.background.default + this.bgColor = props.theme.palette.background.default - if (!props.selectedId) props.selectedId = -1; - this.state = { - pedalboard: this.model.pedalboard.get(), - }; - this.onPedalboardChanged = this.onPedalboardChanged.bind(this); - this.frameRef = React.createRef(); - this.scrollRef = React.createRef(); - this.handleTouchStart = this.handleTouchStart.bind(this); - } + if (!props.selectedId) props.selectedId = -1; + this.state = { + pedalboard: this.model.pedalboard.get(), + }; + this.onPedalboardChanged = this.onPedalboardChanged.bind(this); + this.frameRef = React.createRef(); + this.scrollRef = React.createRef(); + this.handleTouchStart = this.handleTouchStart.bind(this); + } - handleTouchStart(e: any) { - // just has to exist to allow Draggable to receive - // touchyMove. :-/ - } + handleTouchStart(e: any) { + // just has to exist to allow Draggable to receive + // touchyMove. :-/ + } - onDragEnd(instanceId: number, clientX: number, clientY: number) { - if (!this.props.enableStructureEditing) { - return; - } - if (!this.currentLayout) return; - - if (!this.frameRef.current) return; - - let currentLayout: PedalLayout[] = this.currentLayout; - let frameElement = this.frameRef.current; - - let rc = frameElement.getBoundingClientRect(); - clientX -= rc.left; - clientY -= rc.top; - - let it = chainIterator(currentLayout); - - while (true) { - let v = it.next(); - if (v.done) break; - let item = v.value; - - if (item.isSplitter() && item.pedalItem) { - if (item.bounds.contains(clientX, clientY)) { - if (clientX < item.bounds.x + CELL_WIDTH / 2) { - this.model.movePedalboardItemBefore(instanceId, item.pedalItem.instanceId); - this.setSelection(instanceId); - return; - } else if (clientX > item.bounds.right - CELL_WIDTH / 2) { - this.model.movePedalboardItemAfter(instanceId, item.pedalItem.instanceId); - this.setSelection(instanceId); - return; - - } - } - let yMid = (item.bounds.y + item.bounds.bottom) / 2; - if (clientX >= item.bounds.x - && clientY < yMid && clientY >= item.topChildren[0].bounds.y - - ) { - if (clientX < item.topChildren[0].bounds.x) { - let topPedalItem = item.topChildren[0].pedalItem; - if (topPedalItem) { - this.model.movePedalboardItemBefore(instanceId, topPedalItem.instanceId); - this.setSelection(instanceId); - return; - } - } - let lastTop = item.topChildren[item.topChildren.length - 1]; - if (clientX >= lastTop.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { - if (lastTop.pedalItem) { - this.model.movePedalboardItemAfter(instanceId, lastTop.pedalItem.instanceId); - this.setSelection(instanceId); - return; - } - } - - } - if (clientX >= item.bounds.x - && clientY > yMid && clientY < item.bottomChildren[0].bounds.bottom - ) { - if (clientX < item.bottomChildren[0].bounds.x) { - let bottomPedalItem = item.bottomChildren[0].pedalItem; - if (bottomPedalItem) { - this.model.movePedalboardItemBefore(instanceId, bottomPedalItem.instanceId); - this.setSelection(instanceId); - return; - } - } - let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; - if (clientX >= lastBottom.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { - if (lastBottom.pedalItem) { - this.model.movePedalboardItemAfter(instanceId, lastBottom.pedalItem.instanceId); - this.setSelection(instanceId); - return; - } - - } - - } - - } else if (item.bounds.contains(clientX, clientY)) { - if (item.isStart()) { - this.model.movePedalboardItemToStart(instanceId); - this.setSelection(instanceId); + onDragEnd(instanceId: number, clientX: number, clientY: number) { + if (!this.props.enableStructureEditing) { return; - } else if (item.isEnd()) { - this.model.movePedalboardItemToEnd(instanceId); - this.setSelection(instanceId); - return; - } else { - if (item.pedalItem) { - let margin = (CELL_WIDTH - FRAME_SIZE) / 2; - if (clientX < item.bounds.x + margin) { - this.model.movePedalboardItemBefore(instanceId, item.pedalItem.instanceId); - } else if (clientX > item.bounds.right - margin) { - this.model.movePedalboardItemAfter(instanceId, item.pedalItem.instanceId); + } + if (!this.currentLayout) return; + + if (!this.frameRef.current) return; + + let currentLayout: PedalLayout[] = this.currentLayout; + let frameElement = this.frameRef.current; + + let rc = frameElement.getBoundingClientRect(); + clientX -= rc.left; + clientY -= rc.top; + + let it = chainIterator(currentLayout); + + while (true) { + let v = it.next(); + if (v.done) break; + let item = v.value; + + if (item.isSplitter() && item.pedalItem) { + if (item.bounds.contains(clientX, clientY)) { + if (clientX < item.bounds.x + CELL_WIDTH / 2) { + this.model.movePedalboardItemBefore(instanceId, item.pedalItem.instanceId); + this.setSelection(instanceId); + return; + } else if (clientX > item.bounds.right - CELL_WIDTH / 2) { + this.model.movePedalboardItemAfter(instanceId, item.pedalItem.instanceId); + this.setSelection(instanceId); + return; + + } + } + let yMid = (item.bounds.y + item.bounds.bottom) / 2; + if (clientX >= item.bounds.x + && clientY < yMid && clientY >= item.topChildren[0].bounds.y + + ) { + if (clientX < item.topChildren[0].bounds.x) { + let topPedalItem = item.topChildren[0].pedalItem; + if (topPedalItem) { + this.model.movePedalboardItemBefore(instanceId, topPedalItem.instanceId); + this.setSelection(instanceId); + return; + } + } + let lastTop = item.topChildren[item.topChildren.length - 1]; + if (clientX >= lastTop.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { + if (lastTop.pedalItem) { + this.model.movePedalboardItemAfter(instanceId, lastTop.pedalItem.instanceId); + this.setSelection(instanceId); + return; + } + } + + } + if (clientX >= item.bounds.x + && clientY > yMid && clientY < item.bottomChildren[0].bounds.bottom + ) { + if (clientX < item.bottomChildren[0].bounds.x) { + let bottomPedalItem = item.bottomChildren[0].pedalItem; + if (bottomPedalItem) { + this.model.movePedalboardItemBefore(instanceId, bottomPedalItem.instanceId); + this.setSelection(instanceId); + return; + } + } + let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; + if (clientX >= lastBottom.bounds.right && clientX < item.bounds.right - CELL_WIDTH / 2) { + if (lastBottom.pedalItem) { + this.model.movePedalboardItemAfter(instanceId, lastBottom.pedalItem.instanceId); + this.setSelection(instanceId); + return; + } + + } + + } + + } else if (item.bounds.contains(clientX, clientY)) { + if (item.isStart()) { + this.model.movePedalboardItemToStart(instanceId); + this.setSelection(instanceId); + return; + } else if (item.isEnd()) { + this.model.movePedalboardItemToEnd(instanceId); + this.setSelection(instanceId); + return; } else { - this.model.movePedalboardItem(instanceId, item.pedalItem.instanceId); + if (item.pedalItem) { + let margin = (CELL_WIDTH - FRAME_SIZE) / 2; + if (clientX < item.bounds.x + margin) { + this.model.movePedalboardItemBefore(instanceId, item.pedalItem.instanceId); + } else if (clientX > item.bounds.right - margin) { + this.model.movePedalboardItemAfter(instanceId, item.pedalItem.instanceId); + } else { + this.model.movePedalboardItem(instanceId, item.pedalItem.instanceId); + } + this.setSelection(instanceId); + return; + } } - this.setSelection(instanceId); - return; + + } + } + // delete the plugin. + let newId = this.model.setPedalboardItemEmpty(instanceId); + this.setSelection(newId); + + } + + onPedalboardChanged(value?: Pedalboard) { + this.setState({ + pedalboard: value, + }); + } + + componentDidMount() { + this.scrollRef.current!.addEventListener("touchstart", this.handleTouchStart, { passive: false }); + this.model.pedalboard.addOnChangedHandler(this.onPedalboardChanged); + + } + componentWillUnmount() { + this.scrollRef.current!.removeEventListener("touchstart", this.handleTouchStart); + this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged); + } + + offsetLayout_(layoutItems: PedalLayout[], offset: number): void { + for (let i = 0; i < layoutItems.length; ++i) { + let layoutItem = layoutItems[i]; + layoutItem.bounds.y += offset; + if (layoutItem.isSplitter()) { + layoutItem.topConnectorY += offset; + layoutItem.bottomConnectorY += offset; + this.offsetLayout_(layoutItem.topChildren, offset); + this.offsetLayout_(layoutItem.bottomChildren, offset); + } + } + } + + getSplitterIcon(layoutItem: PedalLayout): PluginType { + if (layoutItem.pedalItem === undefined) { + throw new Error("Invalid splitter"); + } + let split = layoutItem.pedalItem as PedalboardSplitItem; + if (split.getSplitType() === SplitType.Ab) { + if (split.isASelected()) { + return PluginType.SplitA; + } else { + return PluginType.SplitB; + + } + } else if (split.getSplitType() === SplitType.Mix) { + return PluginType.SplitMix; //"img/fx_dial.svg"; + } else { + return PluginType.SplitLR; //"img/fx_lr.svg"; + } + } + + doLayout2_(lp: LayoutParams, layoutItems: PedalLayout[]): Rect { + let bounds = new Rect(); + for (let i = 0; i < layoutItems.length; ++i) { + let layoutItem = layoutItems[i]; + if (layoutItem.isSplitter()) { + let x0 = lp.cx; + let y0 = lp.cy; + + layoutItem.bounds.x = x0; + layoutItem.bounds.y = y0; + layoutItem.bounds.height = CELL_HEIGHT; + layoutItem.bounds.width = CELL_WIDTH; + + lp.cx += CELL_WIDTH; + + + let topBounds = this.doLayout2_(lp, layoutItem.topChildren); + if (topBounds.isEmpty()) { + topBounds.x = lp.cx; + topBounds.width = 0; + topBounds.y = y0 - CELL_HEIGHT / 2; + topBounds.height = CELL_HEIGHT; + + } + + + let dyTop = (lp.cy + CELL_HEIGHT / 2) - (topBounds.y + topBounds.height); + + + + this.offsetLayout_(layoutItem.topChildren, dyTop); + topBounds.offset(0, dyTop); + bounds.accumulate(topBounds); + + let topCx = lp.cx; + lp.cx = x0; + lp.cx += CELL_WIDTH; + + let bottomBounds = this.doLayout2_(lp, layoutItem.bottomChildren); + if (bottomBounds.isEmpty()) { + bottomBounds.x = lp.cx; bottomBounds.width = 0; + bottomBounds.y = lp.cy; bottomBounds.height = CELL_HEIGHT; + } + + let dyBottom = (lp.cy + CELL_HEIGHT / 2) - bottomBounds.y; + this.offsetLayout_(layoutItem.bottomChildren, dyBottom) + bottomBounds.offset(0, dyBottom); + bounds.accumulate(bottomBounds); + + lp.cx = Math.max(lp.cx, topCx) + CELL_WIDTH; + lp.cy = y0; + + layoutItem.bounds.width = lp.cx - layoutItem.bounds.x; + bounds.accumulate(layoutItem.bounds); + + if (layoutItem.topChildren.length === 0) { + layoutItem.topConnectorY = bounds.y + CELL_HEIGHT / 2; + } else { + layoutItem.topConnectorY = layoutItem.topChildren[0].bounds.y + CELL_HEIGHT / 2; + } + if (layoutItem.bottomChildren.length === 0) { + layoutItem.bottomConnectorY = bounds.y + bounds.height - CELL_HEIGHT / 2; + } else { + layoutItem.bottomConnectorY = layoutItem.bottomChildren[0].bounds.y + CELL_HEIGHT / 2; + } + } else { + layoutItem.bounds.x = lp.cx; + layoutItem.bounds.y = lp.cy; + lp.cx += CELL_WIDTH; + layoutItem.bounds.width = CELL_WIDTH; + layoutItem.bounds.height = CELL_HEIGHT; + bounds.accumulate(layoutItem.bounds); + + } + } + return bounds; + } + doLayout(layoutItems: PedalLayout[]): LayoutSize { + const TWO_ROW_HEIGHT = 142 - 14; + + if (layoutItems.length === 0) { + // if the current pedalboard is empty, reserve display space anyway. + return { width: 1, height: TWO_ROW_HEIGHT }; + } + + let lp = new LayoutParams(); + + let bounds = this.doLayout2_(lp, layoutItems); + // shift everything down so there are no negative y coordinates. + + if (bounds.height < TWO_ROW_HEIGHT) { + + let extra = Math.floor((TWO_ROW_HEIGHT - Math.ceil(bounds.height)) / 2); + this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra / 2)); + bounds.height += extra; + + } else { + this.offsetLayout_(layoutItems, -bounds.y); + } + + bounds.height += 14; // for labels that aren't accounted for. + return { width: bounds.width, height: bounds.height }; + + } + + onItemClick(e: SyntheticEvent, instanceId?: number): void { + if (instanceId) { + this.setSelection(instanceId); + } + } + setSelection(instanceId: number) { + if (this.props.onSelectionChanged) { + this.props.onSelectionChanged(instanceId); + } + } + + onItemDoubleClick(event: SyntheticEvent, instanceId?: number): void { + event.preventDefault(); + event.stopPropagation(); + + if (this.props.onDoubleClick && instanceId && this.props.enableStructureEditing) { + this.props.onDoubleClick(instanceId); + } + + } + + onItemLongClick(event: SyntheticEvent, instanceId?: number): void { + if (!instanceId) { + return; + } + event.preventDefault(); + event.stopPropagation(); + + if (!this.props.enableStructureEditing) { + this.setSelection(instanceId); + return; + } + if (!Utility.needsZoomedControls()) { + if (this.props.onDoubleClick && this.props.enableStructureEditing && instanceId) { + this.props.onDoubleClick(instanceId); } } } - } - // delete the plugin. - let newId = this.model.setPedalboardItemEmpty(instanceId); - this.setSelection(newId); - } - - onPedalboardChanged(value?: Pedalboard) { - this.setState({ - pedalboard: value, - }); - } - - componentDidMount() { - this.scrollRef.current!.addEventListener("touchstart", this.handleTouchStart, { passive: false }); - this.model.pedalboard.addOnChangedHandler(this.onPedalboardChanged); - - } - componentWillUnmount() { - this.scrollRef.current!.removeEventListener("touchstart", this.handleTouchStart); - this.model.pedalboard.removeOnChangedHandler(this.onPedalboardChanged); - } - - offsetLayout_(layoutItems: PedalLayout[], offset: number): void { - for (let i = 0; i < layoutItems.length; ++i) { - let layoutItem = layoutItems[i]; - layoutItem.bounds.y += offset; - if (layoutItem.isSplitter()) { - layoutItem.topConnectorY += offset; - layoutItem.bottomConnectorY += offset; - this.offsetLayout_(layoutItem.topChildren, offset); - this.offsetLayout_(layoutItem.bottomChildren, offset); - } - } - } - - getSplitterIcon(layoutItem: PedalLayout): PluginType { - if (layoutItem.pedalItem === undefined) { - throw new Error("Invalid splitter"); - } - let split = layoutItem.pedalItem as PedalboardSplitItem; - if (split.getSplitType() === SplitType.Ab) { - if (split.isASelected()) { - return PluginType.SplitA; - } else { - return PluginType.SplitB; - - } - } else if (split.getSplitType() === SplitType.Mix) { - return PluginType.SplitMix; //"img/fx_dial.svg"; - } else { - return PluginType.SplitLR; //"img/fx_lr.svg"; - } - } - - doLayout2_(lp: LayoutParams, layoutItems: PedalLayout[]): Rect { - let bounds = new Rect(); - for (let i = 0; i < layoutItems.length; ++i) { - let layoutItem = layoutItems[i]; - if (layoutItem.isSplitter()) { - let x0 = lp.cx; - let y0 = lp.cy; - - layoutItem.bounds.x = x0; - layoutItem.bounds.y = y0; - layoutItem.bounds.height = CELL_HEIGHT; - layoutItem.bounds.width = CELL_WIDTH; - - lp.cx += CELL_WIDTH; - - - let topBounds = this.doLayout2_(lp, layoutItem.topChildren); - if (topBounds.isEmpty()) { - topBounds.x = lp.cx; - topBounds.width = 0; - topBounds.y = y0 - CELL_HEIGHT / 2; - topBounds.height = CELL_HEIGHT; + strokeConnector(output: ReactNode[], channels: number, enabled: Boolean, svgPath: string) { + let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + if (channels === 2) { + output.push(( + + )); + output.push(( + + )); + } else if (channels === 1) { + output.push(( + + )); } + } + renderConnector(output: ReactNode[], item: PedalLayout, enabled: boolean): void { + // const classes = withStyles.getClasses(this.props); + let x_ = item.bounds.x + CELL_WIDTH / 2; + let y_ = item.bounds.y + CELL_HEIGHT / 2; + let numberOfOutputs = item.numberOfOutputs; + let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + let stereoCenterColor = this.bgColor; - let dyTop = (lp.cy + CELL_HEIGHT / 2) - (topBounds.y + topBounds.height); + if (item.originalInputs === 0) { + // break the input paths. + let rx = item.bounds.x + CELL_WIDTH / 2 - FRAME_SIZE / 2 - 4; + let ry = y_ - 4; - - - this.offsetLayout_(layoutItem.topChildren, dyTop); - topBounds.offset(0, dyTop); - bounds.accumulate(topBounds); - - let topCx = lp.cx; - lp.cx = x0; - lp.cx += CELL_WIDTH; - - let bottomBounds = this.doLayout2_(lp, layoutItem.bottomChildren); - if (bottomBounds.isEmpty()) { - bottomBounds.x = lp.cx; bottomBounds.width = 0; - bottomBounds.y = lp.cy; bottomBounds.height = CELL_HEIGHT; + output.push(( + + )); } + let svgPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_ + CELL_WIDTH, y_).toString(); - let dyBottom = (lp.cy + CELL_HEIGHT / 2) - bottomBounds.y; - this.offsetLayout_(layoutItem.bottomChildren, dyBottom) - bottomBounds.offset(0, dyBottom); - bounds.accumulate(bottomBounds); - lp.cx = Math.max(lp.cx, topCx) + CELL_WIDTH; - lp.cy = y0; - - layoutItem.bounds.width = lp.cx - layoutItem.bounds.x; - bounds.accumulate(layoutItem.bounds); - - if (layoutItem.topChildren.length === 0) { - layoutItem.topConnectorY = bounds.y + CELL_HEIGHT / 2; + if (numberOfOutputs === 2) { + output.push(( + + )); + output.push(( + + )); + } else if (numberOfOutputs === 1) { + output.push(( + + )); } else { - layoutItem.topConnectorY = layoutItem.topChildren[0].bounds.y + CELL_HEIGHT / 2; + output.push(( + + )); + } - if (layoutItem.bottomChildren.length === 0) { - layoutItem.bottomConnectorY = bounds.y + bounds.height - CELL_HEIGHT / 2; + } + renderSplitConnectors(output: ReactNode[], item: PedalLayout, enabled: boolean, shortSplitOutput: boolean,): void { + //const classes = withStyles.getClasses(this.props); + let x_ = item.bounds.x + CELL_WIDTH / 2; + let y_ = item.bounds.y + CELL_HEIGHT / 2; + let yTop = item.topConnectorY; + let yBottom = item.bottomConnectorY; + //let isStereo = item.stereoOutput; + let split = item.pedalItem as PedalboardSplitItem; + + let topEnabled = enabled && split.isASelected(); + let bottomEnabled = enabled && split.isBSelected(); + let topColor = topEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + let bottomColor = bottomEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + + + let topStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yTop).lineTo(x_ + CELL_WIDTH, yTop).toString(); + let bottomStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yBottom).lineTo(x_ + CELL_WIDTH, yBottom).toString(); + + if (item.numberOfInputs === 2 && item.topChildren[0].numberOfInputs === 2) { + output.push(()); + output.push(()); + } else if (item.numberOfInputs !== 0 && item.topChildren[0].numberOfInputs !== 0) { + output.push(()); + } + + if (item.numberOfInputs === 2 && item.bottomChildren[0].numberOfInputs === 2) { + output.push(()); + output.push(()); + + } else if (item.numberOfInputs !== 0 && item.bottomChildren[0].numberOfInputs !== 0) { + output.push(()); + } + + let lastTop = item.topChildren[item.topChildren.length - 1]; + let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; + + let xTop = lastTop.bounds.right - CELL_WIDTH / 2; + let xBottom = lastBottom.bounds.right - CELL_WIDTH / 2; + + let xEnd = shortSplitOutput ? item.bounds.right : item.bounds.right + CELL_WIDTH / 2; + let xTee0 = item.bounds.right - CELL_WIDTH / 2; + + let firstPath: string; // top or bottom depending on draw order. + let secondPath: string; // top or bottom depending on draw order. + + let firstPathStereo: boolean; + let secondPathStereo: boolean; + let firstPathAbsent: boolean; + let secondPathAbsent: boolean; + let firstPathEnabled: boolean; + let secondPathEnabled: boolean; + let xTee: number; + + let monoAdjustment = (STEREO_STROKE_WIDTH - STROKE_WIDTH) / 2; + + let bottomPathFirst = topEnabled && !bottomEnabled; + let topPathFirst = bottomEnabled && !topEnabled; + + + // Third case: L/R stereo output, when both outputs are mono, requires a third stroke. + let thirdPath: string | null = null; // for L/R stereo output (which can be stereo even if both outputs are mono) + let hasThirdPath = item.numberOfOutputs === 2 && (lastTop.numberOfOutputs !== 2) && (lastBottom.numberOfOutputs !== 2); + + if (hasThirdPath) { + firstPathStereo = false; + secondPathStereo = false; + firstPathAbsent = lastTop.numberOfOutputs === 0 || item.numberOfOutputs === 0; + secondPathAbsent = lastBottom.numberOfOutputs === 0 || item.numberOfOutputs === 0; + xTee = xTee0 - monoAdjustment; + firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); + + secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); + + hasThirdPath = true; + thirdPath = new SvgPathBuilder().moveTo(xTee0, y_).lineTo(xEnd, y_).toString(); + firstPathEnabled = bottomEnabled; + secondPathEnabled = topEnabled; + } else if (bottomPathFirst || (topEnabled && lastTop.numberOfOutputs === 2)) { + // draw the bottom path first. + firstPathStereo = item.numberOfOutputs === 2 && lastBottom.numberOfOutputs === 2; + secondPathStereo = item.numberOfOutputs === 2 && lastTop.numberOfOutputs === 2; + firstPathAbsent = item.numberOfOutputs === 0 || lastBottom.numberOfOutputs === 0; + secondPathAbsent = item.numberOfOutputs === 0 || lastTop.numberOfOutputs === 0; + + xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; + firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); + xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; + + secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); + firstPathEnabled = bottomEnabled; + secondPathEnabled = topEnabled; } else { - layoutItem.bottomConnectorY = layoutItem.bottomChildren[0].bounds.y + CELL_HEIGHT / 2; + // draw the top path first. + firstPathStereo = item.numberOfOutputs === 2 && lastTop.numberOfOutputs === 2; + secondPathStereo = item.numberOfOutputs === 2 && lastBottom.numberOfOutputs === 2; + firstPathAbsent = item.numberOfOutputs === 0 || lastTop.numberOfOutputs === 0; + secondPathAbsent = item.numberOfOutputs === 0 || lastBottom.numberOfOutputs === 0; + + xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; + firstPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); + + xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; + secondPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); + + firstPathEnabled = topEnabled; + secondPathEnabled = bottomEnabled; } - } else { - layoutItem.bounds.x = lp.cx; - layoutItem.bounds.y = lp.cy; - lp.cx += CELL_WIDTH; - layoutItem.bounds.width = CELL_WIDTH; - layoutItem.bounds.height = CELL_HEIGHT; - bounds.accumulate(layoutItem.bounds); + let firstPathColor = firstPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + let secondPathColor = secondPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; + if (bottomPathFirst || topPathFirst) { + // display stereo strokes with cutoff line. + if (firstPathStereo) { + output.push(( + + )); + output.push(( + + )); + } else if (!firstPathAbsent) { + output.push(( + + )); + } + if (secondPathStereo) { + output.push(( + + )); + output.push(( + + )); + } else if (!secondPathAbsent) { + output.push(( + + )); + } + + } else { + // stereo strokes merge. + if (firstPathStereo) { + output.push(( + + )); + } else if (!firstPathAbsent) { + output.push(( + + )); + } + if (secondPathStereo) { + output.push(( + + )); + } else if (!secondPathAbsent) { + output.push(( + + )); + } + + // draw stereo inner lines. + if (firstPathStereo) { + output.push(( + + )); + } + if (secondPathStereo) { + output.push(( + + )); + + } + } + if (thirdPath != null) { + // stereo output of L/R splitter + output.push(( + + )); + output.push(( + + )); + + + } } - } - return bounds; - } - doLayout(layoutItems: PedalLayout[]): LayoutSize { - const TWO_ROW_HEIGHT = 142 - 14; - - if (layoutItems.length === 0) { - // if the current pedalboard is empty, reserve display space anyway. - return { width: 1, height: TWO_ROW_HEIGHT }; - } - - let lp = new LayoutParams(); - - let bounds = this.doLayout2_(lp, layoutItems); - // shift everything down so there are no negative y coordinates. - - if (bounds.height < TWO_ROW_HEIGHT) { - - let extra = Math.floor((TWO_ROW_HEIGHT - Math.ceil(bounds.height)) / 2); - this.offsetLayout_(layoutItems, Math.floor(-bounds.y + extra / 2)); - bounds.height += extra; - - } else { - this.offsetLayout_(layoutItems, -bounds.y); - } - - bounds.height += 14; // for labels that aren't accounted for. - return { width: bounds.width, height: bounds.height }; - - } - - onItemClick(e: SyntheticEvent, instanceId?: number): void { - if (instanceId) { - this.setSelection(instanceId); - } - } - setSelection(instanceId: number) { - if (this.props.onSelectionChanged) { - this.props.onSelectionChanged(instanceId); - } - } - - onItemDoubleClick(event: SyntheticEvent, instanceId?: number): void { - event.preventDefault(); - event.stopPropagation(); - - if (this.props.onDoubleClick && instanceId && this.props.enableStructureEditing) { - this.props.onDoubleClick(instanceId); - } - - } - - onItemLongClick(event: SyntheticEvent, instanceId?: number): void { - if (!instanceId) { - return; - } - event.preventDefault(); - event.stopPropagation(); - - if (!this.props.enableStructureEditing) { - this.setSelection(instanceId); - return; - } - if (!Utility.needsZoomedControls()) { - if (this.props.onDoubleClick && this.props.enableStructureEditing && instanceId) { - this.props.onDoubleClick(instanceId); - } - } - - } - - strokeConnector(output: ReactNode[], channels: number, enabled: Boolean, svgPath: string) { - let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - - if (channels === 2) { - output.push(( - - )); - output.push(( - - )); - } else if (channels === 1) { - output.push(( - - )); - } - } - - renderConnector(output: ReactNode[], item: PedalLayout, enabled: boolean): void { - // const classes = withStyles.getClasses(this.props); - let x_ = item.bounds.x + CELL_WIDTH / 2; - let y_ = item.bounds.y + CELL_HEIGHT / 2; - let numberOfOutputs = item.numberOfOutputs; - let color = enabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - let stereoCenterColor = this.bgColor; - - if (item.originalInputs === 0) { - // break the input paths. - let rx = item.bounds.x + CELL_WIDTH / 2 - FRAME_SIZE / 2 - 4; - let ry = y_ - 4; - - output.push(( - - )); - } - let svgPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_ + CELL_WIDTH, y_).toString(); - - - if (numberOfOutputs === 2) { - output.push(( - - )); - output.push(( - - )); - } else if (numberOfOutputs === 1) { - output.push(( - - )); - } else { - output.push(( - - )); - - } - } - renderSplitConnectors(output: ReactNode[], item: PedalLayout, enabled: boolean, shortSplitOutput: boolean,): void { - //const classes = withStyles.getClasses(this.props); - let x_ = item.bounds.x + CELL_WIDTH / 2; - let y_ = item.bounds.y + CELL_HEIGHT / 2; - let yTop = item.topConnectorY; - let yBottom = item.bottomConnectorY; - //let isStereo = item.stereoOutput; - let split = item.pedalItem as PedalboardSplitItem; - - let topEnabled = enabled && split.isASelected(); - let bottomEnabled = enabled && split.isBSelected(); - let topColor = topEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - let bottomColor = bottomEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - - - let topStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yTop).lineTo(x_ + CELL_WIDTH, yTop).toString(); - let bottomStartPath = new SvgPathBuilder().moveTo(x_, y_).lineTo(x_, yBottom).lineTo(x_ + CELL_WIDTH, yBottom).toString(); - - if (item.numberOfInputs === 2 && item.topChildren[0].numberOfInputs === 2) { - output.push(()); - output.push(()); - } else if (item.numberOfInputs !== 0 && item.topChildren[0].numberOfInputs !== 0) { - output.push(()); - } - - if (item.numberOfInputs === 2 && item.bottomChildren[0].numberOfInputs === 2) { - output.push(()); - output.push(()); - - } else if (item.numberOfInputs !== 0 && item.bottomChildren[0].numberOfInputs !== 0) { - output.push(()); - } - - let lastTop = item.topChildren[item.topChildren.length - 1]; - let lastBottom = item.bottomChildren[item.bottomChildren.length - 1]; - - let xTop = lastTop.bounds.right - CELL_WIDTH / 2; - let xBottom = lastBottom.bounds.right - CELL_WIDTH / 2; - - let xEnd = shortSplitOutput ? item.bounds.right : item.bounds.right + CELL_WIDTH / 2; - let xTee0 = item.bounds.right - CELL_WIDTH / 2; - - let firstPath: string; // top or bottom depending on draw order. - let secondPath: string; // top or bottom depending on draw order. - - let firstPathStereo: boolean; - let secondPathStereo: boolean; - let firstPathAbsent: boolean; - let secondPathAbsent: boolean; - let firstPathEnabled: boolean; - let secondPathEnabled: boolean; - let xTee: number; - - let monoAdjustment = (STEREO_STROKE_WIDTH - STROKE_WIDTH) / 2; - - let bottomPathFirst = topEnabled && !bottomEnabled; - let topPathFirst = bottomEnabled && !topEnabled; - - - // Third case: L/R stereo output, when both outputs are mono, requires a third stroke. - let thirdPath: string | null = null; // for L/R stereo output (which can be stereo even if both outputs are mono) - let hasThirdPath = item.numberOfOutputs === 2 && (lastTop.numberOfOutputs !== 2) && (lastBottom.numberOfOutputs !== 2); - - if (hasThirdPath) { - firstPathStereo = false; - secondPathStereo = false; - firstPathAbsent = lastTop.numberOfOutputs === 0 || item.numberOfOutputs === 0; - secondPathAbsent = lastBottom.numberOfOutputs === 0 || item.numberOfOutputs === 0; - xTee = xTee0 - monoAdjustment; - firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); - - secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); - - hasThirdPath = true; - thirdPath = new SvgPathBuilder().moveTo(xTee0, y_).lineTo(xEnd, y_).toString(); - firstPathEnabled = bottomEnabled; - secondPathEnabled = topEnabled; - } else if (bottomPathFirst || (topEnabled && lastTop.numberOfOutputs === 2)) { - // draw the bottom path first. - firstPathStereo = item.numberOfOutputs === 2 && lastBottom.numberOfOutputs === 2; - secondPathStereo = item.numberOfOutputs === 2 && lastTop.numberOfOutputs === 2; - firstPathAbsent = item.numberOfOutputs === 0 || lastBottom.numberOfOutputs === 0; - secondPathAbsent = item.numberOfOutputs === 0 || lastTop.numberOfOutputs === 0; - - xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; - firstPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).toString(); - xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; - - secondPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); - firstPathEnabled = bottomEnabled; - secondPathEnabled = topEnabled; - } else { - // draw the top path first. - firstPathStereo = item.numberOfOutputs === 2 && lastTop.numberOfOutputs === 2; - secondPathStereo = item.numberOfOutputs === 2 && lastBottom.numberOfOutputs === 2; - firstPathAbsent = item.numberOfOutputs === 0 || lastTop.numberOfOutputs === 0; - secondPathAbsent = item.numberOfOutputs === 0 || lastBottom.numberOfOutputs === 0; - - xTee = firstPathStereo ? xTee0 : xTee0 - monoAdjustment; - firstPath = new SvgPathBuilder().moveTo(xTop, yTop).lineTo(xTee, yTop).lineTo(xTee, y_).toString(); - - xTee = secondPathStereo ? xTee0 : xTee0 - monoAdjustment; - secondPath = new SvgPathBuilder().moveTo(xBottom, yBottom).lineTo(xTee, yBottom).lineTo(xTee, y_).lineTo(xEnd, y_).toString(); - - firstPathEnabled = topEnabled; - secondPathEnabled = bottomEnabled; - } - let firstPathColor = firstPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - let secondPathColor = secondPathEnabled ? ENABLED_CONNECTOR_COLOR : DISABLED_CONNECTOR_COLOR; - - if (bottomPathFirst || topPathFirst) { - // display stereo strokes with cutoff line. - if (firstPathStereo) { - output.push(( - - )); - output.push(( - - )); - } else if (!firstPathAbsent) { - output.push(( - - )); - } - if (secondPathStereo) { - output.push(( - - )); - output.push(( - - )); - } else if (!secondPathAbsent) { - output.push(( - - )); + getScrollContainer() { + let el: HTMLElement | undefined | null = this.scrollRef.current; + // actually not here anymore. :-/ It has a reactive definition in MainPage.tsx now. + while (el) { + if (el.id === "pedalboardScroll") { + return el as HTMLDivElement; + } + el = el.parentElement; + } + throw new PiPedalStateError("scroll container not found."); } - } else { - // stereo strokes merge. - if (firstPathStereo) { - output.push(( - - )); - } else if (!firstPathAbsent) { - output.push(( - - )); - } - if (secondPathStereo) { - output.push(( - - )); - } else if (!secondPathAbsent) { - output.push(( - - )); - } + pedalButton( + instanceId: number, + iconType: PluginType, + iconColor: string, + draggable: boolean, + enabled: boolean, + hasBorder: boolean = true, + pluginNotFound: boolean, + hasMidiConnector: boolean + ) + : ReactNode { + const classes = withStyles.getClasses(this.props); + let frameStyle = classes.iconFrame; + if (!hasBorder) { + frameStyle = classes.borderlessIconFrame; + } else { + if (instanceId === this.props.selectedId) { + frameStyle = classes.selectedIconFrame; + } + } - // draw stereo inner lines. - if (firstPathStereo) { - output.push(( - - )); - } - if (secondPathStereo) { - output.push(( - - )); + return ( +
{ e.preventDefault(); }} + > + {/* {!enabled && ( +
+ +
- } - } - if (thirdPath != null) { - // stereo output of L/R splitter - output.push(( - - )); - output.push(( - - )); + )} */} - - } - } - getScrollContainer() { - let el: HTMLElement | undefined | null = this.scrollRef.current; - // actually not here anymore. :-/ It has a reactive definition in MainPage.tsx now. - while (el) { - if (el.id === "pedalboardScroll") { - return el as HTMLDivElement; - } - el = el.parentElement; - } - throw new PiPedalStateError("scroll container not found."); - } - - pedalButton( - instanceId: number, - iconType: PluginType, - draggable: boolean, - enabled: boolean, - hasBorder: boolean = true, - pluginNotFound: boolean, - hasMidiConnector: boolean - ) - : ReactNode { - const classes = withStyles.getClasses(this.props); - return ( -
{ e.preventDefault(); }}> - {hasMidiConnector && midiChannelBindingControlFeatureEnabled && ( -
- -
- - )} - - { this.onItemClick(e, instanceId); }} - onDoubleClick={(e: SyntheticEvent) => { this.onItemDoubleClick(e, instanceId); }} - onContextMenu={(e: SyntheticEvent) => { this.onItemLongClick(e, instanceId); }} - > - - this.getScrollContainer()} - onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }} + { this.onItemClick(e, instanceId); }} + onDoubleClick={(e: SyntheticEvent) => { this.onItemDoubleClick(e, instanceId); }} + onContextMenu={(e: SyntheticEvent) => { this.onItemLongClick(e, instanceId); }} > - - - - -
- ); + + this.getScrollContainer()} + onDragEnd={(x, y) => { this.onDragEnd(instanceId, x, y) }} + style={{ opacity: enabled ? 0.99 : 0.3 }} - } - renderConnectors(output: ReactNode[], layoutChain: PedalLayout[], enabled: boolean, shortSplitOutput: boolean): void { - let length = layoutChain.length - 1; - if (layoutChain.length > 0 && layoutChain[layoutChain.length - 1].isSplitter()) { - ++length; - } - for (let i = 0; i < length; ++i) { - let item = layoutChain[i]; - if (item.isSplitter()) { - let splitter = item.pedalItem as PedalboardSplitItem; - this.renderSplitConnectors(output, item, enabled, i === length - 1 && shortSplitOutput,); - this.renderConnectors(output, item.topChildren, enabled && splitter.isASelected(), false); - this.renderConnectors(output, item.bottomChildren, enabled && splitter.isBSelected(), false); + > +
+ +
- } else if (item.uri !== END_PEDALBOARD_ITEM_URI) { - this.renderConnector(output, item, enabled); +
+
+ +
+ ); } - } - } - renderConnectorFrame(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { - let outputs: ReactNode[] = []; - this.renderConnectors(outputs, layoutChain, true, false); - return ( -
- - - { - outputs - } - - - -
- ); - - } - - renderChain(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { - - const classes = withStyles.getClasses(this.props); - - let result: ReactNode[] = []; - - result.push(this.renderConnectorFrame(layoutChain, layoutSize)); - - let it = chainIterator(layoutChain); - while (true) { - let v = it.next(); - if (v.done) break; - let item = v.value; - switch (item.uri) { - case START_PEDALBOARD_ITEM_URI: - result.push(
-
- - {this.pedalButton(START_CONTROL, item.pluginType, false, true, false, false, false)} -
-
); - break; - case END_PEDALBOARD_ITEM_URI: - result.push(
-
- - {this.pedalButton(END_CONTROL, item.pluginType, false, true, false, false, false)} -
-
); - break; - default: + renderConnectors(output: ReactNode[], layoutChain: PedalLayout[], enabled: boolean, shortSplitOutput: boolean): void { + let length = layoutChain.length - 1; + if (layoutChain.length > 0 && layoutChain[layoutChain.length - 1].isSplitter()) { + ++length; + } + for (let i = 0; i < length; ++i) { + let item = layoutChain[i]; if (item.isSplitter()) { + let splitter = item.pedalItem as PedalboardSplitItem; + this.renderSplitConnectors(output, item, enabled, i === length - 1 && shortSplitOutput,); + this.renderConnectors(output, item.topChildren, enabled && splitter.isASelected(), false); + this.renderConnectors(output, item.bottomChildren, enabled && splitter.isBSelected(), false); - result.push(
-
- {this.pedalButton(item.pedalItem?.instanceId ?? -1, this.getSplitterIcon(item), false, true, true, false, false)} -
-
); + } else if (item.uri !== END_PEDALBOARD_ITEM_URI) { + this.renderConnector(output, item, enabled); - } else { - result.push( -
- {item.name} -
- ) - let uiPlugin = this.model.getUiPlugin(item.pedalItem?.uri ?? ""); - let pluginMissing = uiPlugin === null; - let pluginType = item.pluginType; - if (uiPlugin && uiPlugin.uri === "http://two-play.com/plugins/toob-nam") - { - pluginType = PluginType.NamPlugin; + } + } + } + renderConnectorFrame(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { + let outputs: ReactNode[] = []; + this.renderConnectors(outputs, layoutChain, true, false); + return ( +
+ + + { + outputs + } + + +
+ ); + + } + + renderChain(layoutChain: PedalLayout[], layoutSize: LayoutSize): ReactNode { + + const classes = withStyles.getClasses(this.props); + + let result: ReactNode[] = []; + + result.push(this.renderConnectorFrame(layoutChain, layoutSize)); + + let it = chainIterator(layoutChain); + while (true) { + let v = it.next(); + if (v.done) break; + let item = v.value; + switch (item.uri) { + case START_PEDALBOARD_ITEM_URI: + result.push(
+
+ + {this.pedalButton(START_CONTROL, item.pluginType, item.iconColor, false, true, false, false, false)} +
+
); + break; + case END_PEDALBOARD_ITEM_URI: + result.push(
+
+ + {this.pedalButton(END_CONTROL, item.pluginType, "", false, true, false, false, false)} +
+
); + break; + default: + if (item.isSplitter()) { + + result.push(
+
+ {this.pedalButton(item.pedalItem?.instanceId ?? -1, this.getSplitterIcon(item), "", false, true, true, false, false)} +
+
); + + } else { + result.push( +
+ {item.name} +
+ ) + let uiPlugin = this.model.getUiPlugin(item.pedalItem?.uri ?? ""); + let pluginMissing = uiPlugin === null; + let pluginType = item.pluginType; + if (uiPlugin && uiPlugin.uri === "http://two-play.com/plugins/toob-nam") { + pluginType = PluginType.NamPlugin; + + } + + result.push(
+ {this.pedalButton( + item.pedalItem?.instanceId ?? -1, + pluginType, + item.pedalItem?.iconColor ?? "", + !item.isEmpty(), + item.pedalItem?.isEnabled ?? false, + true, + pluginMissing, + uiPlugin ? ((uiPlugin.has_midi_input !== 0) || (uiPlugin.has_midi_output !== 0)) : false)} + +
); + + } + break; + } + + } + return result; + } + + canInputStero(item: PedalLayout): boolean { + if (item.pedalItem) { + let plugin = this.model.getUiPlugin(item.pedalItem.uri); + if (plugin) { + return plugin.audio_inputs === 2; + } + } + return true; + } + getNumberOfInputs(item: PedalLayout): number { + if (item.pedalItem) { + let plugin = this.model.getUiPlugin(item.pedalItem.uri); + if (plugin) { + return plugin.audio_inputs; + } + } + return 1; + } + getNumberOfOutputs(item: PedalLayout): number { + if (item.pedalItem) { + let plugin = this.model.getUiPlugin(item.pedalItem.uri); + if (plugin) { + return plugin.audio_outputs; + } + } + return 1; + } + + markStereoOutputs(layoutChain: PedalLayout[], numberOfInputs: number, numberOfOutputs: number) { + // analyze forward flow. + this.markStereoForward(layoutChain, numberOfInputs); + // mark items that feed a mono effect as mono. + this.markStereoBackward(layoutChain, numberOfOutputs); + } + + markStereoBackward(layoutChain: PedalLayout[], numberOfOutputs: number): number { + for (let i = layoutChain.length - 1; i >= 0; --i) { + let item = layoutChain[i]; + if (item.isSplitter()) { + item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs) + + this.markStereoBackward(item.topChildren, numberOfOutputs); + this.markStereoBackward(item.bottomChildren, numberOfOutputs); + let topInputs = item.topChildren[0].numberOfInputs; + let bottomInputs = item.bottomChildren[0].numberOfInputs; + + let splitItem = item.pedalItem as PedalboardSplitItem; + if (splitItem.getSplitType() !== SplitType.Lr) { + item.numberOfInputs = CalculateConnection(item.numberOfInputs, Math.max(topInputs, bottomInputs)); } + } else if (item.isEnd()) { - result.push(
- {this.pedalButton( - item.pedalItem?.instanceId ?? -1, - pluginType, - !item.isEmpty(), - item.pedalItem?.isEnabled ?? false, - true, - pluginMissing, - uiPlugin ? ((uiPlugin.has_midi_input !== 0) || (uiPlugin.has_midi_output !== 0)) : false)} + } else if (item.isStart()) { + item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); + return item.numberOfOutputs; -
); - - } - break; - } - - } - return result; - } - - canInputStero(item: PedalLayout): boolean { - if (item.pedalItem) { - let plugin = this.model.getUiPlugin(item.pedalItem.uri); - if (plugin) { - return plugin.audio_inputs === 2; - } - } - return true; - } - getNumberOfInputs(item: PedalLayout): number { - if (item.pedalItem) { - let plugin = this.model.getUiPlugin(item.pedalItem.uri); - if (plugin) { - return plugin.audio_inputs; - } - } - return 1; - } - getNumberOfOutputs(item: PedalLayout): number { - if (item.pedalItem) { - let plugin = this.model.getUiPlugin(item.pedalItem.uri); - if (plugin) { - return plugin.audio_outputs; - } - } - return 1; - } - - markStereoOutputs(layoutChain: PedalLayout[], numberOfInputs: number, numberOfOutputs: number) { - // analyze forward flow. - this.markStereoForward(layoutChain, numberOfInputs); - // mark items that feed a mono effect as mono. - this.markStereoBackward(layoutChain, numberOfOutputs); - } - - markStereoBackward(layoutChain: PedalLayout[], numberOfOutputs: number): number { - for (let i = layoutChain.length - 1; i >= 0; --i) { - let item = layoutChain[i]; - if (item.isSplitter()) { - item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs) - - this.markStereoBackward(item.topChildren, numberOfOutputs); - this.markStereoBackward(item.bottomChildren, numberOfOutputs); - let topInputs = item.topChildren[0].numberOfInputs; - let bottomInputs = item.bottomChildren[0].numberOfInputs; - - let splitItem = item.pedalItem as PedalboardSplitItem; - if (splitItem.getSplitType() !== SplitType.Lr) { - item.numberOfInputs = CalculateConnection(item.numberOfInputs, Math.max(topInputs, bottomInputs)); - } - } else if (item.isEnd()) { - - } else if (item.isStart()) { - item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); - return item.numberOfOutputs; - - } else if (item.isEmpty()) { - if (numberOfOutputs === 0) { - item.numberOfOutputs = 0; - item.numberOfInputs = CalculateConnection(item.numberOfInputs, 2); - } else { - item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); - item.numberOfInputs = CalculateConnection(item.numberOfInputs, numberOfOutputs); - } - } else { - item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); - } - numberOfOutputs = item.numberOfInputs; - } - return numberOfOutputs; - } - markStereoForward(layoutChain: PedalLayout[], numberOfInputs: number): number { - if (layoutChain.length === 0) { - return numberOfInputs; - } - for (let i = 0; i < layoutChain.length; ++i) { - let item = layoutChain[i]; - if (item.isSplitter()) { - let splitter = item.pedalItem as PedalboardSplitItem; - item.numberOfInputs = numberOfInputs; - - let chainInputs = numberOfInputs; - if (splitter.getSplitType() === SplitType.Lr) { - chainInputs = CalculateConnection(numberOfInputs, 1); - } - let topOutputs = this.markStereoForward(item.topChildren, chainInputs); - let bottomOutputs = this.markStereoForward(item.bottomChildren, chainInputs); - - - if (splitter.getSplitType() === SplitType.Ab) { - if (splitter.isASelected()) { - item.numberOfOutputs = topOutputs; + } else if (item.isEmpty()) { + if (numberOfOutputs === 0) { + item.numberOfOutputs = 0; + item.numberOfInputs = CalculateConnection(item.numberOfInputs, 2); + } else { + item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); + item.numberOfInputs = CalculateConnection(item.numberOfInputs, numberOfOutputs); + } } else { - item.numberOfOutputs = bottomOutputs; + item.numberOfOutputs = CalculateConnection(item.numberOfOutputs, numberOfOutputs); } - } else { - item.numberOfOutputs = (topOutputs >= 1 || bottomOutputs >= 1) ? 2 : 1; - } - } else if (item.isStart()) { - item.numberOfOutputs = Math.min(PiPedalModelFactory.getInstance().jackSettings.get().inputAudioPorts.length, 2); - } else if (item.isEnd()) { - item.numberOfInputs = - CalculateConnection( - Math.min(PiPedalModelFactory.getInstance().jackSettings.get().outputAudioPorts.length, 2), - numberOfInputs); - return item.numberOfInputs; - } else if (item.isEmpty()) { - item.numberOfInputs = numberOfInputs; - if (numberOfInputs === 0) { - item.numberOfOutputs = 2; - } else { - item.numberOfOutputs = item.numberOfInputs; - } - } else { - if (item.numberOfInputs === 0) // zero-input plugins merge their output with the input. - { - item.numberOfInputs = numberOfInputs; - item.numberOfOutputs = Math.max(item.numberOfOutputs, numberOfInputs); - } else { - item.numberOfInputs = CalculateConnection(numberOfInputs, this.getNumberOfInputs(item)); - item.numberOfOutputs = this.getNumberOfOutputs(item); + numberOfOutputs = item.numberOfInputs; } + return numberOfOutputs; } - numberOfInputs = item.numberOfOutputs; - } - return numberOfInputs; - } + markStereoForward(layoutChain: PedalLayout[], numberOfInputs: number): number { + if (layoutChain.length === 0) { + return numberOfInputs; + } + for (let i = 0; i < layoutChain.length; ++i) { + let item = layoutChain[i]; + if (item.isSplitter()) { + let splitter = item.pedalItem as PedalboardSplitItem; + item.numberOfInputs = numberOfInputs; - currentLayout?: PedalLayout[]; - private renderKey: number = 0; - render() { - const classes = withStyles.getClasses(this.props); - this.renderKey = 0; - let layoutChain = makeChain(this.model, this.state.pedalboard?.items); - let start = PedalLayout.Start(); - let end = PedalLayout.End(); - if (layoutChain.length !== 0) { - layoutChain.splice(0, 0, start); - layoutChain.splice(layoutChain.length, 0, end); - this.markStereoOutputs(layoutChain, 2, 2); - } - - let layoutSize = this.doLayout(layoutChain); + let chainInputs = numberOfInputs; + if (splitter.getSplitType() === SplitType.Lr) { + chainInputs = CalculateConnection(numberOfInputs, 1); + } + let topOutputs = this.markStereoForward(item.topChildren, chainInputs); + let bottomOutputs = this.markStereoForward(item.bottomChildren, chainInputs); - this.currentLayout = layoutChain; // save for mouse processing &c. + if (splitter.getSplitType() === SplitType.Ab) { + if (splitter.isASelected()) { + item.numberOfOutputs = topOutputs; + } else { + item.numberOfOutputs = bottomOutputs; + } + } else { + item.numberOfOutputs = (topOutputs >= 1 || bottomOutputs >= 1) ? 2 : 1; + } + } else if (item.isStart()) { + item.numberOfOutputs = Math.min(PiPedalModelFactory.getInstance().jackSettings.get().inputAudioPorts.length, 2); + } else if (item.isEnd()) { + item.numberOfInputs = + CalculateConnection( + Math.min(PiPedalModelFactory.getInstance().jackSettings.get().outputAudioPorts.length, 2), + numberOfInputs); + return item.numberOfInputs; + } else if (item.isEmpty()) { + item.numberOfInputs = numberOfInputs; + if (numberOfInputs === 0) { + item.numberOfOutputs = 2; + } else { + item.numberOfOutputs = item.numberOfInputs; + } + } else { + if (item.numberOfInputs === 0) // zero-input plugins merge their output with the input. + { + item.numberOfInputs = numberOfInputs; + item.numberOfOutputs = Math.max(item.numberOfOutputs, numberOfInputs); + } else { + item.numberOfInputs = CalculateConnection(numberOfInputs, this.getNumberOfInputs(item)); + item.numberOfOutputs = this.getNumberOfOutputs(item); + } + } + numberOfInputs = item.numberOfOutputs; + } + return numberOfInputs; + } - return ( -
-
- {this.renderChain(layoutChain, layoutSize)} -
-
- ); - } + currentLayout?: PedalLayout[]; + private renderKey: number = 0; + render() { + const classes = withStyles.getClasses(this.props); + this.renderKey = 0; + let layoutChain = makeChain(this.model, this.state.pedalboard?.items); + let start = PedalLayout.Start(); + let end = PedalLayout.End(); + if (layoutChain.length !== 0) { + layoutChain.splice(0, 0, start); + layoutChain.splice(layoutChain.length, 0, end); + this.markStereoOutputs(layoutChain, 2, 2); + } - }, - pedalboardStyles -)); + let layoutSize = this.doLayout(layoutChain); + + + this.currentLayout = layoutChain; // save for mouse processing &c. + + return ( +
+
+ {this.renderChain(layoutChain, layoutSize)} +
+
+ ); + } + + }, + pedalboardStyles + )); export default PedalboardView \ No newline at end of file diff --git a/vite/src/pipedal/PerformanceView.tsx b/vite/src/pipedal/PerformanceView.tsx index c05142b..c360e1d 100644 --- a/vite/src/pipedal/PerformanceView.tsx +++ b/vite/src/pipedal/PerformanceView.tsx @@ -19,16 +19,17 @@ import { Theme } from '@mui/material/styles'; +import BankIcon from './svg/ic_bank.svg?react'; +import Typography from '@mui/material/Typography'; import SaveIconOutline from '@mui/icons-material/Save'; import WithStyles from './WithStyles'; -import {createStyles} from './WithStyles'; +import { createStyles } from './WithStyles'; import { withStyles } from "tss-react/mui"; import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import IconButtonEx from './IconButtonEx'; -import AppBar from '@mui/material/AppBar'; import SnapshotPanel from './SnapshotPanel'; import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; @@ -41,13 +42,17 @@ import { BankIndex } from './Banks'; import SnapshotEditor from './SnapshotEditor'; import { Snapshot } from './Pedalboard'; import JackStatusView from './JackStatusView'; -import {IDialogStackable, popDialogStack, pushDialogStack} from './DialogStack'; +import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack'; import { css } from '@emotion/react'; const selectColor = isDarkMode() ? "#888" : "#FFFFFF"; const styles = (theme: Theme) => createStyles({ + listIcon: css({ + width: 24, height: 24, + opacity: 0.6, fill: theme.palette.text.primary + }), frame: css({ position: "absolute", display: "flex", flexDirection: "column", flexWrap: "nowrap", justifyContent: "flex-start", left: "0px", top: "0px", bottom: "0px", right: "0px", overflow: "hidden" @@ -75,7 +80,8 @@ interface PerformanceViewProps extends WithStyles { } interface PerformanceViewState { - wrapSelects: boolean + wrapSelects: boolean; + largeAppBar: boolean; presets: PresetIndex; banks: BankIndex; showSnapshotEditor: boolean; @@ -86,7 +92,7 @@ interface PerformanceViewState { export const PerformanceView = - withStyles( + withStyles( class extends ResizeResponsiveComponent implements IDialogStackable { model: PiPedalModel; @@ -100,6 +106,7 @@ export const PerformanceView = presets: this.model.presets.get(), banks: this.model.banks.get(), wrapSelects: false, + largeAppBar: this.updateAppBarZoom(), showSnapshotEditor: false, showStatusMonitor: this.model.showStatusMonitor.get(), snapshotEditorIndex: 0, @@ -112,6 +119,9 @@ export const PerformanceView = } + updateAppBarZoom() { + return this.windowSize.width > 700; + } showStatusMonitorHandler() { this.setState({ @@ -119,7 +129,7 @@ export const PerformanceView = }); } - getTag() { return "performView"} + getTag() { return "performView" } isOpen() { return this.props.open } onDialogStackClose() { this.props.onClose(); @@ -128,15 +138,13 @@ export const PerformanceView = private hasHooks: boolean = false; - updateHooks() : void { + updateHooks(): void { let wantHooks = this.mounted && this.props.open; - if (wantHooks !== this.hasHooks) - { + if (wantHooks !== this.hasHooks) { this.hasHooks = wantHooks; - - if (this.hasHooks) - { + + if (this.hasHooks) { pushDialogStack(this); } else { @@ -144,9 +152,12 @@ export const PerformanceView = } } } - + onWindowSizeChanged(width: number, height: number): void { - this.setState({ wrapSelects: width < 700 }); + this.setState({ + wrapSelects: width < 700, + largeAppBar: this.updateAppBarZoom(), + }); } onPresetsChanged(newValue: PresetIndex) { @@ -156,7 +167,7 @@ export const PerformanceView = this.setState({ banks: this.model.banks.get() }) } onPresetChangedChanged(newValue: boolean) { - this.setState({ presetModified:newValue}); + this.setState({ presetModified: newValue }); } private mounted: boolean = false; @@ -216,84 +227,145 @@ export const PerformanceView = }); return true; } - handleSnapshotEditOk(index: number, name: string, color: string,newSnapshots: (Snapshot|null)[]) - { + handleSnapshotEditOk(index: number, name: string, color: string, newSnapshots: (Snapshot | null)[]) { // results have been sent to the server. we would perform a short-cut commit to our local state, to avoid laggy display updates. // but we don't actually have that in our state. } - handlePreviousBank() - { + handlePreviousBank() { this.model.previousBank(); } - handleNextBank() - { + handleNextBank() { this.model.nextBank(); } - handlePreviousPreset() - { + handlePreviousPreset() { this.model.previousPreset(); } - handleNextPreset() - { + handleNextPreset() { this.model.nextPreset(); } render() { const classes = withStyles.getClasses(this.props); - let wrapSelects = this.state.wrapSelects; let presets = this.state.presets; let banks = this.state.banks; + let appBarIconSize: "large" | undefined = this.state.largeAppBar ? undefined : "large"; return (
- -
- { this.props.onClose(); }} - style={{ - position: "relative",top: 3, - flex: "0 0 auto" }} > - - - + {this.state.largeAppBar ? (
- {/********* BANKS *******************/} -
- { this.handlePreviousBank(); }} - size="medium" - color="inherit" - style={{ - borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 - }} - > - - + { this.props.onClose(); }} + style={{ + position: "relative", top: 3, + flex: "0 0 auto" + }} > + + + {/********* PRESETS *******************/} +
+ { this.handlePreviousPreset(); }} + color="inherit" + style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }} + > + + + + { this.handleNextPreset(); }} + color="inherit" + style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }} + > + + + + { this.model.saveCurrentPreset(); }} + size="medium" + color="inherit" + style={{ flexShrink: 0, visibility: this.state.presetModified ? "visible" : "hidden", marginRight: 16 }} + > + + +
+ {/********* BANKS *******************/} + +
+ - - { this.handleNextBank(); }} - color="inherit" - style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }} - > - - - - {/** spacer */} - { this.handleNextBank(); }} - size="medium" - color="inherit" - style={{visibility: "hidden"}} - > - - - -
- {/********* PRESETS *******************/} -
- { this.handlePreviousPreset(); }} - color="inherit" - style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }} - > - - - - - { this.handleNextPreset(); }} - color="inherit" - style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }} - > - - - - { this.model.saveCurrentPreset(); }} - size="medium" - color="inherit" - style={{flexShrink: 0,visibility: this.state.presetModified? "visible": "hidden"}} - > - - - -
-
- -
+ ) : ( + +
+ { this.props.onClose(); }} + style={{ + position: "relative", top: 3, + flex: "0 0 auto" + }} > + + + +
+ {/********* BANKS *******************/} + + +
+ + +
+ + {/**** 2nd row ****/} +
+ {/********* PRESETS *******************/} + + { this.handlePreviousPreset(); }} + color="inherit" + style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }} + > + + + + + + { this.handleNextPreset(); }} + color="inherit" + style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }} + > + + + + { this.model.saveCurrentPreset(); }} + size="medium" + color="inherit" + style={{ flexShrink: 0, visibility: this.state.presetModified ? "visible" : "hidden" }} + > + + + +
+ + )} +
+
{ return this.handleOnEdit(index); }} />
@@ -413,15 +528,15 @@ export const PerformanceView = )}
{this.state.showSnapshotEditor && ( - { + { this.setState({ showSnapshotEditor: false }); }} - onOk={(index,name,color,newSnapshots)=>{ - this.setState({ showSnapshotEditor: false}); - this.handleSnapshotEditOk(index,name,color,newSnapshots); + onOk={(index, name, color, newSnapshots) => { + this.setState({ showSnapshotEditor: false }); + this.handleSnapshotEditOk(index, name, color, newSnapshots); }} - /> + /> )}
) diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index a42cef1..88c2882 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -491,6 +491,9 @@ export class PiPedalModel //implements PiPedalModel webSocket?: PiPedalSocket; + static getInstance(): PiPedalModel { + return PiPedalModelFactory.getInstance(); + } hasTone3000Auth: ObservableProperty = new ObservableProperty(false); canKeepScreenOn: boolean = false; @@ -2046,13 +2049,14 @@ export class PiPedalModel //implements PiPedalModel } - saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise { + saveCurrentPresetAs(bankInstanceId: number, newName: string, saveAfterInstanceId = -1): Promise { // default behaviour is to save after the currently selected preset. if (saveAfterInstanceId === -1) { saveAfterInstanceId = this.presets.get().selectedInstanceId; } let request: any = { clientId: this.clientId, + bankInstanceId: bankInstanceId, name: newName, saveAfterInstanceId: saveAfterInstanceId @@ -2061,7 +2065,9 @@ export class PiPedalModel //implements PiPedalModel return nullCast(this.webSocket) .request("saveCurrentPresetAs", request) .then((newPresetId) => { - this.loadPreset(newPresetId); + if (bankInstanceId === this.banks.get().selectedBank) { + this.loadPreset(newPresetId); + } return newPresetId; }); } @@ -2186,8 +2192,8 @@ export class PiPedalModel //implements PiPedalModel } - deletePresetItem(instanceId: number): Promise { - return nullCast(this.webSocket).request("deletePresetItem", instanceId); + deletePresetItems(instanceIds: Set): Promise { + return nullCast(this.webSocket).request("deletePresetItems", Array.from(instanceIds)); } deleteBankItem(instanceId: number): Promise { @@ -3376,9 +3382,9 @@ export class PiPedalModel //implements PiPedalModel this.cancelOnNetworkChanging(); }, 30 * 1000); - } - setPedalboardItemTitle(instanceId: number, title: string): void { + + setPedalboardItemTitle(instanceId: number, title: string, iconColor: string): void { let pedalboard = this.pedalboard.get(); if (!pedalboard) { throw new PiPedalStateError("Pedalboard not loaded."); @@ -3386,13 +3392,14 @@ export class PiPedalModel //implements PiPedalModel let newPedalboard = pedalboard.clone(); this.updateVst3State(newPedalboard); let item = newPedalboard.getItem(instanceId); - if (item.title === title) { + if (item.title === title && item.iconColor === iconColor) { return; } item.title = title; + item.iconColor = iconColor; this.pedalboard.set(newPedalboard); // notify the server. - this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title }); + this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title, colorKey: iconColor }); } setAlsaSequencerConfiguration(alsaSequencerConfiguration: AlsaSequencerConfiguration): void { this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration); @@ -3535,6 +3542,16 @@ export class PiPedalModel //implements PiPedalModel this.webSocket?.send("setSelectedPedalboardPlugin", { clientId: this.clientId, pluginInstanceId: pluginId }); } + requestBankPresets(bankInstanceId: number): Promise { + return nullCast(this.webSocket).request("requestBankPresets", {bankInstanceId: bankInstanceId}); + } + + importPresetsFromBank(bankInstanceId: number, presets: number[]): Promise { + return nullCast(this.webSocket).request("importPresetsFromBank", {bankInstanceId: bankInstanceId, presets: presets}); + } + copyPresetsToBank(bankInstanceId: number, presets: number[]): Promise { + return nullCast(this.webSocket).request("copyPresetsToBank", {bankInstanceId: bankInstanceId, presets: presets}); + } }; let instance: PiPedalModel | undefined = undefined; diff --git a/vite/src/pipedal/PluginControl.tsx b/vite/src/pipedal/PluginControl.tsx index 3af35d4..80065cb 100644 --- a/vite/src/pipedal/PluginControl.tsx +++ b/vite/src/pipedal/PluginControl.tsx @@ -307,7 +307,7 @@ const PluginControl = e.stopPropagation(); } - isValidPointer(e: PointerEvent): boolean { + isValidPointer(e: PointerEvent): boolean { if (e.pointerType === "mouse") { return e.button === 0; } else if (e.pointerType === "pen") { @@ -347,7 +347,7 @@ const PluginControl = pointerId: number = 0; pointerType: string = ""; - isCapturedPointer(e: PointerEvent): boolean { + isCapturedPointer(e: PointerEvent): boolean { return this.mouseDown && e.pointerId === this.pointerId && e.pointerType === this.pointerType; @@ -361,7 +361,7 @@ const PluginControl = pointersDown: number = 0; captureElement?: SVGSVGElement = undefined; - onPointerDown(e: PointerEvent): void { + onPointerDown(e: PointerEvent): void { if (!this.mouseDown && this.isValidPointer(e)) { e.preventDefault(); e.stopPropagation(); @@ -387,7 +387,7 @@ const PluginControl = this.mouseDown = true; if (this.pointersDown === 1) { this.isTap = true; - this.tapStartMs = Date.now(); + this.tapStartMs = e.timeStamp; } else { this.isTap = false; } @@ -429,7 +429,7 @@ const PluginControl = } - onPointerLostCapture(e: PointerEvent) { + onPointerLostCapture(e: PointerEvent) { if (this.isCapturedPointer(e)) { if (this.pointersDown !== 0) { --this.pointersDown; @@ -442,7 +442,7 @@ const PluginControl = } - updateRange(e: PointerEvent): number { + updateRange(e: PointerEvent): number { let ultraHigh = false; let high = false; @@ -486,9 +486,11 @@ const PluginControl = } } } - onPointerTap() { - let tapTime = Date.now(); + onPointerTap(e: PointerEvent) { + let tapTime = e.timeStamp; let dT = tapTime - this.lastTapMs; + + this.lastTapMs = tapTime; if (dT < 500) { @@ -497,7 +499,7 @@ const PluginControl = } private tapStartMs: number = 0; - onPointerUp(e: PointerEvent) { + onPointerUp(e: PointerEvent) { if (this.isCapturedPointer(e)) { if (this.pointersDown !== 0) { @@ -514,9 +516,9 @@ const PluginControl = this.releaseCapture(e); if (this.isTap) { - let ms = Date.now() - this.tapStartMs; + let ms = e.timeStamp - this.tapStartMs; if (ms < 200) { - this.onPointerTap(); + this.onPointerTap(e); } } // prevent click from firing on other elements @@ -531,7 +533,7 @@ const PluginControl = } } - releaseCapture(e: PointerEvent) { + releaseCapture(e: PointerEvent) { let img = this.imgRef.current; if (img && img.style) { @@ -556,7 +558,7 @@ const PluginControl = clickSlop() { return 5; // maybe larger on touch devices. } - onPointerMove(e: PointerEvent): void { + onPointerMove(e: PointerEvent): void { if (this.isCapturedPointer(e)) { e.preventDefault(); let dRange = this.updateRange(e) @@ -1138,7 +1140,7 @@ const PluginControl = ); } /* - isSamePointer(PointerEvent e): boolean + isSamePointer(PointerEvent e): boolean { return e.pointerId === this.pointerId && e.pointerType === this.pointerType; diff --git a/vite/src/pipedal/PluginControlView.tsx b/vite/src/pipedal/PluginControlView.tsx index ddc77c9..66ef9fb 100644 --- a/vite/src/pipedal/PluginControlView.tsx +++ b/vite/src/pipedal/PluginControlView.tsx @@ -477,6 +477,7 @@ const PluginControlView = return (( )); diff --git a/vite/src/pipedal/PluginIcon.tsx b/vite/src/pipedal/PluginIcon.tsx index e034eb5..2365c2e 100644 --- a/vite/src/pipedal/PluginIcon.tsx +++ b/vite/src/pipedal/PluginIcon.tsx @@ -23,7 +23,6 @@ import WithStyles from './WithStyles'; import {createStyles} from './WithStyles'; import { withStyles } from "tss-react/mui"; -import { PiPedalModelFactory } from "./PiPedalModel"; import { PluginType } from './Lv2Plugin'; @@ -67,6 +66,104 @@ import FxEmptyIcon from './svg/fx_empty.svg?react'; import FxTerminalIcon from './svg/fx_terminal.svg?react'; import { isDarkMode } from './DarkMode'; +import { PiPedalModel } from './PiPedalModel'; + +export interface IconColorSelect { + key: string, + value: string | undefined, +}; + +export const getIconColorSelections = (darkMode?: boolean): IconColorSelect[] => { + if (darkMode === undefined) darkMode = isDarkMode(); + let result: IconColorSelect[] = []; + + let l: number; + let c: number; + if (darkMode) { + l = 0.8 + c = 0.25; + } else { + l = 0.5; + c = 0.20; + } + + for (let angle = 0; angle < 360; angle += 22.5) { + if (angle === 270-22.5) + { + // for some reason, this one is useless. Patch in a more useful light blue. :-/ + let a2 = 270; + let l2: number; + let c2: number; + if (darkMode) { + l2 = 0.9 + c2 = 0.25; + } else { + l2 = 0.7; + c2 = 0.20; + } + result.push( + { + key: "oklch" + Math.round(angle*10).toString(), + value: ("oklch(" + l2.toString() + " " + c2.toString() + " " + (a2).toString() + ")") + + } + ); + + } else if (angle === 270-45) + { + // for some reason, this one is useless. Patch in a more useful light blue. :-/ + let a2 = 180; + let l2: number; + let c2: number; + if (darkMode) { + l2 = 0.95 + c2 = 0.25; + } else { + l2 = 0.75; + c2 = 0.20; + } + result.push( + { + key: "oklch" + Math.round(angle*10).toString(), + value: ("oklch(" + l2.toString() + " " + c2.toString() + " " + (a2).toString() + ")") + + } + ); + + } else { + result.push( + { + key: "oklch" + Math.round(angle*10).toString(), + value: ("oklch(" + l.toString() + " " + c.toString() + " " + (angle).toString() + ")") + + } + ); + } + } + return result; + +}; + + +function makeDefaultColorMap(): {[key:string]: string} { + let result: {[key:string]: string} = {}; + for (let selection of getIconColorSelections()) + { + result[selection.key] = selection.value??""; + } + return result; +} + +const iconColors: {[key:string]: string} = makeDefaultColorMap(); + +export const getIconColor = (key?: string): string| undefined => { + if (iconColors.hasOwnProperty(key!)) + { + return iconColors[key!]; + } + return undefined; +} + @@ -86,13 +183,13 @@ const styles = (theme: Theme) => export interface PluginIconProps extends WithStyles { size?: number; opacity?: number; + color?: string; offsetY?: number; pluginType: PluginType; pluginMissing?: boolean; } -export function SelectSvgIcon(plugin_type: PluginType, className: string, size: number = 24, opacity: number = 1.0, missingPlugin: boolean = false) { - let color = ""; +export function SelectSvgIcon(plugin_type: PluginType, className: string, size: number = 24, opacity: number = 1.0, missingPlugin: boolean = false, color = "") { if (missingPlugin) { color = isDarkMode() ? "#C00000" : "#B00000"; } @@ -264,11 +361,11 @@ export function SelectBaseIcon(plugin_type: PluginType): string { export function SelectIconUri(plugin_type: PluginType) { let icon = SelectBaseIcon(plugin_type); - return PiPedalModelFactory.getInstance().svgImgUrl(icon); + return PiPedalModel.getInstance().svgImgUrl(icon); } const PluginIcon = withStyles((props: PluginIconProps) => { - const { pluginType, opacity, pluginMissing } = props; + const { pluginType, opacity, pluginMissing,color } = props; const classes = withStyles.getClasses(props); let pluginMissing_: boolean = pluginMissing ?? false; @@ -277,7 +374,7 @@ const PluginIcon = withStyles((props: PluginIconProps) => { if (props.size) size = props.size; let topVal: number = (props.offsetY ?? 0); - let svgIcon = SelectSvgIcon(pluginType, classes.icon, size, opacity, pluginMissing_); + let svgIcon = SelectSvgIcon(pluginType, classes.icon, size, opacity, pluginMissing_,color? color: "" ); if (svgIcon) { return svgIcon; } diff --git a/vite/src/pipedal/PluginNameDialog.tsx b/vite/src/pipedal/PluginNameDialog.tsx new file mode 100644 index 0000000..864d518 --- /dev/null +++ b/vite/src/pipedal/PluginNameDialog.tsx @@ -0,0 +1,190 @@ +// Copyright (c) Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER` +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import React from 'react'; +import Typography from '@mui/material/Typography'; +import IconButtonEx from './IconButtonEx'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import IconColorDropdownButton, { DropdownAlignment } from './IconColorDropdownButton'; +import InputLabel from '@mui/material/InputLabel'; +import DialogEx from './DialogEx'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogContent'; +import { nullCast } from './Utility'; +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import { PiPedalModel } from './PiPedalModel'; +//import TextFieldEx from './TextFieldEx'; +import TextField from '@mui/material/TextField'; +import { PedalboardItem } from './Pedalboard'; + + +export interface PluginNameDialogProps { + open: boolean, + pedalboardItem: PedalboardItem | null, + allowEmpty?: boolean, + label?: string, + onApply: (text: string, color: string) => void, + onClose: () => void +}; + +export interface PluginNameDialogState { + iconColor: string; +}; + +function isTouchUi() { + return 'ontouchstart' in window || navigator.maxTouchPoints > 0; +} +export default class PluginNameDialog extends ResizeResponsiveComponent { + + refText: React.RefObject; + + constructor(props: PluginNameDialogProps) { + super(props); + this.state = { + iconColor: this.props.pedalboardItem?.iconColor ?? "" + }; + this.refText = React.createRef(); + } + mounted: boolean = false; + + + + onWindowSizeChanged(width: number, height: number): void { + } + + + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + } + componentWillUnmount() { + super.componentWillUnmount(); + this.mounted = false; + } + + componentDidUpdate() { + } + checkForIllegalCharacters(filename: string) { + if (filename.indexOf('/') !== -1) { + throw new Error("Illegal character: '/'"); + } + } + + render() { + let props = this.props; + let { open, onClose, onApply } = props; + let model = PiPedalModel.getInstance(); + + + if (!open) return null; + let pedalboardItem = props.pedalboardItem; + if (!pedalboardItem) return null; + + let uiPlugin = model.getUiPlugin(pedalboardItem.uri); + if (uiPlugin === null) { + return null; + } + let defaultName = props.pedalboardItem?.title ?? ""; + const handleClose = () => { + onClose(); + }; + + const handleOk = () => { + let text = nullCast(this.refText.current).value; + text = text.trim(); + try { + this.checkForIllegalCharacters(text); + } catch (e: any) { + let model: PiPedalModel = PiPedalModel.getInstance(); + model.showAlert(e.toString()); + return; + } + if (text.length === 0 && props.allowEmpty !== true) return; + onApply(text, this.state.iconColor); + onClose(); + } + const handleKeyDown = (event: React.KeyboardEvent): void => { + // 'keypress' event misbehaves on mobile so we track 'Enter' key via 'keydown' event + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + handleOk(); + } + }; + return ( + { }} + > + +
+ onClose()} aria-label="back" + > + + + + Plugin Properties + +
+
+ +
+ { this.props.onApply(e.target.value.toString(), this.state.iconColor) }} + autoCapitalize="off" + slotProps={{ + input: { + style: { scrollMargin: 24 } + }, + inputLabel: { + shrink: true + } + }} + id="name" + type="text" + label="Plugin Display name" + placeholder={uiPlugin?.label ?? ""} + defaultValue={defaultName} + inputRef={this.refText} + /> +
+ Icon color + { + if (this.props.pedalboardItem) { + this.setState({ iconColor: selection.key }); + onApply(this.props.pedalboardItem.title, selection.key); + } + }} + pluginType={uiPlugin.plugin_type} + dropdownAlignment={DropdownAlignment.SE} + /> +
+
+
+
+ ); + } +} diff --git a/vite/src/pipedal/PresetDialog.tsx b/vite/src/pipedal/PresetDialog.tsx index 935ce03..857a819 100644 --- a/vite/src/pipedal/PresetDialog.tsx +++ b/vite/src/pipedal/PresetDialog.tsx @@ -17,34 +17,37 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import React, { SyntheticEvent,Component } from 'react'; +import React, { SyntheticEvent } from 'react'; +import ImportPresetFromBankDialog from './ImportPresetFromBankDialog'; +import CopyPresetsToBankDialog from './CopyPresetsToBankDialog'; +import Icon from '@mui/material/Icon'; +import Divider from '@mui/material/Divider'; import { css } from '@emotion/react'; -import {isDarkMode} from './DarkMode'; +import { isDarkMode } from './DarkMode'; import IconButtonEx from './IconButtonEx'; import Typography from '@mui/material/Typography'; import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel'; import Button from '@mui/material/Button'; -import ButtonBase from "@mui/material/ButtonBase"; import DialogEx from './DialogEx'; import AppBar from '@mui/material/AppBar'; import Toolbar from '@mui/material/Toolbar'; import DraggableGrid, { ScrollDirection } from './DraggableGrid'; +import CloseIcon from '@mui/icons-material/Close'; import MoreVertIcon from '@mui/icons-material/MoreVert'; +import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import Fade from '@mui/material/Fade'; import UploadPresetDialog from './UploadPresetDialog'; +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; -import SelectHoverBackground from './SelectHoverBackground'; -import CloseIcon from '@mui/icons-material/Close'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; -import EditIcon from '@mui/icons-material/Edit'; import RenameDialog from './RenameDialog'; -import Slide, {SlideProps} from '@mui/material/Slide'; -import {createStyles} from './WithStyles'; +import Slide, { SlideProps } from '@mui/material/Slide'; +import { createStyles } from './WithStyles'; import { Theme } from '@mui/material/styles'; import WithStyles from './WithStyles'; @@ -53,10 +56,13 @@ import { withStyles } from "tss-react/mui"; import DownloadIcon from './svg/file_download_black_24dp.svg?react'; import UploadIcon from './svg/file_upload_black_24dp.svg?react'; +// function isTouchUi() { +// return 'ontouchstart' in window || navigator.maxTouchPoints > 0; +// } + interface PresetDialogProps extends WithStyles { show: boolean; - isEditDialog: boolean; onDialogClose: () => void; }; @@ -64,11 +70,16 @@ interface PresetDialogProps extends WithStyles { interface PresetDialogState { presets: PresetIndex; - showActionBar: boolean; + multiSelect: boolean; + showTouchActionBar: boolean; - selectedItem: number; + + currentItem: number; + selectedItems: Set; renameOpen: boolean; + importOpen: boolean; + copyToOpen: boolean; moreMenuAnchorEl: HTMLElement | null; openUploadPresetDialog: boolean; @@ -79,7 +90,7 @@ interface PresetDialogState { const styles = (theme: Theme) => createStyles({ listIcon: css({ - width: 24, height: 24, opacity: 0.6, fill: theme.palette.text.primary + width: 24, height: 24, opacity: 0.6, fill: theme.palette.text.primary }), dialogAppBar: css({ position: 'relative', @@ -88,7 +99,8 @@ const styles = (theme: Theme) => createStyles({ dialogActionBar: css({ position: 'relative', top: 0, left: 0, - background: "black" + background: theme.palette.actionBar.main, + color: theme.palette.actionBar.contrastText }), dialogTitle: css({ marginLeft: theme.spacing(2), @@ -106,7 +118,7 @@ const styles = (theme: Theme) => createStyles({ alignItems: "center", textAlign: "left", justifyContent: "center", - paddingLeft: 8 + paddibomngLeft: 8 }), iconFrame: css({ flex: "0 0 auto", @@ -131,7 +143,7 @@ const Transition = React.forwardRef(function Transition( const PresetDialog = withStyles( - class extends Component { + class extends ResizeResponsiveComponent { model: PiPedalModel; @@ -145,9 +157,13 @@ const PresetDialog = withStyles( let presets = this.model.presets.get(); this.state = { presets: presets, - showActionBar: false, - selectedItem: presets.selectedInstanceId, + multiSelect: false, + showTouchActionBar: false, + currentItem: presets.selectedInstanceId, + selectedItems: new Set([presets.selectedInstanceId]), renameOpen: false, + importOpen: false, + copyToOpen: false, moreMenuAnchorEl: null, openUploadPresetDialog: false @@ -156,12 +172,33 @@ const PresetDialog = withStyles( } + onWindowSizeChanged(width: number, height: number) { + super.onWindowSizeChanged(width, height); + } + handleSelectionUpdated(instanceIds: Set) { + this.setState({ + multiSelect: instanceIds.size > 1 + }) + + } + setSelections(instanceIds: Set) { + this.setState({ + selectedItems: instanceIds, + }); + this.handleSelectionUpdated(instanceIds); + } + setSelection(instanceId: number) { + let selectedItems = new Set([instanceId]); + this.setState({ + currentItem: instanceId, + selectedItems: selectedItems + }); + this.handleSelectionUpdated(selectedItems); + } + selectItemAtIndex(index: number) { let instanceId = this.state.presets.presets[index].instanceId; - this.setState({ selectedItem: instanceId }); - } - isEditMode() { - return this.state.showActionBar || this.props.isEditDialog; + this.setSelection(instanceId); } onMoreClick(e: SyntheticEvent): void { @@ -170,7 +207,7 @@ const PresetDialog = withStyles( handleDownloadPreset() { this.handleMoreClose(); - this.model.download("downloadPreset", this.state.selectedItem); + this.model.download("downloadPreset", this.state.currentItem); } handleUploadPreset() { this.handleMoreClose(); @@ -189,8 +226,11 @@ const PresetDialog = withStyles( if (!presets.areEqual(this.state.presets, false)) // avoid a bunch of peculiar effects if we update while a drag is in progress { // if we don't have a valid selection, then use the current preset. - if (this.state.presets.getItem(this.state.selectedItem) == null) { - this.setState({ presets: presets, selectedItem: presets.selectedInstanceId }); + if (this.state.presets.getItem(this.state.currentItem) == null) { + this.setState({ + presets: presets, currentItem: presets.selectedInstanceId, + selectedItems: new Set([presets.selectedInstanceId]) + }); } else { this.setState({ presets: presets }); } @@ -207,8 +247,10 @@ const PresetDialog = withStyles( this.model.presets.removeOnChangedHandler(this.handlePresetsChanged); } - getSelectedIndex() { - let instanceId = this.isEditMode() ? this.state.selectedItem : this.state.presets.selectedInstanceId; + getSelectedIndex(instanceId?: number): number { + if (instanceId === undefined) { + instanceId = this.state.currentItem; + } let presets = this.state.presets; for (let i = 0; i < presets.presets.length; ++i) { if (presets.presets[i].instanceId === instanceId) return i; @@ -217,33 +259,59 @@ const PresetDialog = withStyles( } handleDeleteClick() { - if (!this.state.selectedItem) return; - let selectedItem = this.state.selectedItem; - if (selectedItem !== -1) { - this.model.deletePresetItem(selectedItem) - .then((selectedItem: number) => { - this.setState({ selectedItem: selectedItem }); - }) - .catch((error) => { - this.model.showAlert(error); - }); - + if (this.state.selectedItems.size === 0) { + return; } + this.model.deletePresetItems(this.state.selectedItems) + .then((currentItem: number) => { + this.setSelection(currentItem); + }) + .catch((error) => { + this.model.showAlert(error); + }); } handleDialogClose() { this.props.onDialogClose(); } - handleItemClick(instanceId: number): void { - if (this.isEditMode()) { - this.setState({ selectedItem: instanceId }); + handleItemClick(e: React.MouseEvent, instanceId: number): void { + e.stopPropagation(); + if (e.ctrlKey || this.state.showTouchActionBar) { + let selectedItems = new Set(this.state.selectedItems); + if (selectedItems.has(instanceId)) { + selectedItems.delete(instanceId); + this.setSelections(selectedItems); + } else { + selectedItems.add(instanceId); + this.setState({ + selectedItems: selectedItems, + currentItem: instanceId + }); + this.handleSelectionUpdated(selectedItems); + } + } else if (e.shiftKey) { + let presets = this.state.presets; + let startIndex = this.getSelectedIndex(); + let endIndex = this.getSelectedIndex(instanceId); + if (startIndex === -1 || endIndex === -1) return; // should not happen. + + let selectedItems = new Set(); + + if (endIndex < startIndex) { + let t = startIndex; + startIndex = endIndex; + endIndex = t; + } + for (let i = startIndex; i <= endIndex; ++i) { + selectedItems.add(presets.presets[i].instanceId); + } + this.setSelections(selectedItems); } else { - this.model.loadPreset(instanceId); - this.props.onDialogClose(); + this.setSelection(instanceId); } } - showActionBar(show: boolean): void { - this.setState({ showActionBar: show }); + showTouchActionBar(show: boolean): void { + this.setState({ showTouchActionBar: show }); } @@ -251,27 +319,23 @@ const PresetDialog = withStyles( mapElement(el: any): React.ReactNode { let presetEntry = el as PresetIndexEntry; const classes = withStyles.getClasses(this.props); - let selectedItem = this.isEditMode() ? this.state.selectedItem : this.state.presets.selectedInstanceId; + let selected = this.state.selectedItems.has(presetEntry.instanceId); return ( -
- - this.handleItemClick(presetEntry.instanceId)} +
+ this.handleItemClick(e, presetEntry.instanceId)} + style={{ height: 48 }} > - -
-
- -
-
- - {presetEntry.name} - -
-
- + + + + + +
); @@ -287,30 +351,33 @@ const PresetDialog = withStyles( moveElement(from: number, to: number): void { let newPresets = this.state.presets.clone(); newPresets.movePreset(from, to); + let toInstanceId = newPresets.presets[to].instanceId; this.setState({ presets: newPresets, - selectedItem: newPresets.presets[to].instanceId + currentItem: toInstanceId, + selectedItems: new Set([toInstanceId]) + }); this.updateServerPresets(newPresets); } getSelectedName(): string { - let item = this.state.presets.getItem(this.state.selectedItem); + let item = this.state.presets.getItem(this.state.currentItem); if (item) return item.name; return ""; } handleRenameClick() { - let item = this.state.presets.getItem(this.state.selectedItem); + let item = this.state.presets.getItem(this.state.currentItem); if (item) { this.setState({ renameOpen: true }); } } handleRenameOk(text: string) { - let item = this.state.presets.getItem(this.state.selectedItem); + let item = this.state.presets.getItem(this.state.currentItem); if (!item) return; if (item.name !== text) { - this.model.renamePresetItem(this.state.selectedItem, text) + this.model.renamePresetItem(this.state.currentItem, text) .catch((error) => { this.onError(error); }); @@ -319,11 +386,14 @@ const PresetDialog = withStyles( this.setState({ renameOpen: false }); } handleCopy() { - let item = this.state.presets.getItem(this.state.selectedItem); + let item = this.state.presets.getItem(this.state.currentItem); if (!item) return; - this.model.duplicatePreset(this.state.selectedItem) + this.model.duplicatePreset(this.state.currentItem) .then((newId) => { - this.setState({ selectedItem: newId }); + this.setState({ + currentItem: newId, + selectedItems: new Set([newId]) + }); }).catch((error) => { this.onError(error); }); @@ -333,81 +403,144 @@ const PresetDialog = withStyles( this.model?.showAlert(error); } + handleCloseActionBar(e: SyntheticEvent) { + e.stopPropagation(); + this.setState({ + showTouchActionBar: false, + selectedItems: new Set([this.state.currentItem]), + multiSelect: false + }); + } + handleImportPresetsFromBank() { + this.handleMoreClose(); + this.setState({ importOpen: true }); + } + handleImportDialogOk(bankInstanceId: number, presets: number[]): void { + this.setState({ importOpen: false }); + this.model.importPresetsFromBank(bankInstanceId, presets) + .then((instanceId) => { + if (instanceId !== -1) { + this.setSelection(instanceId); + setTimeout(() => { + let el = document.getElementById("psetdlgItem_" + instanceId); + if (el) { + el.scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, 0); + } + }) + .catch((error) => { + this.model.showAlert(error); + }); + } + handleCopyToBankDialogOk(bankInstanceId: number): void { + this.setState({ copyToOpen: false }); + let selectedItems: number[] = []; + for (let i = 0; i < this.state.presets.presets.length; ++i) { + let preset = this.state.presets.presets[i]; + if (this.state.selectedItems.has(preset.instanceId)) { + selectedItems.push(preset.instanceId); + } + } + this.model.copyPresetsToBank(bankInstanceId, selectedItems) + .then((instanceId) => { + }) + .catch((error) => { + this.model.showAlert(error); + }); + } + + handleCopyPresetsToBank() { + this.handleMoreClose(); + this.setState({ copyToOpen: true }); + } render() { const classes = withStyles.getClasses(this.props); - let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar; - let defaultSelectedIndex = this.getSelectedIndex(); - + const showActionBar = this.state.multiSelect || this.state.showTouchActionBar; return ( { this.handleDialogClose() }} TransitionComponent={Transition} - style={{userSelect: "none"}} - onEnterKey={()=>{}} - > + style={{ userSelect: "none" }} + onEnterKey={() => { }} + >
- + - { this.handleCloseActionBar(e); }} > - + - - Presets + + {this.state.selectedItems.size.toString() + " items selected"} - this.showActionBar(true)} > - + + { this.handleDeleteClick(); }} > + Delete - { e.stopPropagation(); e.preventDefault(); }} > - {(!this.props.isEditDialog) ? ( - this.showActionBar(false)} aria-label="close"> - - - ) : ( - - - - - )} - + + + + Presets - {(this.state.presets.getItem(this.state.selectedItem) != null) + {(this.state.presets.getItem(this.state.currentItem) != null) && ( -
- - - { this.setState({ renameOpen: false }) }} - onOk={(text: string) => { - this.handleRenameOk(text); - } - } - /> - this.handleDeleteClick()} > - Delete - +
+ {this.state.selectedItems.size === 1 && ( + + )} + {this.state.selectedItems.size === 1 && ( + + + )} + {this.state.renameOpen && ( + { this.setState({ renameOpen: false }) }} + onOk={(text: string) => { + this.handleRenameOk(text); + } + } + /> + )} + {this.state.selectedItems.size === 1 && ( + this.handleDeleteClick()} + style={{ opacity: this.state.selectedItems.size !== 1 ? 0.5 : 1.0 }} + > + Delete + + )} { this.onMoreClick(e) }} > @@ -419,15 +552,43 @@ const PresetDialog = withStyles( onClose={() => this.handleMoreClose()} TransitionComponent={Fade} > - { this.handleDownloadPreset(); }} > + {this.state.selectedItems.size !== 0 && ( + { + this.handleMoreClose(); + this.handleCopyPresetsToBank(); + }} + > + + + + + Copy to bank... + + + + )} + { this.handleImportPresetsFromBank(); }} > - + - Download preset + Import presets from bank... + + + {this.state.selectedItems.size !== 0 && ( + { this.handleDownloadPreset(); }} > + + + + + Download preset + + + + )} { this.handleUploadPreset() }}> @@ -449,12 +610,16 @@ const PresetDialog = withStyles(
this.showActionBar(true)} - canDrag={this.isEditMode()} + onLongPress={(e,item) => { + if (e.pointerType === "touch") + { + this.showTouchActionBar(true); + } + }} + canDrag={!this.state.multiSelect && !this.state.showTouchActionBar} onDragStart={(index, x, y) => { this.selectItemAtIndex(index) }} moveElement={(from, to) => { this.moveElement(from, to); }} scroll={ScrollDirection.Y} - defaultSelectedIndex={defaultSelectedIndex} > { this.state.presets.presets.map((element) => { @@ -464,14 +629,28 @@ const PresetDialog = withStyles(
- this.setState({selectedItem: instanceId}) } - uploadAfter={this.state.selectedItem} - open={this.state.openUploadPresetDialog} - onClose={() => { this.setState({ openUploadPresetDialog: false }) }} /> + this.setSelection(instanceId)} + uploadAfter={this.state.currentItem} + open={this.state.openUploadPresetDialog} + onClose={() => { this.setState({ openUploadPresetDialog: false }) }} /> + {this.state.importOpen && ( + this.setState({ importOpen: false })} + onOk={(bankInstanceId, presets) => this.handleImportDialogOk(bankInstanceId, presets)} + /> + )} + {this.state.copyToOpen && ( + this.setState({ copyToOpen: false })} + onOk={(bankInstanceId) => this.handleCopyToBankDialogOk(bankInstanceId)} + /> + )} diff --git a/vite/src/pipedal/PresetSelector.tsx b/vite/src/pipedal/PresetSelector.tsx index a903f79..acb0579 100644 --- a/vite/src/pipedal/PresetSelector.tsx +++ b/vite/src/pipedal/PresetSelector.tsx @@ -17,7 +17,8 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -import { SyntheticEvent, Component } from 'react'; +import { SyntheticEvent } from 'react'; +import ArrowRightIcon from '@mui/icons-material/ArrowRight'; import IconButtonEx from './IconButtonEx'; import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel'; import SaveIconOutline from '@mui/icons-material/Save'; @@ -25,7 +26,7 @@ import MoreVertIcon from '@mui/icons-material/MoreVert'; import { Theme } from '@mui/material/styles'; import WithStyles from './WithStyles'; import { withStyles } from "tss-react/mui"; -import {createStyles} from './WithStyles'; +import { createStyles } from './WithStyles'; import PresetDialog from './PresetDialog'; import Menu from '@mui/material/Menu'; @@ -33,9 +34,13 @@ import MenuItem from '@mui/material/MenuItem'; import Fade from '@mui/material/Fade'; import Divider from '@mui/material/Divider'; import RenameDialog from './RenameDialog' +import SavePresetAsDialog from './SavePresetAsDialog'; +import ImportPresetFromBankDialog from './ImportPresetFromBankDialog'; + import Select from '@mui/material/Select'; import UploadPresetDialog from './UploadPresetDialog'; -import {isDarkMode} from './DarkMode'; +import { isDarkMode } from './DarkMode'; +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; interface PresetSelectorProps extends WithStyles { @@ -48,7 +53,12 @@ interface PresetSelectorState { showPresetsDialog: boolean; showEditPresetsDialog: boolean; presetsMenuAnchorRef: HTMLElement | null; + presetsSubmenuAnchorRef: HTMLElement | null; + compactHorizontalLayoutMenu: boolean; + saveAsDialogOpen: boolean; + importDialogOpen: boolean; + renameDialogOpen: boolean; renameDialogTitle: string; renameDialogDefaultName: string; @@ -59,7 +69,7 @@ interface PresetSelectorState { } -const selectColor = isDarkMode()? "#888": "#FFFFFF"; +const selectColor = isDarkMode() ? "#888" : "#FFFFFF"; const styles = (theme: Theme) => createStyles({ select: { // fu fu fu.Overrides for white selector on dark background. @@ -81,8 +91,8 @@ const styles = (theme: Theme) => createStyles({ const PresetSelector = withStyles( - class extends Component { - + class extends ResizeResponsiveComponent { + private MENU_THRESHOLD: number = 570; model: PiPedalModel; constructor(props: PresetSelectorProps) { @@ -92,10 +102,14 @@ const PresetSelector = presets: this.model.presets.get(), enabled: false, + compactHorizontalLayoutMenu: this.windowSize.height < this.MENU_THRESHOLD, showPresetsDialog: false, showEditPresetsDialog: false, presetsMenuAnchorRef: null, + presetsSubmenuAnchorRef: null, renameDialogOpen: false, + saveAsDialogOpen: false, + importDialogOpen: false, renameDialogTitle: "", renameDialogDefaultName: "", renameDialogActionName: "", @@ -109,12 +123,34 @@ const PresetSelector = this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this); } + onWindowSizeChanged(width: number, height: number): void { + super.onWindowSizeChanged(width,height); + this.setState({ compactHorizontalLayoutMenu: height < this.MENU_THRESHOLD }); + } - handlePresetMenuClick(event: SyntheticEvent): void { + handlePresetsMenuClick(event: SyntheticEvent): void { this.setState({ presetsMenuAnchorRef: (event.currentTarget as HTMLElement) }); } + handlePresetsSubmenuClick(event: SyntheticEvent): void { + this.setState({ presetsSubmenuAnchorRef: (event.currentTarget as HTMLElement) }); + } handlePresetsMenuClose(): void { - this.setState({ presetsMenuAnchorRef: null }); + this.setState({ + presetsMenuAnchorRef: null, + presetsSubmenuAnchorRef: null, + }); + } + handlePresetsSubmenuClose(): void { + this.setState({ + presetsSubmenuAnchorRef: null, + + }); + } + + handlePresetsMenuImport(e: SyntheticEvent): void { + this.handlePresetsMenuClose(); + e.stopPropagation(); + this.setState({ importDialogOpen: true }); } handleDownloadPreset(e: SyntheticEvent) { @@ -144,21 +180,8 @@ const PresetSelector = let currentPresets = this.model.presets.get(); let item = currentPresets.getItem(currentPresets.selectedInstanceId); if (item == null) return; - let name = item.name; - - this.renameDialogOpen(name, "Save Preset As", "OK") - .then((newName) => { - return this.model.saveCurrentPresetAs(newName); - }) - .then((newInstanceId) => { - - }) - .catch((error) => { - this.showError(error); - }) - ; - + this.setState({ saveAsDialogOpen: true }); } handlePresetsMenuRename(e: SyntheticEvent): void { this.handlePresetsMenuClose(); @@ -198,7 +221,7 @@ const PresetSelector = showError(error: string) { this.model.showAlert(error); } - renameDialogOpen(defaultText: string, title: string,acceptButtonText: string): Promise { + renameDialogOpen(defaultText: string, title: string, acceptButtonText: string): Promise { let result = new Promise( (resolve, reject) => { this.setState( @@ -219,6 +242,30 @@ const PresetSelector = return result; } + handleSaveAsDialogOk(bankInstanceId: number, name: string): void { + this.setState({ saveAsDialogOpen: false }); + + this.model.saveCurrentPresetAs(bankInstanceId, name) + .then((instanceId) => { + this.model.loadPreset(instanceId); + }) + .catch((error) => { + this.showError(error); + }); + } + handleImportDialogOk(bankInstanceId: number, presets: number[]): void { + this.setState({ importDialogOpen: false }); + this.model.importPresetsFromBank(bankInstanceId, presets) + .then((instanceId) => { + if (instanceId !== -1) { + this.model.loadPreset(instanceId); + } + }) + .catch((error) => { + this.showError(error); + }); + } + handleRenameDialogClose(): void { this.setState({ renameDialogOpen: false, @@ -259,11 +306,13 @@ const PresetSelector = this.updatePresetState(); } componentDidMount() { + super.componentDidMount(); this.model.presets.addOnChangedHandler(this.handlePresetsChanged); this.updatePresetState(); } componentWillUnmount() { this.model.presets.removeOnChangedHandler(this.handlePresetsChanged) + super.componentWillUnmount(); } showPresetDialog(show: boolean) { this.setState({ @@ -318,7 +367,7 @@ const PresetSelector = onChange={(e, extra) => this.handleChange(e, extra)} onClose={(e) => this.handleSelectClose(e)} displayEmpty - value={presets.selectedInstanceId === 0? '' : presets.selectedInstanceId} + value={presets.selectedInstanceId === 0 ? '' : presets.selectedInstanceId} inputProps={{ classes: { icon: classes.icon }, 'aria-label': "Select preset" @@ -339,13 +388,13 @@ const PresetSelector = }
-
+
this.handlePresetMenuClick(e)} + style={{ flex: "0 0 auto", color: "#FFFFFF" }} + onClick={(e) => this.handlePresetsMenuClick(e)} size="large" - > + > this.handlePresetsMenuSave(e)}>Save preset - this.handlePresetsMenuSaveAs(e)}>Save preset as... - this.handlePresetsMenuRename(e)}>Rename... - this.handlePresetsMenuNew(e)}>New... + + {this.state.compactHorizontalLayoutMenu ? ( + this.handlePresetsSubmenuClick(e)} + style={{ display: 'flex', flexFlow: "row nowrap", alignItems: 'center', justifyContent: 'space-between' }} + > + Edit + + + ) + :( + [ + ( this.handlePresetsMenuSaveAs(e)}>Save preset as...), + ( this.handlePresetsMenuRename(e)}>Rename...), + ( this.handlePresetsMenuImport(e)}>Import from bank...), + ( this.handlePresetsMenuNew(e)}>New...), + ] + + )} { this.handleDownloadPreset(e); }} >Download preset { this.handleUploadPreset(e) }}>Upload preset @@ -366,7 +430,39 @@ const PresetSelector = this.handleMenuEditPresets()}>Manage presets...
- this.handleDialogClose()} /> + + {/* Submenu */} + this.handlePresetsSubmenuClose()} + anchorOrigin={{ vertical: 'top', horizontal: 'right' }} + anchorPosition={{left: 48, top: 0}} + anchorEl={this.state.presetsSubmenuAnchorRef} + open={this.state.presetsSubmenuAnchorRef !== null} > + this.handlePresetsMenuSaveAs(e)}>Save preset as... + this.handlePresetsMenuRename(e)}>Rename... + this.handlePresetsMenuImport(e)}>Import from bank... + this.handlePresetsMenuNew(e)}>New... + + + + {this.state.showPresetsDialog&& ( + this.handleDialogClose()} + /> + )} + {this.state.saveAsDialogOpen && ( + { this.setState({ saveAsDialogOpen: false }) }} + onOk={(bankInstanceId, name) => { + this.handleSaveAsDialogOk(bankInstanceId, name); + }} /> + )} + {this.state.importDialogOpen && ( + { this.setState({ importDialogOpen: false }) }} + onOk={(bankInstanceId, presets) => { + this.handleImportDialogOk(bankInstanceId, presets); + }} /> + )} void, + onClose: () => void +}; + +export interface SavePresetAsDialogState { + selectedBank: number; + banks: BankIndex; + + fullScreen: boolean; +}; + +function isTouchUi() +{ + return 'ontouchstart' in window || navigator.maxTouchPoints > 0; +} +export default class SavePresetAsDialog extends ResizeResponsiveComponent { + + refText: React.RefObject; + + model: PiPedalModel; + constructor(props: SavePresetAsDialogProps) { + super(props); + this.model = PiPedalModel.getInstance(); + this.state = { + banks: this.model.banks.get(), + selectedBank: this.model.banks.get().selectedBank, + fullScreen: false + + }; + this.refText = React.createRef(); + } + mounted: boolean = false; + + + + onWindowSizeChanged(width: number, height: number): void + { + this.setState({fullScreen: height < 200}) + } + + + componentDidMount() + { + super.componentDidMount(); + this.mounted = true; + } + componentWillUnmount() + { + super.componentWillUnmount(); + this.mounted = false; + } + + componentDidUpdate() + { + } + checkForIllegalCharacters(filename: string) { + } + + render() { + let props = this.props; + let { open, defaultName, onClose, onOk } = props; + + const handleClose = () => { + onClose(); + }; + + const handleOk = () => { + let text = nullCast(this.refText.current).value; + text = text.trim(); + try { + this.checkForIllegalCharacters(text); + } catch (e:any) + { + let model:PiPedalModel = PiPedalModelFactory.getInstance(); + model.showAlert(e.toString()); + return; + } + if (text.length === 0) return; + onOk(this.state.selectedBank,text); + } + const handleKeyDown = (event: React.KeyboardEvent): void => { + // 'keypress' event misbehaves on mobile so we track 'Enter' key via 'keydown' event + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + handleOk(); + } + }; + return ( + {}} + > + + Save Preset As + + + + Bank + + + + + + + + + + + ); + } +} diff --git a/vite/src/pipedal/SelectHoverBackground.tsx b/vite/src/pipedal/SelectHoverBackground.tsx index 060136c..bc482af 100644 --- a/vite/src/pipedal/SelectHoverBackground.tsx +++ b/vite/src/pipedal/SelectHoverBackground.tsx @@ -57,6 +57,7 @@ interface HoverProps extends WithStyles { selected: boolean; showHover?: boolean; borderRadius?: number; + clipChildren?: boolean; children?: React.ReactNode; } diff --git a/vite/src/pipedal/SnapshotButton.tsx b/vite/src/pipedal/SnapshotButton.tsx index 5fbd65a..7844fc9 100644 --- a/vite/src/pipedal/SnapshotButton.tsx +++ b/vite/src/pipedal/SnapshotButton.tsx @@ -75,15 +75,15 @@ export default class SnapshotButton extends ResizeResponsiveComponent 4) uiScale = 4; this.setState({uiScale: uiScale}); } diff --git a/vite/src/pipedal/ToobFrequencyResponseView.tsx b/vite/src/pipedal/ToobFrequencyResponseView.tsx index 2948cb9..871aa90 100644 --- a/vite/src/pipedal/ToobFrequencyResponseView.tsx +++ b/vite/src/pipedal/ToobFrequencyResponseView.tsx @@ -28,6 +28,7 @@ import { withStyles } from "tss-react/mui"; import { PiPedalModelFactory, PiPedalModel, State } from "./PiPedalModel"; import Utility from './Utility'; import SvgPathBuilder from './SvgPathBuilder'; +import { UiFrequencyPlot } from './Lv2Plugin'; const StandardItemSize = { width: 80, height: 110 }; @@ -50,6 +51,7 @@ interface ToobFrequencyResponseProps extends WithStyles { instanceId: number; propertyName?: string; width?: number; + frequencyPlot?: UiFrequencyPlot } interface ToobFrequencyResponseState { @@ -57,6 +59,8 @@ interface ToobFrequencyResponseState { maxDb: number; minF: number; maxF: number; + logX: boolean; + dbY: boolean; path: string; } @@ -74,10 +78,12 @@ const ToobFrequencyResponseView = this.model = PiPedalModelFactory.getInstance(); this.state = { path: "", - minDb: -30, - maxDb: 5, - minF: 30, - maxF: 20000 + minDb: this.props.frequencyPlot?.yBottom ?? -30, + maxDb: this.props.frequencyPlot?.yTop ?? 5, + minF: this.props.frequencyPlot?.xLeft ?? 30, + maxF: this.props.frequencyPlot?.xRight ?? 20000, + logX: this.props.frequencyPlot?.xLog ?? true, + dbY: this.props.frequencyPlot?.yDb ?? true }; this.pathRef = React.createRef(); @@ -140,16 +146,22 @@ const ToobFrequencyResponseView = } toY(value: number): number { - value = Math.abs(value); + if (this.state.dbY) + { + value = Math.abs(value); - var db; - if (value < this.MIN_DB_AF) { - db = -192.0; + var db; + if (value < this.MIN_DB_AF) { + db = -192.0; + } else { + db = 20 * Math.log10(value); + } + var y = (db - this.dbMin) / (this.dbMax - this.dbMin) * (this.yMax - this.yMin) + this.yMin; + return y; } else { - db = 20 * Math.log10(value); + var y = value-this.dbMin /(this.dbMax-this.dbMin)* (this.yMax - this.yMin) + this.yMin; + return y; } - var y = (db - this.dbMin) / (this.dbMax - this.dbMin) * (this.yMax - this.yMin) + this.yMin; - return y; } onFrequencyResponseUpdated(data: number[]) { @@ -159,6 +171,7 @@ const ToobFrequencyResponseView = let dbMin = data[2]; let dbMax = data[3]; let xMax = this.xMax(); + let dbY = this.state.dbY; let n = data.length-4; @@ -166,16 +179,22 @@ const ToobFrequencyResponseView = return (xMax - this.xMin) * bin/n; }; - let toY_ = (value: number): number => { - - var db; - if (value < this.MIN_DB_AF) { - db = -192.0; - } else { - db = 20 * Math.log10(value); + let toY_ = dbY? + (value: number): number => { + var db; + if (value < this.MIN_DB_AF) { + db = -192.0; + } else { + db = 20 * Math.log10(value); + } + var y = (db - dbMin) / (dbMax - dbMin) * (this.yMax - this.yMin) + this.yMin; + return y; } - var y = (db - dbMin) / (dbMax - dbMin) * (this.yMax - this.yMin) + this.yMin; - return y; + : + (value: number): number => { + var y = (value - dbMin) / (dbMax - dbMin) * (this.yMax - this.yMin) + this.yMin; + return y; + }; @@ -242,7 +261,7 @@ const ToobFrequencyResponseView = } grid(): React.ReactNode[] { let result: React.ReactNode[] = []; - + if (!this.state.dbY) return result; let xMax = this.xMax(); for (var db = Math.ceil(this.dbMax / this.dbTickSpacing) * this.dbTickSpacing; db < this.dbMin; db += this.dbTickSpacing) { var y = (db - this.dbMin) / (this.dbMax - this.dbMin) * (this.yMax - this.yMin); @@ -277,6 +296,7 @@ const ToobFrequencyResponseView = dbMin: number = 5; dbMax: number = -35; + dbY: boolean = true; private nextKey: number = 0; render() { @@ -284,6 +304,7 @@ const ToobFrequencyResponseView = this.nextKey = 0; this.dbMax = this.state.minDb; this.dbMin = this.state.maxDb; + this.dbY = this.state.dbY; this.fMin = this.state.minF; this.fMax = this.state.maxF; diff --git a/vite/src/pipedal/svg/fx_spectral.svg b/vite/src/pipedal/svg/fx_spectral.svg index dadd1a1..5f08cc2 100644 --- a/vite/src/pipedal/svg/fx_spectral.svg +++ b/vite/src/pipedal/svg/fx_spectral.svg @@ -1,25 +1,28 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + +