Merge pull request #401 from rerdavies/dev

Merge v1.5.93 from dev
This commit is contained in:
Robin Davies
2025-09-21 19:14:35 -04:00
committed by GitHub
89 changed files with 4349 additions and 1831 deletions
+2 -1
View File
@@ -22,7 +22,8 @@
// Resolved by CMake Tools: // Resolved by CMake Tools:
"program": "${command:cmake.launchTargetPath}", "program": "${command:cmake.launchTargetPath}",
"args": [ "args": [
"[ProfileThumbnails]" "hw:M2",
"hw:M2"
], ],
"stopAtEntry": false, "stopAtEntry": false,
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
+2 -2
View File
@@ -1,13 +1,13 @@
cmake_minimum_required(VERSION 3.16.0) cmake_minimum_required(VERSION 3.16.0)
project(pipedal project(pipedal
VERSION 1.5.92 VERSION 1.5.93
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
HOMEPAGE_URL "https://rerdavies.github.io/pipedal" HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
) )
execute_process( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE ) 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 (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE})
set (CMAKE_INSTALL_PREFIX "/usr/") set (CMAKE_INSTALL_PREFIX "/usr/")
+2 -2
View File
@@ -9,11 +9,11 @@
<img src="https://img.shields.io/github/downloads/rerdavies/pipedal/total?color=%23808080&link=https%3A%2F%2Frerdavies.github.io%2Fpipedal%2Fdownload.html"/> <img src="https://img.shields.io/github/downloads/rerdavies/pipedal/total?color=%23808080&link=https%3A%2F%2Frerdavies.github.io%2Fpipedal%2Fdownload.html"/>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.5.92</a> Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.5.93</a>
Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Documentation:&nbsp;[https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html). Documentation:&nbsp;[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!
&nbsp; &nbsp;
+3 -1
View File
@@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# Run a CMake build. # Run a Clean Production CMake build.
set -e set -e
@@ -14,3 +14,5 @@ cd ..
time cmake --build ./build --target all --config Release -- -j 3 time cmake --build ./build --target all --config Release -- -j 3
./makePackage.sh
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
2
+5 -5
View File
@@ -13,18 +13,18 @@ page_icon: img/Install4.jpg
Download the most recent Debian (.deb) package for your platform: 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) - [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.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.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.92](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_amd64.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 update
sudo apt upgrade sudo apt upgrade
cd ~/Downloads 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. You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies.
+21
View File
@@ -1,5 +1,26 @@
# Release Notes # 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 ## PiPedal 1.5.92 Release
### Major Features in This Build ### Major Features in This Build
+4 -4
View File
@@ -4,9 +4,9 @@
Download the most recent Debian (.deb) package for your platform: 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) - [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.92 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_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.92 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.92/pipedal_1.5.92_amd64.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 Install the package by running
@@ -14,7 +14,7 @@ Install the package by running
``` ```
sudo apt update sudo apt update
cd ~/Downloads 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 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. expected, and can be safely ignored.
+1 -1
View File
@@ -93,7 +93,7 @@ cabir:impulseFile3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
TooB Cab IR is a convolution-based guitar cabinet impulse response simulator. TooB Cab IR is a convolution-based guitar cabinet impulse response simulator.
+1 -1
View File
@@ -50,7 +50,7 @@ toob:frequencyResponseVector
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "TooB CabSim"; mod:label "TooB CabSim";
@@ -53,7 +53,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
Toob Convolution Reverb Stereo uses convolution reverb impulse/response files in order to produce highly Toob Convolution Reverb Stereo uses convolution reverb impulse/response files in order to produce highly
@@ -51,7 +51,7 @@ toobimpulse:impulseFile
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
+1 -1
View File
@@ -66,7 +66,7 @@ inputStage:filterGroup
doap:license <https://two-play.com/TooB/licenses/isc> ; doap:license <https://two-play.com/TooB/licenses/isc> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "TooB Input"; mod:label "TooB Input";
+1 -1
View File
@@ -68,7 +68,7 @@ pstage:stage3
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "Power Stage"; mod:label "Power Stage";
+1 -1
View File
@@ -57,7 +57,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment "TooB spectrum analyzer" ; rdfs:comment "TooB spectrum analyzer" ;
mod:brand "TooB"; mod:brand "TooB";
+1 -1
View File
@@ -57,7 +57,7 @@ tonestack:eqGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>; uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
+1 -1
View File
@@ -52,7 +52,7 @@ toob:frequencyResponseVector
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
uiext:ui <http://two-play.com/plugins/toob-three-band-eq-ui>; uiext:ui <http://two-play.com/plugins/toob-three-band-eq-ui>;
Binary file not shown.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
Emulation of a Boss CE-2 Chorus. Emulation of a Boss CE-2 Chorus.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
A straightforward no-frills digital delay. A straightforward no-frills digital delay.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original. Emulation of a Boss BF-2 Flanger, based on circuit analysis and simulation of the original.
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
Digital emulation of a Boss BF-2 Flanger. Digital emulation of a Boss BF-2 Flanger.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ 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. Toob Freeverb is an Lv2 implementation of the famous Freeverb reverb effect. FreeVerb delivers a well-balanced reverb with very little tonal coloration.
+1 -1
View File
@@ -41,7 +41,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals. A 7 octave graphic equalizer, with frequencies chosen to be useful for EQ-ing guitar signals.
""" ; """ ;
+1 -1
View File
@@ -92,7 +92,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ; doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 2 ; lv2:minorVersion 2 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
ui:ui <http://two-play.com/plugins/toob-looper-four-ui>; ui:ui <http://two-play.com/plugins/toob-looper-four-ui>;
+1 -1
View File
@@ -78,7 +78,7 @@ myprefix:output_group
doap:license <https://opensource.org/license/mit/> ; doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 2 ; lv2:minorVersion 2 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
ui:ui <http://two-play.com/plugins/toob-looper-one-ui>; ui:ui <http://two-play.com/plugins/toob-looper-one-ui>;
+1 -1
View File
@@ -67,7 +67,7 @@ toobml:sagGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
The TooB ML Amplifier plugin provides emulation of a variety of amplifiers and overdrive pedals that are implemented 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. using neural-network-based machine learning models of real amplifiers.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
Remix a stereo input signal. Remix a stereo input signal.
@@ -72,7 +72,7 @@ toobNam:calibrationGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ 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 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 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
+1 -1
View File
@@ -54,7 +54,7 @@ noisegate:envelope_group
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
A noise gate is an audio processing tool that controls the volume of an audio signal 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. by allowing it to pass through only when it exceeds a set threshold.
+2 -2
View File
@@ -79,7 +79,7 @@ parametric_eq:hfGroup
<http://two-play.com/plugins/toob-parametric-eq> <http://two-play.com/plugins/toob-parametric-eq>
a lv2:Plugin , a lv2:Plugin ,
lv2:EQPlugin ; lv2:ParaEQPlugin ;
doap:name "TooB Parametric EQ" , doap:name "TooB Parametric EQ" ,
"TooB Parametric EQ"@en-gb-gb "TooB Parametric EQ"@en-gb-gb
; ;
@@ -87,7 +87,7 @@ parametric_eq:hfGroup
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
uiext:ui <http://two-play.com/plugins/toob-parametric-eq-ui>; uiext:ui <http://two-play.com/plugins/toob-parametric-eq-ui>;
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
A loose emulation of an MXR® Phase 90 Phaser. A loose emulation of an MXR® Phase 90 Phaser.
+1 -1
View File
@@ -60,7 +60,7 @@ toobPlayer:seek
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
Play audio files. Audio files can optionally be loooped by pressing the "Set loop" button. Play audio files. Audio files can optionally be loooped by pressing the "Set loop" button.
+1 -1
View File
@@ -51,7 +51,7 @@ recordPrefix:audioFile
doap:license <https://opensource.org/license/mit/> ; doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 2 ; lv2:minorVersion 2 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
ui:ui <http://two-play.com/plugins/toob-record-mono-ui>; ui:ui <http://two-play.com/plugins/toob-record-mono-ui>;
+1 -1
View File
@@ -88,7 +88,7 @@ myprefix:loop3_group
doap:license <https://opensource.org/license/mit/> ; doap:license <https://opensource.org/license/mit/> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 2 ; lv2:minorVersion 2 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
ui:ui <http://two-play.com/plugins/toob-record-stereo-ui>; ui:ui <http://two-play.com/plugins/toob-record-stereo-ui>;
+199
View File
@@ -0,0 +1,199 @@
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
@prefix epp: <http://lv2plug.in/ns/ext/port-props#> .
@prefix uiext: <http://lv2plug.in/ns/extensions/ui#> .
@prefix idpy: <http://harrisonconsoles.com/lv2/inlinedisplay#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix mod: <http://moddevices.com/ns/mod#> .
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .
@prefix work: <http://lv2plug.in/ns/ext/worker#> .
@prefix pg: <http://lv2plug.in/ns/ext/port-groups#> .
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix state: <http://lv2plug.in/ns/ext/state#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix toobTremolo: <http://two-play.com/plugins/toob-tremolo#> .
@prefix pipedal_ui: <http://github.com/rerdavies/pipedal/ui#> .
<http://two-play.com/rerdavies#me>
a foaf:Person ;
foaf:name "Robin Davies" ;
foaf:mbox <mailto:rerdavies@gmail.com> ;
foaf:homepage <https://github.com/sponsors/rerdavies> .
<http://two-play.com/plugins/toob-tremolo>
a lv2:Plugin ,
lv2:ModulatorPlugin ;
doap:name "TooB Tremolo (Stereo)" ,
"TooB Tremolo (Stereo)"@en-us-us
;
uiext:ui <http://two-play.com/plugins/toob-tremolo-ui> ;
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
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" ;
]
.
<http://two-play.com/plugins/toob-tremolo-ui>
a uiext:X11UI ;
lv2:binary <ToobAmpUI.so>;
lv2:extensionData uiext::idle ;
lv2:extensionData uiext:resize ;
lv2:extensionData uiext:idleInterface;
lv2:requiredFeature uiext:idleInterface ;
.
<http://two-play.com/plugins/toob-tremolo>
pipedal_ui:ui <http://two-play.com/plugins/toob-tremolo#piPedalUI>.
<http://two-play.com/plugins/toob-tremolo#piPedalUI>
a pipedal_ui:ui ;
pipedal_ui:frequencyPlot
[
lv2:index 4 ;
pipedal_ui:patchProperty <http://two-play.com/plugins/toob-tremolo#lfoShape>;
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 ;
]
.
+184
View File
@@ -0,0 +1,184 @@
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
@prefix epp: <http://lv2plug.in/ns/ext/port-props#> .
@prefix uiext: <http://lv2plug.in/ns/extensions/ui#> .
@prefix idpy: <http://harrisonconsoles.com/lv2/inlinedisplay#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix mod: <http://moddevices.com/ns/mod#> .
@prefix param: <http://lv2plug.in/ns/ext/parameters#> .
@prefix work: <http://lv2plug.in/ns/ext/worker#> .
@prefix pg: <http://lv2plug.in/ns/ext/port-groups#> .
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix patch: <http://lv2plug.in/ns/ext/patch#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix state: <http://lv2plug.in/ns/ext/state#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix toobTremeloMono: <http://two-play.com/plugins/toob-tremolo-mono#> .
@prefix pipedal_ui: <http://github.com/rerdavies/pipedal/ui#> .
<http://two-play.com/rerdavies#me>
a foaf:Person ;
foaf:name "Robin Davies" ;
foaf:mbox <mailto:rerdavies@gmail.com> ;
foaf:homepage <https://github.com/sponsors/rerdavies> .
<http://two-play.com/plugins/toob-tremolo-mono>
a lv2:Plugin ,
lv2:ModulatorPlugin ;
doap:name "TooB Tremolo (Mono)" ,
"TooB Tremolo (Mono)"@en-us-us
;
uiext:ui <http://two-play.com/plugins/toob-tremolo-mono-ui> ;
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ;
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" ;
]
.
<http://two-play.com/plugins/toob-tremolo-mono-ui>
a uiext:X11UI ;
lv2:binary <ToobAmpUI.so>;
lv2:extensionData uiext::idle ;
lv2:extensionData uiext:resize ;
lv2:extensionData uiext:idleInterface;
lv2:requiredFeature uiext:idleInterface ;
.
<http://two-play.com/plugins/toob-tremolo-mono>
pipedal_ui:ui <http://two-play.com/plugins/toob-tremolo-mono#piPedalUI>.
<http://two-play.com/plugins/toob-tremolo-mono#piPedalUI>
a pipedal_ui:ui ;
pipedal_ui:frequencyPlot
[
lv2:index 4 ;
pipedal_ui:patchProperty <http://two-play.com/plugins/toob-tremolo#lfoShape>;
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 ;
]
.
+1 -1
View File
@@ -40,7 +40,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
TooB Tuner is a chromatic guitar tuner. TooB Tuner is a chromatic guitar tuner.
""" ; """ ;
+1 -1
View File
@@ -39,7 +39,7 @@
doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ; doap:license <https://rerdavies.github.io/pipedal/LicenseToobAmp> ;
doap:maintainer <http://two-play.com/rerdavies#me> ; doap:maintainer <http://two-play.com/rerdavies#me> ;
lv2:minorVersion 0 ; lv2:minorVersion 0 ;
lv2:microVersion 70 ; lv2:microVersion 71 ;
rdfs:comment """ rdfs:comment """
Volume control. Volume control.
Binary file not shown.
Binary file not shown.
@@ -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/
@@ -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. Margarets 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
+8
View File
@@ -106,6 +106,14 @@
lv2:binary <ToobAmp.so> ; lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobMix.ttl> . rdfs:seeAlso <ToobMix.ttl> .
<http://two-play.com/plugins/toob-tremolo> a lv2:Plugin ;
lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobTremolo.ttl> .
<http://two-play.com/plugins/toob-tremolo-mono> a lv2:Plugin ;
lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobTremoloMono.ttl> .
<http://two-play.com/plugins/toob-player> a lv2:Plugin ; <http://two-play.com/plugins/toob-player> a lv2:Plugin ;
lv2:binary <ToobAmp.so> ; lv2:binary <ToobAmp.so> ;
rdfs:seeAlso <ToobPlayer.ttl> . rdfs:seeAlso <ToobPlayer.ttl> .
@@ -1 +0,0 @@
# NeuralPi Tones
+303 -271
View File
@@ -23,316 +23,348 @@
#include "Pedalboard.hpp" #include "Pedalboard.hpp"
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
namespace pipedal { namespace pipedal
{
#define GETTER_SETTER_REF(name) \ #define GETTER_SETTER_REF(name) \
decltype(name##_)& name() { return name##_;} \ decltype(name##_) &name() { return name##_; } \
const decltype(name##_)& name() const { return name##_;} \ const decltype(name##_) &name() const { return name##_; } \
void name(const decltype(name##_) &value) { name##_ = value; } void name(const decltype(name##_) &value) { name##_ = value; }
#define GETTER_SETTER_VEC(name) \ #define GETTER_SETTER_VEC(name) \
decltype(name##_)& name() { return name##_;} \ decltype(name##_) &name() { return name##_; } \
const decltype(name##_)& name() const { return name##_;} const decltype(name##_) &name() const { return name##_; }
#define GETTER_SETTER(name) \
#define GETTER_SETTER(name) \ decltype(name##_) name() const { return name##_; } \
decltype(name##_) name() const { return name##_;} \
void name(decltype(name##_) value) { name##_ = value; } void name(decltype(name##_) value) { name##_ = value; }
class PresetIndexEntry
{
int64_t instanceId_ = 0;
std::string name_;
class PresetIndexEntry { public:
int64_t instanceId_ = 0; PresetIndexEntry() {}
std::string name_; PresetIndexEntry(uint64_t instanceId, const std::string &name)
public: : instanceId_(instanceId), name_(name)
GETTER_SETTER(instanceId);
GETTER_SETTER(name);
DECLARE_JSON_MAP(PresetIndexEntry);
};
class PresetIndex {
int64_t selectedInstanceId_ = -1;
bool presetChanged_ = false;
std::vector<PresetIndexEntry> presets_;
public:
bool GetPresetName(int64_t instanceId, std::string*pResult) {
for (size_t i = 0; i < presets_.size(); ++i)
{ {
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<PresetIndexEntry> presets_;
public:
bool GetPresetName(int64_t instanceId, std::string *pResult)
{
for (size_t i = 0; i < presets_.size(); ++i)
{ {
*pResult = presets_[i].name(); if (presets_[i].instanceId() == instanceId)
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<std::unique_ptr<BankFileEntry>> 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<BankFileEntry> 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<BankFileEntry> entry = std::make_unique<BankFileEntry>();
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)
{ {
++it; *pResult = presets_[i].name();
this->presets_.insert(it,std::move(entry)); return true;
break;
} }
} }
return false;
} }
return instanceId; GETTER_SETTER(selectedInstanceId);
} GETTER_SETTER_VEC(presets);
bool hasItem(int64_t instanceId) GETTER_SETTER(presetChanged);
{
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;
} 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<std::unique_ptr<BankFileEntry>> presets_;
public:
GETTER_SETTER(name);
GETTER_SETTER(nextInstanceId);
GETTER_SETTER(selectedPreset);
GETTER_SETTER_VEC(presets);
void clear()
{ {
if (presets_[i]->instanceId() == instanceId) nextInstanceId_ = 0;
{ presets_.clear();
return *(presets_[i].get()); selectedPreset_ = -1;
}
} }
throw PiPedalArgumentException("Instance not found.");
} void move(size_t from, size_t to)
int64_t deletePreset(int64_t instanceId) {
for (size_t i = 0; i < presets_.size(); ++i)
{ {
if (presets_[i]->instanceId() == instanceId) if (from >= this->presets_.size())
{ {
presets_.erase(presets_.begin()+i); throw std::invalid_argument("Argument out of range.");
int64_t newSelection; }
if (i < presets_.size()) if (to >= this->presets_.size())
{
throw std::invalid_argument("Argument out of range.");
}
std::unique_ptr<BankFileEntry> 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<BankFileEntry> entry = std::make_unique<BankFileEntry>();
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(); if ((*it)->instanceId() == afterItem)
} else if (presets_.size() > 1) { {
newSelection = presets_[0]->instanceId(); ++it;
} else { this->presets_.insert(it, std::move(entry));
// zero length? We can never have a zero-length bank. break;
// 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_) }
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."); bool hasName(const std::string &name)
}
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<BankIndexEntry> 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; for (size_t i = 0; i < presets_.size(); ++i)
}
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); if (presets_[i]->preset().name() == name)
return bank.instanceId(); {
return true;
}
} }
return false;
} }
// else at the end. bool renamePreset(int64_t instanceId, const std::string &name)
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) if (hasName(name))
{ {
return &entries_[i]; throw PiPedalStateException("A preset by that name already exists.");
} }
} for (size_t i = 0; i < presets_.size(); ++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]; 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<BankIndexEntry> 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
#undef GETTER_SETTER_VEC #undef GETTER_SETTER_VEC
#undef GETTER_SETTER_REF #undef GETTER_SETTER_REF
} }
+80 -60
View File
@@ -562,7 +562,6 @@ void InstallAudioService()
#endif #endif
} }
static bool IsP2pServiceEnabled() static bool IsP2pServiceEnabled()
{ {
WifiDirectConfigSettings settings; WifiDirectConfigSettings settings;
@@ -742,7 +741,7 @@ void SetVarPermissions(
catch (const std::exception &e) catch (const std::exception &e)
{ {
// spurious errors on pulse config files. // 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, 80,
81, // unofficial alternate 81, // unofficial alternate
8080, // unofficial alternate 8080, // unofficial alternate
8081, // unofficial alternate 8081, // unofficial alternate
8008, // unofficial alternate 8008, // unofficial alternate
83 // Not official at all. 83 // Not official at all.
}; };
static bool isPortInUse(int port)
{
static bool isPortInUse(int port) {
int sockfd = socket(AF_INET, SOCK_STREAM, 0); int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) { if (sockfd < 0)
{
throw std::runtime_error(SS("Socket creation failed: " << strerror(errno))); throw std::runtime_error(SS("Socket creation failed: " << strerror(errno)));
} }
Finally ff{[sockfd]() { Finally ff{[sockfd]()
close(sockfd); {
}}; close(sockfd);
}};
struct sockaddr_in addr; struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET; addr.sin_family = AF_INET;
@@ -953,23 +952,27 @@ static bool isPortInUse(int port) {
addr.sin_port = htons(port); addr.sin_port = htons(port);
// Try to bind to the 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 (result < 0)
if (errno == EADDRINUSE) { {
return true; // Port is in use if (errno == EADDRINUSE)
} else { {
return true; // Port is in use
}
else
{
cout << "Warning: Unexpected error binding socket to port " << port << ": " << strerror(errno) << endl; cout << "Warning: Unexpected error binding socket to port " << port << ": " << strerror(errno) << endl;
return true; return true;
} }
} }
return false; // Port is free return false; // Port is free
} }
static std::string AutoSelectPort()
static std::string AutoSelectPort() { {
constexpr size_t MAX_PORT = sizeof(autoSelectPorts)/sizeof(autoSelectPorts[0]); constexpr size_t MAX_PORT = sizeof(autoSelectPorts) / sizeof(autoSelectPorts[0]);
int32_t port = -1; int32_t port = -1;
for (size_t i = 0; i < MAX_PORT; ++i) for (size_t i = 0; i < MAX_PORT; ++i)
@@ -980,29 +983,32 @@ static std::string AutoSelectPort() {
break; break;
} }
} }
if (port == -1) { if (port == -1)
{
cout << "Warning: Can't find an available HTTP port. Setting to port 80 anyway." << endl; cout << "Warning: Can't find an available HTTP port. Setting to port 80 anyway." << endl;
port = 80; 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); return SS(port);
} }
bool SetWebServerPort(std::string portOption) bool SetWebServerPort(std::string portOption)
{ {
try { try
{
auto nPos = portOption.find_last_of(':'); auto nPos = portOption.find_last_of(':');
std::string strPort; std::string strPort;
if (nPos != std::string::npos) if (nPos != std::string::npos)
{ {
strPort = portOption.substr(nPos+1); strPort = portOption.substr(nPos + 1);
} else { }
else
{
strPort = portOption; strPort = portOption;
} }
std::istringstream ss(strPort); std::istringstream ss(strPort);
@@ -1021,7 +1027,8 @@ bool SetWebServerPort(std::string portOption)
StartService(true); StartService(true);
cout << "PiPedal web server is listening on port " << iPort << endl; 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; cout << "Error: " << e.what() << endl;
return false; return false;
@@ -1031,7 +1038,8 @@ bool SetWebServerPort(std::string portOption)
void CheckPreemptDynamicConfig() void CheckPreemptDynamicConfig()
{ {
try { try
{
BootConfig bootConfig; BootConfig bootConfig;
if (bootConfig.KernelType() == "PREEMPT_DYNAMIC") if (bootConfig.KernelType() == "PREEMPT_DYNAMIC")
@@ -1044,13 +1052,12 @@ void CheckPreemptDynamicConfig()
cout << endl; cout << endl;
} }
} }
}
} catch(const std::exception& /* ignore */) catch (const std::exception & /* ignore */)
{ {
} }
} }
void Install(const fs::path &programPrefix, const std::string endpointAddress) void Install(const fs::path &programPrefix, const std::string endpointAddress)
{ {
cout << "Configuring pipedal" << endl; cout << "Configuring pipedal" << endl;
@@ -1058,10 +1065,10 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress)
if (!UsingNetworkManager()) if (!UsingNetworkManager())
{ {
cout << "Warning: The current OS is not using the NetworkManager network stack." << endl; 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 << " Consult PiPedal documentation to find out how to correct this " << endl;
cout << " issue on Ubuntu Server." << endl; cout << " issue on Ubuntu Server." << endl;
} }
try try
{ {
@@ -1301,10 +1308,11 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress)
if (endpointAddress.starts_with(ADDR_ANY)) if (endpointAddress.starts_with(ADDR_ANY))
{ {
cout << "PiPedal web server is listening on port " << endpointAddress.substr(ADDR_ANY.length()) << endl; 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; cout << "PiPedal web server is listening on port " << endpointAddress << endl;
} }
} }
static std::string GetCurrentWebServicePort() static std::string GetCurrentWebServicePort()
@@ -1494,25 +1502,27 @@ void ListValidCountryCodes()
if (ccs.size() == 0) if (ccs.size() == 0)
{ {
cout << "No regulatory domains found." << endl; cout << "No regulatory domains found." << endl;
} else { }
using pair = std::pair<std::string,std::string>; else
{
using pair = std::pair<std::string, std::string>;
std::vector<pair> list; std::vector<pair> 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 collator = Locale::GetInstance()->GetCollator();
auto compare = [&collator]( auto compare = [&collator](
pair &left, pair &left,
pair &right) pair &right)
{ {
return collator->Compare( return collator->Compare(
left.second, right.second) < 0; left.second, right.second) < 0;
}; };
std::sort(list.begin(), list.end(), compare); std::sort(list.begin(), list.end(), compare);
for (const auto &entry: list) for (const auto &entry : list)
{ {
cout << entry.first << " - " << entry.second << endl; cout << entry.first << " - " << entry.second << endl;
} }
@@ -1521,12 +1531,15 @@ void ListValidCountryCodes()
static std::string ProcessPortOption(std::string portOption) 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(); std::string t = GetCurrentWebServicePort();
if (portOption != "0") {// allows easy debugging of port AutoSelectPort. if (portOption != "0")
{ // allows easy debugging of port AutoSelectPort.
portOption = t; portOption = t;
} else { }
else
{
portOption = ""; portOption = "";
} }
if (portOption == "") if (portOption == "")
@@ -1569,7 +1582,7 @@ int main(int argc, char **argv)
bool noWifi = false; bool noWifi = false;
parser.AddOption("--nosudo", &nosudo); // strictly a debugging aid. Run without sudo, until we fail because of permissions. 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("--install", &install);
parser.AddOption("--uninstall", &uninstall); parser.AddOption("--uninstall", &uninstall);
parser.AddOption("--stop", &stop); parser.AddOption("--stop", &stop);
@@ -1589,15 +1602,17 @@ int main(int argc, char **argv)
parser.AddOption("--alsa-devices", &alsaDevices); parser.AddOption("--alsa-devices", &alsaDevices);
parser.AddOption("--alsa-device", &alsaDevice); parser.AddOption("--alsa-device", &alsaDevice);
parser.AddOption("--fix-permissions", &fix_permissions); parser.AddOption("--fix-permissions", &fix_permissions);
try try
{ {
parser.Parse(argc, (const char **)argv); parser.Parse(argc, (const char **)argv);
int actionCount = int actionCount =
(!alsaDevice.empty()) + alsaDevices + help + get_current_port + install + (!alsaDevice.empty()) + alsaDevices + help + get_current_port + install +
uninstall + stop + start + enable + disable + enable_hotspot + uninstall + stop + start + enable + disable + enable_hotspot +
disable_hotspot + restart + enable_p2p + disable_p2p + list_p2p_channels + disable_hotspot + restart + enable_p2p + disable_p2p + list_p2p_channels +
fix_permissions + list_wifi_country_codes; fix_permissions + list_wifi_country_codes;
if (actionCount == 0 && portOption.length() != 0) if (actionCount == 0 && portOption.length() != 0)
{ {
++actionCount; ++actionCount;
@@ -1615,6 +1630,12 @@ int main(int argc, char **argv)
throw std::runtime_error("No action provided."); 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 ((!enable_p2p) && (!enable_hotspot) && (!list_p2p_channels))
{ {
if (parser.Arguments().size() != 0) if (parser.Arguments().size() != 0)
@@ -1744,7 +1765,6 @@ int main(int argc, char **argv)
Install(prefix, portOption); Install(prefix, portOption);
FileSystemSync(); FileSystemSync();
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
+4 -2
View File
@@ -153,12 +153,13 @@ bool Pedalboard::SetControlValue(int64_t pedalItemId, const std::string &symbol,
return item->SetControlValue(symbol,value); 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); PedalboardItem*item = GetItem(pedalItemId);
if (!item) return false; 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->title(title);
item->iconColor(iconColor);
return true; return true;
} }
@@ -532,6 +533,7 @@ JSON_MAP_BEGIN(PedalboardItem)
JSON_MAP_REFERENCE(PedalboardItem,pathProperties) JSON_MAP_REFERENCE(PedalboardItem,pathProperties)
JSON_MAP_REFERENCE(PedalboardItem,title) JSON_MAP_REFERENCE(PedalboardItem,title)
JSON_MAP_REFERENCE(PedalboardItem,useModUi) JSON_MAP_REFERENCE(PedalboardItem,useModUi)
JSON_MAP_REFERENCE(PedalboardItem,iconColor)
JSON_MAP_END() JSON_MAP_END()
+3 -1
View File
@@ -106,6 +106,7 @@ public:
std::map<std::string,std::string> pathProperties_; std::map<std::string,std::string> pathProperties_;
std::string title_; std::string title_;
bool useModUi_ = false; bool useModUi_ = false;
std::string iconColor_;
// non persistent state. // non persistent state.
PropertyMap patchProperties; PropertyMap patchProperties;
@@ -135,6 +136,7 @@ public:
GETTER_SETTER(stateUpdateCount) GETTER_SETTER(stateUpdateCount)
GETTER_SETTER_REF(lv2State) GETTER_SETTER_REF(lv2State)
GETTER_SETTER_REF(title) GETTER_SETTER_REF(title)
GETTER_SETTER_REF(iconColor)
GETTER_SETTER(useModUi) GETTER_SETTER(useModUi)
Lv2PluginState&lv2State() { return lv2State_; } // non-const version. 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 INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume.
static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output 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 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 SetItemEnabled(int64_t pedalItemId, bool enabled);
bool SetItemUseModUi(int64_t pedalItemId, bool enabled); bool SetItemUseModUi(int64_t pedalItemId, bool enabled);
void SetCurrentSnapshotModified(bool modified); void SetCurrentSnapshotModified(bool modified);
+60 -28
View File
@@ -33,6 +33,7 @@
#include <iomanip> #include <iomanip>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include <sched.h>
using namespace pipedal; using namespace pipedal;
@@ -59,7 +60,7 @@ void PrintHelp()
pp << Indent(0) << "Syntax\n\n"; pp << Indent(0) << "Syntax\n\n";
pp << Indent(2) << "pipedal_latency_test [<options>] <input-device> [<output-device>]\n\n"; pp << Indent(2) << "pipedal_latency_test [<options>] <input-device> [<output-device>]\n\n";
pp << "where <input-device> is the name of an ALSA capture device and <output-device> is the name of a playback device. " pp << "where <input-device> is the name of an ALSA capture device and <output-device> is the name of a playback device. "
"If <output-device> is omitted, the input device will be used for both capture and playback. Typically the device names start with 'hw:'.\n\n"; "If <output-device> 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(0) << "Options\n\n";
pp << Indent(15); pp << Indent(15);
@@ -85,13 +86,13 @@ void PrintHelp()
<< "PiPedal Latency Tester measures internal buffer delays as well as operating system and " << "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 " << "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"; << "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" pp
<< Indent(6) << "sudo systemctl stop pipedald\n\n"; << "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(0) << "Examples\n\n";
pp << Indent(2) << "pipedal_latency_test --list\n\n"; pp << Indent(2) << "pipedal_latency_test --list\n\n";
@@ -123,7 +124,6 @@ void ListDevices()
} }
} }
using ChannelsT = std::vector<int>; using ChannelsT = std::vector<int>;
class AlsaTester : private AudioDriverHost class AlsaTester : private AudioDriverHost
@@ -169,12 +169,13 @@ public:
delete[] inputBuffers; delete[] inputBuffers;
delete[] outputBuffers; delete[] outputBuffers;
} }
std::vector<std::string> SelectChannels(const std::vector<std::string>&available, const std::vector<int>& selection) std::vector<std::string> SelectChannels(const std::vector<std::string> &available, const std::vector<int> &selection)
{ {
if (selection.size() == 0) return available; if (selection.size() == 0)
return available;
std::vector<std::string> result; std::vector<std::string> result;
for (int sel: selection) for (int sel : selection)
{ {
if (sel < 0 || sel >= available.size()) if (sel < 0 || sel >= available.size())
{ {
@@ -195,14 +196,13 @@ public:
JackConfiguration jackConfiguration; JackConfiguration jackConfiguration;
jackConfiguration.AlsaInitialize(serverSettings); jackConfiguration.AlsaInitialize(serverSettings);
auto & availableInputs = jackConfiguration.inputAudioPorts(); auto &availableInputs = jackConfiguration.inputAudioPorts();
auto & availableOutputs = jackConfiguration.outputAudioPorts(); auto &availableOutputs = jackConfiguration.outputAudioPorts();
std::vector<std::string> inputAudioPorts, outputAudioPorts; std::vector<std::string> inputAudioPorts, outputAudioPorts;
inputAudioPorts = SelectChannels(availableInputs,this->inputChannels); inputAudioPorts = SelectChannels(availableInputs, this->inputChannels);
outputAudioPorts = SelectChannels(availableOutputs,this->outputChannels); outputAudioPorts = SelectChannels(availableOutputs, this->outputChannels);
JackChannelSelection channelSelection( JackChannelSelection channelSelection(
inputAudioPorts, outputAudioPorts, inputAudioPorts, outputAudioPorts,
@@ -360,7 +360,6 @@ public:
} }
virtual void OnAudioTerminated() virtual void OnAudioTerminated()
{ {
} }
virtual void OnProcess(size_t nFrames) virtual void OnProcess(size_t nFrames)
{ {
@@ -413,7 +412,7 @@ TestResult RunLatencyTest(
const ChannelsT &outputChannels, const ChannelsT &outputChannels,
uint32_t sampleRate, int bufferSize, int buffers) 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(); return tester.Test();
} }
@@ -432,6 +431,28 @@ static std::string overheadDisplay(float value)
return s.str(); return s.str();
} }
static bool testRealtimePriorityPrivileges()
{
struct sched_param currentParam;
int currentPolicy = sched_getscheduler(0);
if (currentPolicy == -1) {
return false;
}
if (sched_getparam(0, &currentParam) != 0) {
return false;
}
struct sched_param param;
param.sched_priority = 85;
if (sched_setscheduler(0, SCHED_RR, &param) != 0)
{
return false;
}
// Restore normal priority
sched_setscheduler(0, currentPolicy, &currentParam);
return true;
}
void RunLatencyTest( void RunLatencyTest(
const std::string &inputDeviceId, const std::string &inputDeviceId,
const std::string &outputDeviceId, const std::string &outputDeviceId,
@@ -439,8 +460,20 @@ void RunLatencyTest(
const ChannelsT &outputChannels, const ChannelsT &outputChannels,
uint32_t sampleRate) uint32_t sampleRate)
{ {
PrettyPrinter pp; 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 <your user id>" << "\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 SIZE_COLUMN_WIDTH = 8;
const int BUFFERS_COLUMN_WIDTH = 20; const int BUFFERS_COLUMN_WIDTH = 20;
@@ -467,7 +500,7 @@ void RunLatencyTest(
for (auto bufferCount : bufferCounts) 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); pp.Column(column);
column += BUFFERS_COLUMN_WIDTH; column += BUFFERS_COLUMN_WIDTH;
@@ -495,8 +528,7 @@ void RunLatencyTest(
} }
} }
ChannelsT ParseChannels(const std::string &channels)
ChannelsT ParseChannels(const std::string&channels)
{ {
ChannelsT result; ChannelsT result;
std::stringstream s(channels); std::stringstream s(channels);
@@ -504,9 +536,11 @@ ChannelsT ParseChannels(const std::string&channels)
while (true) while (true)
{ {
int c = s.peek(); int c = s.peek();
if (c == -1) break; if (c == -1)
break;
if (c == ',') { if (c == ',')
{
s.get(); s.get();
c = s.peek(); c = s.peek();
} }
@@ -518,14 +552,12 @@ ChannelsT ParseChannels(const std::string&channels)
while (s.peek() >= '0' && s.peek() <= '9') while (s.peek() >= '0' && s.peek() <= '9')
{ {
c = s.get(); c = s.get();
v = v*10 + c-'0'; v = v * 10 + c - '0';
} }
result.push_back(v); result.push_back(v);
} }
return result; return result;
} }
int main(int argc, const char **argv) int main(int argc, const char **argv)
@@ -567,7 +599,7 @@ std:
inputChannels = ParseChannels(strInputChannels); inputChannels = ParseChannels(strInputChannels);
outputChannels = ParseChannels(strOutputChannels); 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; std::string outDev = parser.Arguments().size() == 2 ? parser.Arguments()[1] : inDev;
RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate); RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate);
} }
+41 -13
View File
@@ -887,7 +887,7 @@ int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string &
return presetId; 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<std::recursive_mutex> guard{mutex}; std::lock_guard<std::recursive_mutex> guard{mutex};
@@ -897,7 +897,7 @@ int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &n
UpdateVst3Settings(pedalboard); UpdateVst3Settings(pedalboard);
pedalboard.name(name); pedalboard.name(name);
int64_t result = storage.SaveCurrentPresetAs(pedalboard, name, saveAfterInstanceId); int64_t result = storage.SaveCurrentPresetAs(pedalboard, bankInstanceId, name, saveAfterInstanceId);
FirePresetsChanged(clientId); FirePresetsChanged(clientId);
return result; return result;
} }
@@ -1174,12 +1174,12 @@ int64_t PiPedalModel::DeleteBank(int64_t clientId, int64_t instanceId)
return newSelection; return newSelection;
} }
int64_t PiPedalModel::DeletePreset(int64_t clientId, int64_t instanceId) int64_t PiPedalModel::DeletePresets(int64_t clientId, const std::vector<int64_t> &presetInstanceIds)
{ {
std::lock_guard<std::recursive_mutex> guard{mutex}; std::lock_guard<std::recursive_mutex> guard{mutex};
int64_t oldSelection = storage.GetCurrentPresetId(); int64_t oldSelection = storage.GetCurrentPresetId();
int64_t newSelection = storage.DeletePreset(instanceId); int64_t newSelection = storage.DeletePresets(presetInstanceIds);
this->FirePresetsChanged(clientId); // fire now. this->FirePresetsChanged(clientId); // fire BEFORE we load a new preset.
if (oldSelection != newSelection) if (oldSelection != newSelection)
{ {
this->LoadPreset( this->LoadPreset(
@@ -1745,15 +1745,18 @@ void PiPedalModel::SendSetPatchProperty(
{ {
std::shared_ptr<Lv2PluginInfo> pluginInfo = GetPluginInfo(pedalboardItem->uri_); std::shared_ptr<Lv2PluginInfo> pluginInfo = GetPluginInfo(pedalboardItem->uri_);
auto pipedalUi = pluginInfo->piPedalUI(); auto pipedalUi = pluginInfo->piPedalUI();
auto fileProperty = pipedalUi->GetFileProperty(propertyUri); if (pipedalUi)
if (fileProperty)
{ {
auto fileProperty = pipedalUi->GetFileProperty(propertyUri);
if (fileProperty)
{
json_variant abstractPath = pluginHost.AbstractPath(value); json_variant abstractPath = pluginHost.AbstractPath(value);
std::string atomString = abstractPath.to_string(); std::string atomString = abstractPath.to_string();
pedalboardItem->pathProperties_[propertyUri] = atomString; pedalboardItem->pathProperties_[propertyUri] = atomString;
}
this->SetPresetChanged(clientId, true);
} }
this->SetPresetChanged(clientId, true);
} }
LV2_Atom *atomValue = atomConverter.ToAtom(value); LV2_Atom *atomValue = atomConverter.ToAtom(value);
@@ -3226,10 +3229,10 @@ void PiPedalModel::MoveAudioFile(
AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory); AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory);
dir->MoveAudioFile(directory, fromPosition, toPosition); 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<std::recursive_mutex> lock(mutex); std::lock_guard<std::recursive_mutex> lock(mutex);
if (!this->pedalboard.SetItemTitle(instanceId, title)) if (!this->pedalboard.SetItemTitle(instanceId, title, colorKey))
{ {
return; return;
} }
@@ -3254,3 +3257,28 @@ bool PiPedalModel::HasTone3000Auth() const
{ {
return storage.GetTone3000Auth() != ""; return storage.GetTone3000Auth() != "";
} }
std::vector<PresetIndexEntry> PiPedalModel::RequestBankPresets(int64_t bankInstanceId)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.RequestBankPresets(bankInstanceId);
}
int64_t PiPedalModel::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
uint64_t lastAdded = storage.ImportPresetsFromBank(bankInstanceId, presets);
FirePresetsChanged(-1);
return lastAdded;
}
int64_t PiPedalModel::CopyPresetsToBank(int64_t bankInstanceId, const std::vector<int64_t> &presets)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
uint64_t lastAdded = storage.CopyPresetsToBank(bankInstanceId, presets);
return lastAdded;
}
+7 -3
View File
@@ -378,13 +378,14 @@ namespace pipedal
int64_t UploadPreset(const BankFile &bankFile, int64_t uploadAfter = -1); int64_t UploadPreset(const BankFile &bankFile, int64_t uploadAfter = -1);
void UploadPluginPresets(const PluginPresets &pluginPresets); void UploadPluginPresets(const PluginPresets &pluginPresets);
void SaveCurrentPreset(int64_t clientId); 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); int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name);
void LoadPreset(int64_t clientId, int64_t instanceId); void LoadPreset(int64_t clientId, int64_t instanceId);
bool UpdatePresets(int64_t clientId, const PresetIndex &presets); bool UpdatePresets(int64_t clientId, const PresetIndex &presets);
void UpdatePluginPresets(const PluginUiPresets &pluginPresets); void UpdatePluginPresets(const PluginUiPresets &pluginPresets);
int64_t DeletePreset(int64_t clientId, int64_t instanceId); int64_t DeletePresets(int64_t clientId, const std::vector<int64_t> &presetInstanceIds);
bool RenamePreset(int64_t clientId, int64_t instanceId, const std::string &name); bool RenamePreset(int64_t clientId, int64_t instanceId, const std::string &name);
int64_t CopyPreset(int64_t clientId, int64_t fromIndex, int64_t toIndex); int64_t CopyPreset(int64_t clientId, int64_t fromIndex, int64_t toIndex);
uint64_t CopyPluginPreset(const std::string &pluginUri, uint64_t presetId); uint64_t CopyPluginPreset(const std::string &pluginUri, uint64_t presetId);
@@ -486,13 +487,16 @@ namespace pipedal
int32_t from, int32_t from,
int32_t to); 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); void SetTone3000Auth(const std::string &apiKey);
bool HasTone3000Auth() const; bool HasTone3000Auth() const;
void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId); void SetSelectedPedalboardPlugin(uint64_t clientId, uint64_t pedalboardId);
std::vector<PresetIndexEntry> RequestBankPresets(int64_t bankInstanceId);
int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
}; };
} // namespace pipedal. } // namespace pipedal.
+62 -7
View File
@@ -46,6 +46,38 @@
using namespace std; using namespace std;
using namespace pipedal; using namespace pipedal;
class CopyPresetsToBankBody {
public:
int64_t bankInstanceId_;
std::vector<int64_t> 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<int64_t> 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 class PathPatchPropertyChangedBody
{ {
public: public:
@@ -179,12 +211,14 @@ class SetPedalboardItemTitleBody
public: public:
uint64_t instanceId_; uint64_t instanceId_;
std::string title_; std::string title_;
std::string colorKey_;
DECLARE_JSON_MAP(SetPedalboardItemTitleBody); DECLARE_JSON_MAP(SetPedalboardItemTitleBody);
}; };
JSON_MAP_BEGIN(SetPedalboardItemTitleBody) JSON_MAP_BEGIN(SetPedalboardItemTitleBody)
JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, instanceId) JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, instanceId)
JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, title) JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, title)
JSON_MAP_REFERENCE(SetPedalboardItemTitleBody, colorKey)
JSON_MAP_END() JSON_MAP_END()
@@ -334,6 +368,7 @@ class SaveCurrentPresetAsBody
{ {
public: public:
int64_t clientId_ = -1; int64_t clientId_ = -1;
int64_t bankInstanceId_ = -1;
std::string name_; std::string name_;
int64_t saveAfterInstanceId_ = -1; int64_t saveAfterInstanceId_ = -1;
@@ -341,6 +376,7 @@ public:
}; };
JSON_MAP_BEGIN(SaveCurrentPresetAsBody) JSON_MAP_BEGIN(SaveCurrentPresetAsBody)
JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, clientId) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, clientId)
JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, bankInstanceId)
JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, name) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, name)
JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, saveAfterInstanceId) JSON_MAP_REFERENCE(SaveCurrentPresetAsBody, saveAfterInstanceId)
JSON_MAP_END() JSON_MAP_END()
@@ -1312,7 +1348,7 @@ public:
{ {
SaveCurrentPresetAsBody body; SaveCurrentPresetAsBody body;
pReader->read(&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); Reply(replyTo, "saveCurrentPresetsAs", result);
} }
else if (message == "setSelectedPedalboardPlugin") else if (message == "setSelectedPedalboardPlugin")
@@ -1445,12 +1481,12 @@ public:
model.RequestShutdown(true); model.RequestShutdown(true);
this->Reply(replyTo, "restart"); this->Reply(replyTo, "restart");
} }
else if (message == "deletePresetItem") else if (message == "deletePresetItems")
{ {
int64_t instanceId = 0; std::vector<int64_t> items;
pReader->read(&instanceId); pReader->read(&items);
int64_t result = model.DeletePreset(this->clientId, instanceId); int64_t result = model.DeletePresets(this->clientId, items);
this->Reply(replyTo, "deletePresetItem", result); this->Reply(replyTo, "deletePresetItems", result);
} }
else if (message == "deleteBankItem") else if (message == "deleteBankItem")
{ {
@@ -1569,7 +1605,7 @@ public:
{ {
SetPedalboardItemTitleBody body; SetPedalboardItemTitleBody body;
pReader->read(&body); pReader->read(&body);
model.SetPedalboardItemTitle(body.instanceId_, body.title_); model.SetPedalboardItemTitle(body.instanceId_, body.title_, body.colorKey_);
} }
else if (message == "getPatchProperty") else if (message == "getPatchProperty")
{ {
@@ -1792,6 +1828,25 @@ public:
{ {
std::vector<AlsaSequencerPortSelection> result = model.GetAlsaSequencerPorts(); std::vector<AlsaSequencerPortSelection> result = model.GetAlsaSequencerPorts();
this->Reply(replyTo,"getAlsaSequencerPorts", result); 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 else
{ {
+3 -1
View File
@@ -538,7 +538,8 @@ UiFrequencyPlot::UiFrequencyPlot(PluginHost*pHost, const LilvNode*node,
this->xRight_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__xRight,22000); this->xRight_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__xRight,22000);
this->yTop_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__yTop,5); this->yTop_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__yTop,5);
this->yBottom_ = GetFloat(pWorld,node,pHost->lilvUris->pipedalUI__yBottom,-35); 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); 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, xLeft)
JSON_MAP_REFERENCE(UiFrequencyPlot, xRight) JSON_MAP_REFERENCE(UiFrequencyPlot, xRight)
JSON_MAP_REFERENCE(UiFrequencyPlot, xLog) JSON_MAP_REFERENCE(UiFrequencyPlot, xLog)
JSON_MAP_REFERENCE(UiFrequencyPlot, yDb)
JSON_MAP_REFERENCE(UiFrequencyPlot, yTop) JSON_MAP_REFERENCE(UiFrequencyPlot, yTop)
JSON_MAP_REFERENCE(UiFrequencyPlot, yBottom) JSON_MAP_REFERENCE(UiFrequencyPlot, yBottom)
JSON_MAP_REFERENCE(UiFrequencyPlot, width) JSON_MAP_REFERENCE(UiFrequencyPlot, width)
+3
View File
@@ -63,6 +63,7 @@
#define PIPEDAL_UI__xLeft PIPEDAL_UI_PREFIX "xLeft" #define PIPEDAL_UI__xLeft PIPEDAL_UI_PREFIX "xLeft"
#define PIPEDAL_UI__xRight PIPEDAL_UI_PREFIX "xRight" #define PIPEDAL_UI__xRight PIPEDAL_UI_PREFIX "xRight"
#define PIPEDAL_UI__xLog PIPEDAL_UI_PREFIX "xLog" #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__yTop PIPEDAL_UI_PREFIX "yTop"
#define PIPEDAL_UI__yBottom PIPEDAL_UI_PREFIX "yBottom" #define PIPEDAL_UI__yBottom PIPEDAL_UI_PREFIX "yBottom"
#define PIPEDAL_UI__width PIPEDAL_UI_PREFIX "width" #define PIPEDAL_UI__width PIPEDAL_UI_PREFIX "width"
@@ -196,6 +197,7 @@ namespace pipedal
float yTop_ = 5; float yTop_ = 5;
float yBottom_ = -30; float yBottom_ = -30;
bool xLog_ = true; bool xLog_ = true;
bool yDb_ = true;
float width_ = 60; float width_ = 60;
public: public:
@@ -213,6 +215,7 @@ namespace pipedal
float yTop() const { return yTop_; } float yTop() const { return yTop_; }
float yBottom() const { return yBottom_; } float yBottom() const { return yBottom_; }
float width() const { return width_; } float width() const { return width_; }
bool yDb() const { return yDb_; }
public: public:
DECLARE_JSON_MAP(UiFrequencyPlot); DECLARE_JSON_MAP(UiFrequencyPlot);
+1
View File
@@ -165,6 +165,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
pipedalUI__yTop = lilv_new_uri(pWorld, PIPEDAL_UI__yTop); pipedalUI__yTop = lilv_new_uri(pWorld, PIPEDAL_UI__yTop);
pipedalUI__yBottom = lilv_new_uri(pWorld, PIPEDAL_UI__yBottom); pipedalUI__yBottom = lilv_new_uri(pWorld, PIPEDAL_UI__yBottom);
pipedalUI__xLog = lilv_new_uri(pWorld, PIPEDAL_UI__xLog); 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__width = lilv_new_uri(pWorld, PIPEDAL_UI__width);
pipedalUI__graphicEq = lilv_new_uri(pWorld, PIPEDAL_UI__graphicEq); pipedalUI__graphicEq = lilv_new_uri(pWorld, PIPEDAL_UI__graphicEq);
+1
View File
@@ -810,6 +810,7 @@ namespace pipedal
AutoLilvNode pipedalUI__xLeft; AutoLilvNode pipedalUI__xLeft;
AutoLilvNode pipedalUI__xRight; AutoLilvNode pipedalUI__xRight;
AutoLilvNode pipedalUI__xLog; AutoLilvNode pipedalUI__xLog;
AutoLilvNode pipedalUI__yDb;
AutoLilvNode pipedalUI__yTop; AutoLilvNode pipedalUI__yTop;
AutoLilvNode pipedalUI__yBottom; AutoLilvNode pipedalUI__yBottom;
AutoLilvNode pipedalUI__width; AutoLilvNode pipedalUI__width;
+171 -15
View File
@@ -225,7 +225,6 @@ static void CopyDirectory(const std::filesystem::path &source, const std::filesy
} }
} }
void Storage::MaybeCopyDefaultPresets() void Storage::MaybeCopyDefaultPresets()
{ {
auto presetsDirectory = this->GetPresetsDirectory(); auto presetsDirectory = this->GetPresetsDirectory();
@@ -239,13 +238,17 @@ void Storage::MaybeCopyDefaultPresets()
} }
static void removeFileNoThrow(const std::filesystem::path &path) static void removeFileNoThrow(const std::filesystem::path &path)
{ {
try { try
{
fs::remove(path); fs::remove(path);
} catch (const std::exception&) }
catch (const std::exception &)
{ {
} }
} }
void Storage::UpgradeFactoryPresets() void Storage::UpgradeFactoryPresets()
{ {
auto presetsDirectory = this->GetPresetsDirectory(); auto presetsDirectory = this->GetPresetsDirectory();
@@ -520,7 +523,6 @@ void Storage::SavePluginPresetIndex()
} }
} }
void Storage::SaveBankIndex() void Storage::SaveBankIndex()
{ {
pipedal::ofstream_synced os; pipedal::ofstream_synced os;
@@ -653,17 +655,162 @@ void Storage::SaveCurrentPreset(const Pedalboard &pedalboard)
item.preset(pedalboard); item.preset(pedalboard);
SaveCurrentBank(); 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; Pedalboard newPedalboard = pedalboard;
newPedalboard.name(name); newPedalboard.name(name);
int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId); if (bankInstanceId == this->bankIndex.selectedBank())
currentBank.selectedPreset(newInstanceId); {
SaveCurrentBank(); int64_t newInstanceId = currentBank.addPreset(newPedalboard, saveAfterInstanceId);
return newInstanceId; 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<std::string>&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<int64_t> &presets)
{
if (bankIndex.selectedBank() == bankInstanceId) {
throw std::runtime_error("Can't import to self.");
}
std::set<int64_t> presetsSet { presets.begin(), presets.end()};
auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId);
std::set<std::string> 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<int64_t> &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<int64_t> presetsSet { presets.begin(), presets.end()};
std::set<std::string> 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<PresetIndexEntry> Storage::RequestBankPresets(int64_t bankInstanceId)
{
auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId);
std::vector<PresetIndexEntry> 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) void Storage::SetPresetIndex(const PresetIndex &presets)
{ {
// painful because we must move unique_ptrs. // painful because we must move unique_ptrs.
@@ -734,9 +881,12 @@ Pedalboard Storage::GetPreset(int64_t instanceId) const
throw PiPedalException("Not found."); throw PiPedalException("Not found.");
} }
int64_t Storage::DeletePreset(int64_t presetId) int64_t Storage::DeletePresets(const std::vector<int64_t> &presetInstanceIds)
{ {
int64_t newSelection = currentBank.deletePreset(presetId); int64_t newSelection = currentBank.selectedPreset();
for (auto presetId: presetInstanceIds) {
newSelection = currentBank.deletePreset(presetId);
}
SaveCurrentBank(); SaveCurrentBank();
return newSelection; return newSelection;
} }
@@ -826,25 +976,30 @@ int64_t Storage::CreateNewPreset()
} }
} }
newPedalboard.name(name); 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) int64_t Storage::CopyPreset(int64_t fromId, int64_t toId)
{ {
auto &fromItem = this->currentBank.getItem(fromId); auto &fromItem = this->currentBank.getItem(fromId);
int64_t result;
if (toId == -1) if (toId == -1)
{ {
Pedalboard newPedalboard = fromItem.preset(); Pedalboard newPedalboard = fromItem.preset();
std::string name = GetPresetCopyName(fromItem.preset().name()); std::string name = GetPresetCopyName(fromItem.preset().name());
newPedalboard.name(name); newPedalboard.name(name);
return this->currentBank.addPreset(newPedalboard, fromId); result = this->currentBank.addPreset(newPedalboard, fromId);
} }
else else
{ {
auto &toItem = this->currentBank.getItem(toId); auto &toItem = this->currentBank.getItem(toId);
toItem.preset(fromItem.preset()); toItem.preset(fromItem.preset());
return toId; result = toId;
} }
SaveCurrentBank();
return result;
} }
void Storage::SetJackChannelSelection(const JackChannelSelection &channelSelection) void Storage::SetJackChannelSelection(const JackChannelSelection &channelSelection)
@@ -2861,6 +3016,7 @@ std::string Storage::FromAbstractPathJson(const std::string &pathJson)
return v.to_string(); return v.to_string();
} }
JSON_MAP_BEGIN(UserSettings) JSON_MAP_BEGIN(UserSettings)
JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, governor)
JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
+7 -2
View File
@@ -149,7 +149,7 @@ public:
int64_t GetBankByMidiBankNumber(uint8_t bankNumber); int64_t GetBankByMidiBankNumber(uint8_t bankNumber);
const Pedalboard& GetCurrentPreset(); const Pedalboard& GetCurrentPreset();
void SaveCurrentPreset(const Pedalboard&pedalboard); 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; int64_t GetCurrentPresetId() const;
void GetPresetIndex(PresetIndex*pResult); void GetPresetIndex(PresetIndex*pResult);
@@ -162,7 +162,7 @@ public:
bool LoadPreset(int64_t presetId); bool LoadPreset(int64_t presetId);
int64_t DeletePreset(int64_t presetId); int64_t DeletePresets(const std::vector<int64_t>& presetInstanceIds);
bool RenamePreset(int64_t presetId, const std::string&name); bool RenamePreset(int64_t presetId, const std::string&name);
int64_t CopyPreset(int64_t fromId, int64_t toId = -1); int64_t CopyPreset(int64_t fromId, int64_t toId = -1);
int64_t CreateNewPreset(); int64_t CreateNewPreset();
@@ -268,6 +268,11 @@ public:
void LoadTone3000Auth(); void LoadTone3000Auth();
void SetTone3000Auth(const std::string&apiKey); void SetTone3000Auth(const std::string&apiKey);
std::string GetTone3000Auth() const; std::string GetTone3000Auth() const;
std::vector<PresetIndexEntry> RequestBankPresets(int64_t bankInstanceId);
int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector<int64_t> &presets);
}; };
+129 -15
View File
@@ -43,10 +43,11 @@ std::string psystem(const std::string &command)
{ {
throw std::runtime_error("popen() failed!"); throw std::runtime_error("popen() failed!");
} }
Finally ff{[pipe]() { Finally ff{[pipe]()
pclose(pipe); {
}}; pclose(pipe);
while (fgets(buffer.data(), buffer.size()-1, pipe) != nullptr) }};
while (fgets(buffer.data(), buffer.size() - 1, pipe) != nullptr)
{ {
result += buffer.data(); result += buffer.data();
} }
@@ -68,7 +69,8 @@ void SignPackage()
{ {
packagePath = SS("build/pipedal_" << PROJECT_VER << "_amd64.deb"); packagePath = SS("build/pipedal_" << PROJECT_VER << "_amd64.deb");
packagePath = fs::absolute(packagePath); packagePath = fs::absolute(packagePath);
if (!fs::exists(packagePath)) { if (!fs::exists(packagePath))
{
packagePath = SS("build/pipedal_" << PROJECT_VER << "_*.deb"); packagePath = SS("build/pipedal_" << PROJECT_VER << "_*.deb");
throw std::runtime_error(SS("File does not exist: " << packagePath)); throw std::runtime_error(SS("File does not exist: " << packagePath));
} }
@@ -82,8 +84,8 @@ void SignPackage()
std::string signCmd = std::string signCmd =
SS("/usr/bin/gpg --pinentry-mode loopback --yes --default-key " << UPDATE_GPG_FINGERPRINT2 SS("/usr/bin/gpg --pinentry-mode loopback --yes --default-key " << UPDATE_GPG_FINGERPRINT2
<< " --armor --output " << packagePath << ".asc" << " --armor --output " << packagePath << ".asc"
<< " --detach-sign " << packagePath.c_str()); << " --detach-sign " << packagePath.c_str());
int result = system(signCmd.c_str()); int result = system(signCmd.c_str());
if (result != EXIT_SUCCESS) if (result != EXIT_SUCCESS)
{ {
@@ -226,6 +228,8 @@ public:
bool draft = false, prerelease = false; bool draft = false, prerelease = false;
std::vector<DownloadCountAsset> assets; std::vector<DownloadCountAsset> assets;
std::string published_at; 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) 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); auto tt = std::chrono::system_clock::to_time_t(tp);
std::tm tm = *std::gmtime(&tt); std::tm tm = *std::gmtime(&tt);
std::stringstream ss; 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(); 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) void GetDownloadCounts(bool gplotDownloads)
{ {
// error handling and temporary file use is different enough that it justifies // 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 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<std::chrono::seconds>(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) 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 &release : releases)
{ {
for (const auto &asset : release.assets) for (const auto &asset : release.assets)
@@ -339,7 +415,10 @@ void GetDownloadCounts(bool gplotDownloads)
cumulativeCount += asset.downloads; cumulativeCount += asset.downloads;
} }
} }
cout << release.published_at << " " << cumulativeCount << endl; cout << release.published_at << " " << cumulativeCount
<< " " << release.average_daily_downloads
<< " " << "\"" << release.name << "\""
<< endl;
} }
} }
else else
@@ -351,14 +430,49 @@ void GetDownloadCounts(bool gplotDownloads)
{ {
return left.published_at > right.published_at; // latest date first. 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) for (const auto &release : releases)
{ {
cout << release.name << endl; cout << release.name << " " << release.published_at << endl;
for (const auto &asset : release.assets) 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 << " " << 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;
} }
} }
} }
-6
View File
@@ -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 ls
Carla Project icons. Carla Project icons.
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sudo /bin/bash
rm -rf /etc/pipedal/react
mkdir -p /etc/pipedal/react
cp -r dist/* /etc/pipedal/react/
+24
View File
@@ -28,6 +28,7 @@ import { isDarkMode } from './DarkMode';
import Tone3000AuthComplete from './Tone3000AuthComplete'; import Tone3000AuthComplete from './Tone3000AuthComplete';
import FontTest from './FontTest'; import FontTest from './FontTest';
import IconTest from './IconTest';
declare module '@mui/material/styles' { declare module '@mui/material/styles' {
interface Theme { interface Theme {
@@ -38,6 +39,12 @@ declare module '@mui/material/styles' {
mainBackground?: React.CSSProperties['color']; mainBackground?: React.CSSProperties['color'];
toolbarColor?: 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: { secondary: {
main: "#FF6060" main: "#FF6060"
},
actionBar: {
main: '#130b22ff',
contrastText: '#FFFFFF'
} }
}, },
mainBackground: "#222", mainBackground: "#222",
toolbarColor: '#222' toolbarColor: '#222'
@@ -208,8 +220,14 @@ const theme = createTheme(
}, },
secondary: { secondary: {
main: "#FF6060" main: "#FF6060"
},
actionBar: {
main: '#130b22ff',
contrastText: '#FFFFFF'
} }
}, },
mainBackground: "#FFFFFF", mainBackground: "#FFFFFF",
toolbarColor: '#FFFFFF' toolbarColor: '#FFFFFF'
@@ -235,6 +253,11 @@ function isFontTest() {
let param = url.searchParams.get("fontTest"); let param = url.searchParams.get("fontTest");
return (param !== null) 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 { const App = (class extends React.Component {
// Before the component mounts, we initialise our state // Before the component mounts, we initialise our state
@@ -258,6 +281,7 @@ const App = (class extends React.Component {
{ {
isTone3000Auth() && (<Tone3000AuthComplete />) isTone3000Auth() && (<Tone3000AuthComplete />)
|| isFontTest() && (<FontTest />) || isFontTest() && (<FontTest />)
|| isIconTest() && (<IconTest />)
|| (<AppThemed />) || (<AppThemed />)
} }
</ThemeProvider> </ThemeProvider>
@@ -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<CopyPresetsToBankDialogProps, CopyPresetsToBankDialogState> {
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 (
<DialogEx tag="importPreset" open={open} fullWidth maxWidth="xs" onClose={handleClose} aria-labelledby="Rename-dialog-title"
fullScreen={this.state.fullScreen}
style={{ userSelect: "none" }}
onEnterKey={() => { }}
>
<div style={{ display: "flex", flexFlow: "column nowrap", height: this.state.fullScreen ? "100vh" : undefined }}>
{!this.state.fullScreen && (
<DialogTitle style={{ paddingTop: 8, paddingBottom: 0 }}>
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
Copy Presets to Bank
</Typography>
</Toolbar>
</DialogTitle>
)}
<DialogContent style={{ flex: "0 0 auto", paddingTop: 8, paddingBottom: 8, }}>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 0 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{ marginBottom: 8 }}
onChange={(e) => { this.handleBankChanged(e.target.value as number); }}>
{this.state.banks.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
</DialogContent>
<DialogActions style={{ flex: "0 0 auto" }}>
<Button onClick={handleClose} variant="dialogSecondary" >
Cancel
</Button>
<Button
onClick={() => { this.handleOk(); }} variant="dialogPrimary"
disabled={false}
>
OK
</Button>
</DialogActions>
</div>
</DialogEx>
);
}
}
+7 -2
View File
@@ -27,7 +27,7 @@ import { PiPedalStateError } from './PiPedalError';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
const SELECT_SCALE = 1.5; const SELECT_SCALE = 1.2;
const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_TICK_DELAY = 30;
const AUTOSCROLL_THRESHOLD = 48; const AUTOSCROLL_THRESHOLD = 48;
@@ -58,6 +58,7 @@ export interface DraggableProps extends WithStyles<typeof styles> {
children?: ReactNode | ReactNode[]; children?: ReactNode | ReactNode[];
draggable?: boolean; draggable?: boolean;
style?: React.CSSProperties;
} }
@@ -149,6 +150,7 @@ const Draggable =
startClientY: number = 0; startClientY: number = 0;
dragStarted: boolean = false; dragStarted: boolean = false;
savedIndex: string = ""; savedIndex: string = "";
savedOpacity: string = "";
lastPointerDown: number = 0; lastPointerDown: number = 0;
originalBounds?: DOMRect; originalBounds?: DOMRect;
captureElement?: HTMLDivElement; captureElement?: HTMLDivElement;
@@ -244,6 +246,8 @@ const Draggable =
this.pointerId = e.pointerId; this.pointerId = e.pointerId;
this.pointerType = e.pointerType; this.pointerType = e.pointerType;
this.savedIndex = e.currentTarget.style.zIndex; this.savedIndex = e.currentTarget.style.zIndex;
this.savedOpacity = e.currentTarget.style.opacity;
e.currentTarget.style.opacity = "0.9";
if (this.pointerType !== "touch") { if (this.pointerType !== "touch") {
this.dragTarget.style.transform = "scale(" + SELECT_SCALE + ")"; this.dragTarget.style.transform = "scale(" + SELECT_SCALE + ")";
this.dragTarget.style.zIndex = "3"; this.dragTarget.style.zIndex = "3";
@@ -254,6 +258,7 @@ const Draggable =
clearDragTransform(element: HTMLDivElement): void { clearDragTransform(element: HTMLDivElement): void {
element.style.transform = ""; element.style.transform = "";
element.style.zIndex = this.savedIndex; element.style.zIndex = this.savedIndex;
element.style.opacity = this.savedOpacity;
} }
onPointerCancel(e: PointerEvent<HTMLDivElement>) { onPointerCancel(e: PointerEvent<HTMLDivElement>) {
@@ -486,7 +491,7 @@ const Draggable =
render() { render() {
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
return ( return (
<div className={classes.frame} style={{ transform: "" }} <div className={classes.frame} style={{ transform: "", ...this.props.style }}
onPointerDown={this.onPointerDown} onPointerDown={this.onPointerDown}
onPointerMove={this.onPointerMove} onPointerMove={this.onPointerMove}
onPointerCancel={this.onPointerCancel} onPointerCancel={this.onPointerCancel}
+14 -4
View File
@@ -82,7 +82,7 @@ export interface DraggableGridProps
defaultSelectedIndex?: number; defaultSelectedIndex?: number;
onLongPress?: (itemIndex: number) => void; onLongPress?: (event: PointerEvent<HTMLDivElement>, itemIndex: number) => void;
canDrag?: boolean; canDrag?: boolean;
@@ -209,7 +209,7 @@ const DraggableGrid =
} }
bringSelectedItemIntoView() { bringSelectedItemIntoView() {
if (this.state.indexToSelect && this.state.indexToSelect >= 0) if (this.state.indexToSelect !== undefined && this.state.indexToSelect >= 0)
{ {
let grid = this.refGrid.current; let grid = this.refGrid.current;
if (grid) if (grid)
@@ -295,6 +295,7 @@ const DraggableGrid =
startClientX: number = 0; startClientX: number = 0;
startClientY: number = 0; startClientY: number = 0;
dragStarted: boolean = false; dragStarted: boolean = false;
dragThresholdWasExceeded: boolean = false;
savedIndex: string = ""; savedIndex: string = "";
savedBackground: string = ""; savedBackground: string = "";
lastPointerDown: number = 0; lastPointerDown: number = 0;
@@ -529,6 +530,11 @@ const DraggableGrid =
if (this.startIndex !== this.currentIndex) { if (this.startIndex !== this.currentIndex) {
this.props.moveElement(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(); this.cancelDrag();
} }
@@ -693,6 +699,7 @@ const DraggableGrid =
///window.addEventListener("touchstart",this.handleTouchStart, {passive: false}); ///window.addEventListener("touchstart",this.handleTouchStart, {passive: false});
this.dragStarted = false; this.dragStarted = false;
this.dragThresholdWasExceeded = false;
this.startX = e.clientX; this.startX = e.clientX;
this.startY = e.clientY; this.startY = e.clientY;
this.startClientX = e.clientX; this.startClientX = e.clientX;
@@ -756,8 +763,11 @@ const DraggableGrid =
handlePointerMove(e: PointerEvent<HTMLDivElement>): boolean { handlePointerMove(e: PointerEvent<HTMLDivElement>): boolean {
if (this.isCapturedPointer(e)) { if (this.isCapturedPointer(e)) {
if (!this.dragStarted && this.dragThresholdExceeded(e)) { if (this.dragThresholdExceeded(e)) {
this.startDrag(); if (!this.dragStarted) {
this.startDrag();
}
this.dragThresholdWasExceeded = true;
} }
if (this.dragStarted) { if (this.dragStarted) {
this.lastX = e.clientX; this.lastX = e.clientX;
@@ -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<ColorListDropdownButtonProps> = (props: ColorListDropdownButtonProps) => {
let { colorList, colorKey, onColorChange, dropdownAlignment } = props;
if (!colorList) colorList = getIconColorSelections();
const theme = useTheme();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
let selectedColor = getIconColor(colorKey);
if (selectedColor === undefined) {
selectedColor = theme.palette.text.secondary;
}
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleColorSelect = (selection: IconColorSelect) => {
handleClose();
onColorChange(selection);
};
return (
<div>
<ButtonBase
onClick={handleClick}
style={{
height: 48, padding: 12, borderRadius: 8, margin: 4
}}
>
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center", gap: 0 }} >
{props.pluginType ? (
<div style={{ margin: 4 }}>
<PluginIcon
pluginType={props.pluginType}
color={selectedColor}
size={24}
/>
</div>
) : (
<div style={{
width: 24, height: 24,
background: selectedColor,
borderRadius: 6,
borderColor: theme.palette.text.secondary,
borderWidth: 1,
borderStyle: "solid"
}} />
)}
<ArrowDropDownIcon />
</div>
</ButtonBase>
<Menu
anchorEl={anchorEl}
open={anchorEl !== null}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left')
}}
transformOrigin={{
vertical: 'top',
horizontal: (dropdownAlignment === DropdownAlignment.SW ? 'right' : 'left'),
}}
>
<div style={{ display: "flex", flexFlow: "row nowrap", columnGap: 8, padding: 8 }}>
<MenuItem key={"default"} onClick={() => 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
}}
></MenuItem>
<div style={{ display: "flex", flexFlow: "row wrap", width: 56 * 4 + 2 }}>
{colorList.map((color) => {
let selected = color.key === selectedColor;
return (
<MenuItem key={color.key} onClick={() => 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
}}
></MenuItem>
)
}
)}
</div>
</div>
</Menu>
</div>
);
};
export default IconColorDropdownButton;
+73
View File
@@ -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<string | undefined>(undefined);
const [selectedKey, setSelectedKey] = useState<string>("");
return (
<>
<div style={{ display: "flex", flexFlow: "row wrap", rowGap: 8, columnGap: 8, padding: 16, background: "#282828" }}>
{
getIconColorSelections().map((color) => {
return (
<div key={color.key} style={{ width: 48, height: 48, background: color.value, borderRadius: 4,opacity: 0.8 }}
onClick={() => {
setSelectedColor(color.value );
}}
/>
);
})
}
</div>
<div style={{ display: "flex", flexFlow: "row wrap", rowGap: 8, columnGap: 8, padding: 16, background: "#E8E8E8" }}>
{
getIconColorSelections(false).map((color) => {
return (
<div key={color.key} style={{ width: 48, height: 48, background: color.value, borderRadius: 4,opacity: 0.8 }}
onClick={() => { setSelectedColor(color.value ); }}
/>
);
})
}
</div>
<div>
<IconColorDropdownButton
colorKey={selectedKey}
onColorChange={(selection) =>
{
setSelectedColor(selection.value);
setSelectedKey(selection.key);
}
}
dropdownAlignment={DropdownAlignment.SE} />
</div>
<div style={{ display: "flex", flexFlow: "row wrap", rowGap: 8, columnGap: 8, padding: 16 }}>
{
Object.values(PluginType).map((pluginType) => (
<div key={pluginType}>
<PluginIcon pluginType={pluginType} size={32} opacity={0.8} color={selectedColor} />
</div>
))
}
</div>
<div style={{ display: "flex", flexFlow: "row wrap", rowGap: 8, columnGap: 8, padding: 16, background: "#E8E8E8" }}>
{
Object.values(PluginType).map((pluginType) => (
<div key={pluginType}>
<PluginIcon pluginType={pluginType} size={32} opacity={0.8} color={selectedColor} />
</div>
))
}
</div>
</>
);
}
export default IconTest;
@@ -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<ImportPresetFromBankDialogProps, ImportPresetFromBankDialogState> {
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 (
<DialogEx tag="importPreset" open={open} fullWidth maxWidth="xs" onClose={handleClose} aria-labelledby="Rename-dialog-title"
fullScreen={this.state.fullScreen}
style={{ userSelect: "none" }}
onEnterKey={() => { }}
>
<div style={{ display: "flex", flexFlow: "column nowrap", height: this.state.fullScreen ? "100vh" : "80vh" }}>
{!this.state.fullScreen ? (
<>
<DialogTitle style={{ paddingTop: 8, paddingBottom: 0 }}>
<Toolbar style={{ padding: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
Import Presets from Bank
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent style={{ flex: "0 0 auto", paddingTop: 8, paddingBottom: 8, }}>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 0 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{ marginBottom: 8 }}
onChange={(e) => { this.handleBankChanged(e.target.value as number); }}>
{this.state.banks.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
</DialogContent>
</>
) : (
<div style={{ display: "flex", flexFlow: "row nowrap", alignItems: "top", flex: "0 0 auto", paddingTop: 8, paddingBottom: 8, paddingLeft: 16, paddingRight: 16 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<div style={{ marginLeft: 24, display: "flex", flex: "0 0 auto", flexFlow: "column nowrap", justifyContent: "start" }}>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 0 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{ marginBottom: 8 }}
onChange={(e) => { this.handleBankChanged(e.target.value as number); }}>
{this.state.banks.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
</div>
</div>
)}
<Divider />
<List sx={{ flex: "1 1 auto", width: '100%', bgColor: 'background.paper', overflowX: 'auto' }}>
{this.state.bankPresets?.map((presetEntry) => {
const labelId = `checkbox-list-label-${presetEntry.instanceId}`;
return (
<ListItem key={presetEntry.instanceId}
onClick={() => {
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] });
}}>
<ListItemIcon style={{ marginLeft: 16 }}>
<Checkbox
edge="start"
checked={this.state.selectedPresets.indexOf(presetEntry.instanceId) !== -1}
tabIndex={-1}
disableRipple
inputProps={{ 'aria-labelledby': labelId }}
/>
</ListItemIcon>
<ListItemText id={labelId} primary={presetEntry.name} />
</ListItem>
);
})}
</List>
<Divider />
<DialogActions style={{ flex: "0 0 auto" }}>
<Button onClick={handleClose} variant="dialogSecondary" >
Cancel
</Button>
<Button
onClick={() => { this.handleOk(); }} variant="dialogPrimary"
disabled={this.state.selectedPresets.length === 0}
style={{ opacity: this.state.selectedPresets.length === 0 ? 0.5 : 1.0 }}
>
OK
</Button>
</DialogActions>
</div>
</DialogEx >
);
}
}
+2
View File
@@ -223,6 +223,7 @@ export class UiFrequencyPlot {
this.xLeft = input.xLeft; this.xLeft = input.xLeft;
this.xRight = input.xRight; this.xRight = input.xRight;
this.xLog = input.xLog; this.xLog = input.xLog;
this.yDb = input.yDb;
this.yTop = input.yTop; this.yTop = input.yTop;
this.yBottom = input.yBottom; this.yBottom = input.yBottom;
this.width = input.width; this.width = input.width;
@@ -242,6 +243,7 @@ export class UiFrequencyPlot {
xLeft: number = -1; xLeft: number = -1;
xRight: number = -1; xRight: number = -1;
xLog: boolean = true; xLog: boolean = true;
yDb: boolean = true;
yTop: number = -1; yTop: number = -1;
yBottom: number = -1; yBottom: number = -1;
width: number = -1; width: number = -1;
+49 -19
View File
@@ -24,7 +24,7 @@ import { withStyles } from "tss-react/mui";
import IconButtonEx from './IconButtonEx'; import IconButtonEx from './IconButtonEx';
import ButtonEx from './ButtonEx'; import ButtonEx from './ButtonEx';
import ButtonBase from '@mui/material/ButtonBase'; import ButtonBase from '@mui/material/ButtonBase';
import RenameDialog from './RenameDialog'; import PluginIcon, { getIconColor } from './PluginIcon';
import ToolTipEx from './ToolTipEx'; import ToolTipEx from './ToolTipEx';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
@@ -64,9 +64,11 @@ import PipedalUiIcon from './svg/pp_ui.svg?react';
import SnapshotDialog from './SnapshotDialog'; import SnapshotDialog from './SnapshotDialog';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
import { setDefaultModGuiPreference } from './ModGuiHost'; import { setDefaultModGuiPreference } from './ModGuiHost';
import PluginNameDialog from './PluginNameDialog';
const SPLIT_CONTROLBAR_THRESHHOLD = 750; const SPLIT_CONTROLBAR_THRESHHOLD = 750;
const SHOW_ICON_THRESHHOLD = 475;
const DISPLAY_AUTHOR_THRESHHOLD = 750; const DISPLAY_AUTHOR_THRESHHOLD = 750;
const DISPLAY_AUTHOR_SPLIT_THRESHOLD = 500; const DISPLAY_AUTHOR_SPLIT_THRESHOLD = 500;
const HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK = 500; const HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK = 500;
@@ -128,6 +130,7 @@ interface MainState {
splitControlBar: boolean; splitControlBar: boolean;
displayAuthor: boolean; displayAuthor: boolean;
horizontalScrollLayout: boolean; horizontalScrollLayout: boolean;
canDisplayPluginIcon: boolean;
showMidiBindingsDialog: boolean; showMidiBindingsDialog: boolean;
screenHeight: number; screenHeight: number;
displayNameDialogOpen: boolean; displayNameDialogOpen: boolean;
@@ -147,6 +150,9 @@ export const MainPage =
getSplitToolbar() { getSplitToolbar() {
return this.windowSize.width < SPLIT_CONTROLBAR_THRESHHOLD && this.windowSize.height >= HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK; return this.windowSize.width < SPLIT_CONTROLBAR_THRESHHOLD && this.windowSize.height >= HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK;
} }
getShowPluginIcon() {
return this.windowSize.width > SHOW_ICON_THRESHHOLD;
}
getDisplayAuthor() { getDisplayAuthor() {
if (this.getSplitToolbar()) { if (this.getSplitToolbar()) {
return this.windowSize.width >= DISPLAY_AUTHOR_SPLIT_THRESHOLD; return this.windowSize.width >= DISPLAY_AUTHOR_SPLIT_THRESHOLD;
@@ -173,6 +179,7 @@ export const MainPage =
addMenuAnchorEl: null, addMenuAnchorEl: null,
splitControlBar: this.getSplitToolbar(), splitControlBar: this.getSplitToolbar(),
displayAuthor: this.getDisplayAuthor(), displayAuthor: this.getDisplayAuthor(),
canDisplayPluginIcon: this.getShowPluginIcon(),
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK, horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
showMidiBindingsDialog: false, showMidiBindingsDialog: false,
screenHeight: this.windowSize.height, screenHeight: this.windowSize.height,
@@ -304,6 +311,7 @@ export const MainPage =
this.setState({ this.setState({
splitControlBar: this.getSplitToolbar(), splitControlBar: this.getSplitToolbar(),
displayAuthor: this.getDisplayAuthor(), displayAuthor: this.getDisplayAuthor(),
canDisplayPluginIcon: this.getShowPluginIcon(),
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK, horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
screenHeight: this.windowSize.height screenHeight: this.windowSize.height
}); });
@@ -415,6 +423,7 @@ export const MainPage =
return pedalboardItem.uri; return pedalboardItem.uri;
} }
titleBar(pedalboardItem: PedalboardItem | null, canShowModUi: boolean): React.ReactNode { titleBar(pedalboardItem: PedalboardItem | null, canShowModUi: boolean): React.ReactNode {
let uiPlugin = pedalboardItem ? this.model.getUiPlugin(pedalboardItem?.uri) : null;
let title = ""; let title = "";
let author = ""; let author = "";
let infoPluginUri = ""; let infoPluginUri = "";
@@ -489,21 +498,45 @@ export const MainPage =
this.handleEditPluginDisplayName(); this.handleEditPluginDisplayName();
}} }}
> >
<div style={{ flex: "0 1 auto" }}> <div style={{ flex: "0 1 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center", overflow: "hidden" }}>
<span> {this.state.canDisplayPluginIcon && uiPlugin && (
<span className={classes.title}>{title}</span> <div style={{ flex: "0 0 auto", marginRight: 8 }}>
{this.state.displayAuthor && ( <PluginIcon
<span className={classes.author}>{author}</span> pluginType={uiPlugin.plugin_type}
)} color={getIconColor(pedalboardItem?.iconColor ?? "")}
</span> size={24}
/>
</div>
)}
<div style={{ flex: "0 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
<span>
<span className={classes.title}>{title}</span>
{this.state.displayAuthor && (
<span className={classes.author}>{author}</span>
)}
</span>
</div>
</div> </div>
</ButtonBase>) </ButtonBase>)
: ( : (
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}> <div style={{ flex: "0 1 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center", overflow: "hidden" }}>
<span className={classes.title}>{title}</span> {this.state.canDisplayPluginIcon && uiPlugin && (
{this.state.displayAuthor && ( <div style={{ flex: "0 0 auto", marginRight: 8 }}>
<span className={classes.author}>{author}</span> <PluginIcon
pluginType={uiPlugin.plugin_type}
color={getIconColor(pedalboardItem?.iconColor ?? "")}
size={24}
/>
</div>
)} )}
<div style={{ flex: "0 1 auto", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
<span>
<span className={classes.title}>{title}</span>
{this.state.displayAuthor && (
<span className={classes.author}>{author}</span>
)}
</span>
</div>
</div> </div>
)} )}
@@ -770,17 +803,14 @@ export const MainPage =
<SnapshotDialog open={this.state.snapshotDialogOpen} onOk={() => this.onSnapshotDialogOk()} /> <SnapshotDialog open={this.state.snapshotDialogOpen} onOk={() => this.onSnapshotDialogOk()} />
)} )}
{(this.state.displayNameDialogOpen) && ( {(this.state.displayNameDialogOpen) && (
<RenameDialog open={this.state.displayNameDialogOpen} <PluginNameDialog open={this.state.displayNameDialogOpen}
pedalboardItem={pedalboardItem}
allowEmpty={true} allowEmpty={true}
defaultName={pedalboardItem?.title ?? ""}
title="Plugin Display Name"
acceptActionName="OK"
onClose={() => this.setState({ displayNameDialogOpen: false })} onClose={() => this.setState({ displayNameDialogOpen: false })}
onOk={(newName: string) => { onApply={(newName: string, color: string) => {
if (pedalboardItem) { if (pedalboardItem) {
this.model.setPedalboardItemTitle(pedalboardItem.instanceId, newName); this.model.setPedalboardItemTitle(pedalboardItem.instanceId, newName, color);
} }
this.setState({ displayNameDialogOpen: false });
}} }}
/> />
)} )}
+2
View File
@@ -80,6 +80,7 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
this.lilvPresetUri = input.lilvPresetUri; this.lilvPresetUri = input.lilvPresetUri;
this.pathProperties = input.pathProperties; this.pathProperties = input.pathProperties;
this.useModUi = input.useModUi ?? false; this.useModUi = input.useModUi ?? false;
this.iconColor = input.iconColor??"";
return this; return this;
} }
@@ -215,6 +216,7 @@ export class PedalboardItem implements Deserializable<PedalboardItem> {
lilvPresetUri: string = ""; lilvPresetUri: string = "";
pathProperties: {[Name: string]: string} = {}; pathProperties: {[Name: string]: string} = {};
useModUi: boolean = false; // true if this item should use the mod-ui. useModUi: boolean = false; // true if this item should use the mod-ui.
iconColor: string = "";
}; };
export class SnapshotValue { export class SnapshotValue {
File diff suppressed because it is too large Load Diff
+269 -154
View File
@@ -19,16 +19,17 @@
import { Theme } from '@mui/material/styles'; 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 SaveIconOutline from '@mui/icons-material/Save';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
import {createStyles} from './WithStyles'; import { createStyles } from './WithStyles';
import { withStyles } from "tss-react/mui"; import { withStyles } from "tss-react/mui";
import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel';
import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButtonEx from './IconButtonEx'; import IconButtonEx from './IconButtonEx';
import AppBar from '@mui/material/AppBar';
import SnapshotPanel from './SnapshotPanel'; import SnapshotPanel from './SnapshotPanel';
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
@@ -41,13 +42,17 @@ import { BankIndex } from './Banks';
import SnapshotEditor from './SnapshotEditor'; import SnapshotEditor from './SnapshotEditor';
import { Snapshot } from './Pedalboard'; import { Snapshot } from './Pedalboard';
import JackStatusView from './JackStatusView'; import JackStatusView from './JackStatusView';
import {IDialogStackable, popDialogStack, pushDialogStack} from './DialogStack'; import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack';
import { css } from '@emotion/react'; import { css } from '@emotion/react';
const selectColor = isDarkMode() ? "#888" : "#FFFFFF"; const selectColor = isDarkMode() ? "#888" : "#FFFFFF";
const styles = (theme: Theme) => createStyles({ const styles = (theme: Theme) => createStyles({
listIcon: css({
width: 24, height: 24,
opacity: 0.6, fill: theme.palette.text.primary
}),
frame: css({ frame: css({
position: "absolute", display: "flex", flexDirection: "column", flexWrap: "nowrap", position: "absolute", display: "flex", flexDirection: "column", flexWrap: "nowrap",
justifyContent: "flex-start", left: "0px", top: "0px", bottom: "0px", right: "0px", overflow: "hidden" justifyContent: "flex-start", left: "0px", top: "0px", bottom: "0px", right: "0px", overflow: "hidden"
@@ -75,7 +80,8 @@ interface PerformanceViewProps extends WithStyles<typeof styles> {
} }
interface PerformanceViewState { interface PerformanceViewState {
wrapSelects: boolean wrapSelects: boolean;
largeAppBar: boolean;
presets: PresetIndex; presets: PresetIndex;
banks: BankIndex; banks: BankIndex;
showSnapshotEditor: boolean; showSnapshotEditor: boolean;
@@ -86,7 +92,7 @@ interface PerformanceViewState {
export const PerformanceView = export const PerformanceView =
withStyles( withStyles(
class extends ResizeResponsiveComponent<PerformanceViewProps, PerformanceViewState> implements IDialogStackable { class extends ResizeResponsiveComponent<PerformanceViewProps, PerformanceViewState> implements IDialogStackable {
model: PiPedalModel; model: PiPedalModel;
@@ -100,6 +106,7 @@ export const PerformanceView =
presets: this.model.presets.get(), presets: this.model.presets.get(),
banks: this.model.banks.get(), banks: this.model.banks.get(),
wrapSelects: false, wrapSelects: false,
largeAppBar: this.updateAppBarZoom(),
showSnapshotEditor: false, showSnapshotEditor: false,
showStatusMonitor: this.model.showStatusMonitor.get(), showStatusMonitor: this.model.showStatusMonitor.get(),
snapshotEditorIndex: 0, snapshotEditorIndex: 0,
@@ -112,6 +119,9 @@ export const PerformanceView =
} }
updateAppBarZoom() {
return this.windowSize.width > 700;
}
showStatusMonitorHandler() { showStatusMonitorHandler() {
this.setState({ this.setState({
@@ -119,7 +129,7 @@ export const PerformanceView =
}); });
} }
getTag() { return "performView"} getTag() { return "performView" }
isOpen() { return this.props.open } isOpen() { return this.props.open }
onDialogStackClose() { onDialogStackClose() {
this.props.onClose(); this.props.onClose();
@@ -128,15 +138,13 @@ export const PerformanceView =
private hasHooks: boolean = false; private hasHooks: boolean = false;
updateHooks() : void { updateHooks(): void {
let wantHooks = this.mounted && this.props.open; let wantHooks = this.mounted && this.props.open;
if (wantHooks !== this.hasHooks) if (wantHooks !== this.hasHooks) {
{
this.hasHooks = wantHooks; this.hasHooks = wantHooks;
if (this.hasHooks) if (this.hasHooks) {
{
pushDialogStack(this); pushDialogStack(this);
} else { } else {
@@ -144,9 +152,12 @@ export const PerformanceView =
} }
} }
} }
onWindowSizeChanged(width: number, height: number): void { onWindowSizeChanged(width: number, height: number): void {
this.setState({ wrapSelects: width < 700 }); this.setState({
wrapSelects: width < 700,
largeAppBar: this.updateAppBarZoom(),
});
} }
onPresetsChanged(newValue: PresetIndex) { onPresetsChanged(newValue: PresetIndex) {
@@ -156,7 +167,7 @@ export const PerformanceView =
this.setState({ banks: this.model.banks.get() }) this.setState({ banks: this.model.banks.get() })
} }
onPresetChangedChanged(newValue: boolean) { onPresetChangedChanged(newValue: boolean) {
this.setState({ presetModified:newValue}); this.setState({ presetModified: newValue });
} }
private mounted: boolean = false; private mounted: boolean = false;
@@ -216,84 +227,145 @@ export const PerformanceView =
}); });
return true; 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. // 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. // but we don't actually have that in our state.
} }
handlePreviousBank() handlePreviousBank() {
{
this.model.previousBank(); this.model.previousBank();
} }
handleNextBank() handleNextBank() {
{
this.model.nextBank(); this.model.nextBank();
} }
handlePreviousPreset() handlePreviousPreset() {
{
this.model.previousPreset(); this.model.previousPreset();
} }
handleNextPreset() handleNextPreset() {
{
this.model.nextPreset(); this.model.nextPreset();
} }
render() { render() {
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
let wrapSelects = this.state.wrapSelects;
let presets = this.state.presets; let presets = this.state.presets;
let banks = this.state.banks; let banks = this.state.banks;
let appBarIconSize: "large" | undefined = this.state.largeAppBar ? undefined : "large";
return ( return (
<div className={classes.frame} style={{ overflow: "clip", height: "100%" }} > <div className={classes.frame} style={{ overflow: "clip", height: "100%" }} >
<div className={classes.frame} style={{ overflow: "clip", height: "100%" }} > <div className={classes.frame} style={{ overflow: "clip", height: "100%" }} >
<AppBar id="select-plugin-dialog-title" <div id="select-plugin-dialog-title"
style={{ style={{
position: "static", flex: "0 0 auto", height: wrapSelects ? 108 : 58, position: "static", flex: "0 0 auto", height: undefined,
display: this.state.showSnapshotEditor ? "none" : undefined display: this.state.showSnapshotEditor ? "none" : undefined,
background: "transparent"
}} }}
> >
<div style={{ {this.state.largeAppBar ? (
display: "flex", flexFlow: "row nowrap", alignContent: "center",
paddingTop: 3, paddingBottom: 3, paddingRight: 8, paddingLeft: 8,
alignItems: "start"
}}>
<IconButtonEx tooltip="Back" aria-label="menu" color="inherit"
onClick={() => { this.props.onClose(); }}
style={{
position: "relative",top: 3,
flex: "0 0 auto" }} >
<ArrowBackIcon />
</IconButtonEx>
<div style={{ <div style={{
flex: "1 1 1px", display: "flex", display: "flex", flexFlow: "row nowrap", alignContent: "center",
flexFlow: wrapSelects ? "column nowrap" : "row nowrap", paddingTop: 3, paddingBottom: 3, paddingRight: 8, paddingLeft: 8,
alignItems: wrapSelects ? "stretch" : undefined, alignItems: "center", justifyContent: "space-between"
justifyContent: wrapSelects ? undefined : "space-evenly",
marginLeft: 8,
gap: 8
}}> }}>
{/********* BANKS *******************/} <IconButtonEx tooltip="Back" aria-label="menu" color="inherit"
<div style={{ flex: "1 1 1px", display: "flex", flexFlow: "row nowrap", maxWidth: 500 }}> onClick={() => { this.props.onClose(); }}
<IconButtonEx tooltip="Previous bank" style={{
aria-label="previous-bank" position: "relative", top: 3,
onClick={() => { this.handlePreviousBank(); }} flex: "0 0 auto"
size="medium" }} >
color="inherit" <ArrowBackIcon fontSize="large" />
style={{ </IconButtonEx>
borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4
}}
>
<ArrowBackIosIcon style={{ opacity: 0.75 }} />
</IconButtonEx>
{/********* PRESETS *******************/}
<div style={{
flex: "2 2 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center", marginLeft: 24
}}>
<IconButtonEx tooltip="Previous preset"
aria-label="previous-preset"
onClick={() => { this.handlePreviousPreset(); }}
color="inherit"
style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }}
>
<ArrowBackIosIcon style={{ opacity: 0.75 }} fontSize={appBarIconSize} />
</IconButtonEx>
<Select variant="standard" <Select variant="standard"
className={classes.select} className={classes.select}
style={{ flex: "1 1 1px", width: "100%", position: "relative", top: 0, color: "#FFFFFF" }} disabled={false} style={{
flex: "1 1 1px", width: "100%", color: "#FFFFFF",
fontSize: (this.state.largeAppBar ? "2.0rem" : undefined),
}} disabled={false}
displayEmpty
onClose={(e) => this.handlePresetSelectClose(e)}
value={presets.selectedInstanceId === 0 ? '' : presets.selectedInstanceId}
inputProps={{
classes: { icon: classes.select_icon },
'aria-label': "Select preset"
}
}
>
{
presets.presets.map((preset) => {
let name = preset.name;
if (this.state.presets.selectedInstanceId === preset.instanceId && this.state.presetModified) {
name += "*";
}
return (
<MenuItem key={preset.instanceId} value={preset.instanceId} >
{name}
</MenuItem>
);
})
}
</Select>
<IconButtonEx tooltip="Next preset"
aria-label="next-preset"
onClick={() => { this.handleNextPreset(); }}
color="inherit"
style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }}
>
<ArrowForwardIosIcon style={{ opacity: 0.75 }} fontSize={appBarIconSize} />
</IconButtonEx>
<IconButtonEx tooltip="Save preset"
aria-label="save-preset"
onClick={() => { this.model.saveCurrentPreset(); }}
size="medium"
color="inherit"
style={{ flexShrink: 0, visibility: this.state.presetModified ? "visible" : "hidden", marginRight: 16 }}
>
<SaveIconOutline style={{ opacity: 0.75 }} color="inherit" fontSize={appBarIconSize} />
</IconButtonEx>
</div>
{/********* BANKS *******************/}
<div style={{
flex: "1 1 auto", display: "flex", flexFlow: "row nowrap", maxWidth: 500, marginRight: 16
}}>
<Select variant="standard"
className={classes.select}
renderValue={(selected) => {
let entry = banks.entries.find(e => e.instanceId === selected);
return <div style={{
display: "flex", flexFlow: "row nowrap", gap: 4,
alignItems: "center", justifyContent: "stretch",
}}>
<BankIcon className={classes.listIcon} style={{ flex: "0 0 auto", width: 20, height: 20 }} />
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}
variant="body2"
>
{entry ? entry.name : "-"}
</Typography>
</div>;
}}
style={{
flex: "1 1 1px", width: "100%", position: "relative", top: 0, color: "#FFFFFF",
fontSize: "1.0rem",
}} disabled={false}
displayEmpty displayEmpty
onClose={(e) => this.handleBankSelectClose(e)} onClose={(e) => this.handleBankSelectClose(e)}
value={banks.selectedBank === 0 ? undefined : banks.selectedBank} value={banks.selectedBank === 0 ? undefined : banks.selectedBank}
@@ -312,98 +384,141 @@ export const PerformanceView =
}) })
} }
</Select> </Select>
<IconButtonEx tooltip="Next bank"
aria-label="next-bank"
onClick={() => { this.handleNextBank(); }}
color="inherit"
style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }}
>
<ArrowForwardIosIcon style={{ opacity: 0.75 }} />
</IconButtonEx>
{/** spacer */} {/** spacer */}
<IconButtonEx
tooltip="Next bank"
aria-label="next-bank"
onClick={() => { this.handleNextBank(); }}
size="medium"
color="inherit"
style={{visibility: "hidden"}}
>
<ArrowForwardIosIcon style={{ opacity: 0.75 }} />
</IconButtonEx>
</div> </div>
{/********* PRESETS *******************/}
<div style={{ flex: "1 1 1px", display: "flex", flexFlow: "row nowrap", maxWidth: 500 }}>
<IconButtonEx tooltip="Previous preset"
aria-label="previous-preset"
onClick={() => { this.handlePreviousPreset(); }}
color="inherit"
style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }}
>
<ArrowBackIosIcon style={{ opacity: 0.75 }} />
</IconButtonEx>
<Select variant="standard"
className={classes.select}
style={{
flex: "1 1 1px", width: "100%", color: "#FFFFFF"
}} disabled={false}
displayEmpty
onClose={(e) => this.handlePresetSelectClose(e)}
value={presets.selectedInstanceId === 0 ? '' : presets.selectedInstanceId}
inputProps={{
classes: { icon: classes.select_icon },
'aria-label': "Select preset"
}
}
>
{
presets.presets.map((preset) => {
let name = preset.name;
if (this.state.presets.selectedInstanceId === preset.instanceId && this.state.presetModified)
{
name += "*";
}
return (
<MenuItem key={preset.instanceId} value={preset.instanceId} >
{name}
</MenuItem>
);
})
}
</Select>
<IconButtonEx tooltip="Next preset"
aria-label="next-preset"
onClick={() => { this.handleNextPreset(); }}
color="inherit"
style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }}
>
<ArrowForwardIosIcon style={{ opacity: 0.75 }} />
</IconButtonEx>
<IconButtonEx tooltip="Save preset"
aria-label="save-preset"
onClick={() => { this.model.saveCurrentPreset(); }}
size="medium"
color="inherit"
style={{flexShrink: 0,visibility: this.state.presetModified? "visible": "hidden"}}
>
<SaveIconOutline style={{ opacity: 0.75 }} color="inherit" />
</IconButtonEx>
</div>
</div> </div>
</div> ) : (
</AppBar >
<div style={{ flex: "1 0 auto", display: "flex", marginTop: 16,marginBottom: 20 }}> <div style={{
display: "grid",
gridTemplateColumns: "min-content min-content auto min-content min-content",
}}>
<IconButtonEx tooltip="Back" aria-label="menu" color="inherit"
onClick={() => { this.props.onClose(); }}
style={{
position: "relative", top: 3,
flex: "0 0 auto"
}} >
<ArrowBackIcon fontSize="medium" />
</IconButtonEx>
<div/>
{/********* BANKS *******************/}
<Select variant="standard"
className={classes.select}
renderValue={(selected) => {
let entry = banks.entries.find(e => e.instanceId === selected);
return <div style={{
display: "flex", flexFlow: "row nowrap", gap: 4,
alignItems: "center", justifyContent: "stretch",
}}>
<BankIcon className={classes.listIcon} style={{ flex: "0 0 auto", width: 20, height: 20 }} />
<Typography noWrap style={{ flex: "1 1 auto", textAlign: "left" }}
variant="body2"
>
{entry ? entry.name : "-"}
</Typography>
</div>;
}}
style={{
flex: "1 1 1px", width: "100%", position: "relative", top: 0, color: "#FFFFFF",
fontSize: "1.0rem",
}} disabled={false}
displayEmpty
onClose={(e) => this.handleBankSelectClose(e)}
value={banks.selectedBank === 0 ? undefined : banks.selectedBank}
inputProps={{
classes: { icon: classes.select_icon },
'aria-label': "Select preset"
}}
>
{
banks.entries.map((entry) => {
return (
<MenuItem key={entry.instanceId} value={entry.instanceId} >
{entry.name}
</MenuItem>
);
})
}
</Select>
<div/>
<div/>
{/**** 2nd row ****/}
<div/>
{/********* PRESETS *******************/}
<IconButtonEx tooltip="Previous preset"
aria-label="previous-preset"
onClick={() => { this.handlePreviousPreset(); }}
color="inherit"
style={{ borderTopRightRadius: "3px", borderBottomRightRadius: "3px", marginRight: 4 }}
>
<ArrowBackIosIcon style={{ opacity: 0.75 }} fontSize={appBarIconSize} />
</IconButtonEx>
<Select variant="standard"
className={classes.select}
style={{
flex: "1 1 1px", width: "100%", color: "#FFFFFF",
fontSize: (this.state.largeAppBar ? "2.0rem" : undefined),
}} disabled={false}
displayEmpty
onClose={(e) => this.handlePresetSelectClose(e)}
value={presets.selectedInstanceId === 0 ? '' : presets.selectedInstanceId}
inputProps={{
classes: { icon: classes.select_icon },
'aria-label': "Select preset"
}
}
>
{
presets.presets.map((preset) => {
let name = preset.name;
if (this.state.presets.selectedInstanceId === preset.instanceId && this.state.presetModified) {
name += "*";
}
return (
<MenuItem key={preset.instanceId} value={preset.instanceId} >
{name}
</MenuItem>
);
})
}
</Select>
<IconButtonEx tooltip="Next preset"
aria-label="next-preset"
onClick={() => { this.handleNextPreset(); }}
color="inherit"
style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }}
>
<ArrowForwardIosIcon style={{ opacity: 0.75 }} fontSize={appBarIconSize} />
</IconButtonEx>
<IconButtonEx tooltip="Save preset"
aria-label="save-preset"
onClick={() => { this.model.saveCurrentPreset(); }}
size="medium"
color="inherit"
style={{ flexShrink: 0, visibility: this.state.presetModified ? "visible" : "hidden" }}
>
<SaveIconOutline style={{ opacity: 0.75 }} color="inherit" fontSize={appBarIconSize} />
</IconButtonEx>
</div>
)}
</div >
<div style={{ flex: "1 0 auto", display: "flex", marginTop: 16, marginBottom: 20 }}>
<SnapshotPanel onEdit={(index) => { return this.handleOnEdit(index); }} /> <SnapshotPanel onEdit={(index) => { return this.handleOnEdit(index); }} />
</div> </div>
@@ -413,15 +528,15 @@ export const PerformanceView =
)} )}
</div> </div>
{this.state.showSnapshotEditor && ( {this.state.showSnapshotEditor && (
<SnapshotEditor snapshotIndex={this.state.snapshotEditorIndex} <SnapshotEditor snapshotIndex={this.state.snapshotEditorIndex}
onClose={() => { onClose={() => {
this.setState({ showSnapshotEditor: false }); this.setState({ showSnapshotEditor: false });
}} }}
onOk={(index,name,color,newSnapshots)=>{ onOk={(index, name, color, newSnapshots) => {
this.setState({ showSnapshotEditor: false}); this.setState({ showSnapshotEditor: false });
this.handleSnapshotEditOk(index,name,color,newSnapshots); this.handleSnapshotEditOk(index, name, color, newSnapshots);
}} }}
/> />
)} )}
</div > </div >
) )
+25 -8
View File
@@ -491,6 +491,9 @@ export class PiPedalModel //implements PiPedalModel
webSocket?: PiPedalSocket; webSocket?: PiPedalSocket;
static getInstance(): PiPedalModel {
return PiPedalModelFactory.getInstance();
}
hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false); hasTone3000Auth: ObservableProperty<boolean> = new ObservableProperty<boolean>(false);
canKeepScreenOn: boolean = false; canKeepScreenOn: boolean = false;
@@ -2046,13 +2049,14 @@ export class PiPedalModel //implements PiPedalModel
} }
saveCurrentPresetAs(newName: string, saveAfterInstanceId = -1): Promise<number> { saveCurrentPresetAs(bankInstanceId: number, newName: string, saveAfterInstanceId = -1): Promise<number> {
// default behaviour is to save after the currently selected preset. // default behaviour is to save after the currently selected preset.
if (saveAfterInstanceId === -1) { if (saveAfterInstanceId === -1) {
saveAfterInstanceId = this.presets.get().selectedInstanceId; saveAfterInstanceId = this.presets.get().selectedInstanceId;
} }
let request: any = { let request: any = {
clientId: this.clientId, clientId: this.clientId,
bankInstanceId: bankInstanceId,
name: newName, name: newName,
saveAfterInstanceId: saveAfterInstanceId saveAfterInstanceId: saveAfterInstanceId
@@ -2061,7 +2065,9 @@ export class PiPedalModel //implements PiPedalModel
return nullCast(this.webSocket) return nullCast(this.webSocket)
.request<number>("saveCurrentPresetAs", request) .request<number>("saveCurrentPresetAs", request)
.then((newPresetId) => { .then((newPresetId) => {
this.loadPreset(newPresetId); if (bankInstanceId === this.banks.get().selectedBank) {
this.loadPreset(newPresetId);
}
return newPresetId; return newPresetId;
}); });
} }
@@ -2186,8 +2192,8 @@ export class PiPedalModel //implements PiPedalModel
} }
deletePresetItem(instanceId: number): Promise<number> { deletePresetItems(instanceIds: Set<number>): Promise<number> {
return nullCast(this.webSocket).request<number>("deletePresetItem", instanceId); return nullCast(this.webSocket).request<number>("deletePresetItems", Array.from(instanceIds));
} }
deleteBankItem(instanceId: number): Promise<number> { deleteBankItem(instanceId: number): Promise<number> {
@@ -3376,9 +3382,9 @@ export class PiPedalModel //implements PiPedalModel
this.cancelOnNetworkChanging(); this.cancelOnNetworkChanging();
}, },
30 * 1000); 30 * 1000);
} }
setPedalboardItemTitle(instanceId: number, title: string): void {
setPedalboardItemTitle(instanceId: number, title: string, iconColor: string): void {
let pedalboard = this.pedalboard.get(); let pedalboard = this.pedalboard.get();
if (!pedalboard) { if (!pedalboard) {
throw new PiPedalStateError("Pedalboard not loaded."); throw new PiPedalStateError("Pedalboard not loaded.");
@@ -3386,13 +3392,14 @@ export class PiPedalModel //implements PiPedalModel
let newPedalboard = pedalboard.clone(); let newPedalboard = pedalboard.clone();
this.updateVst3State(newPedalboard); this.updateVst3State(newPedalboard);
let item = newPedalboard.getItem(instanceId); let item = newPedalboard.getItem(instanceId);
if (item.title === title) { if (item.title === title && item.iconColor === iconColor) {
return; return;
} }
item.title = title; item.title = title;
item.iconColor = iconColor;
this.pedalboard.set(newPedalboard); this.pedalboard.set(newPedalboard);
// notify the server. // 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 { setAlsaSequencerConfiguration(alsaSequencerConfiguration: AlsaSequencerConfiguration): void {
this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration); this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration);
@@ -3535,6 +3542,16 @@ export class PiPedalModel //implements PiPedalModel
this.webSocket?.send("setSelectedPedalboardPlugin", { clientId: this.clientId, pluginInstanceId: pluginId }); this.webSocket?.send("setSelectedPedalboardPlugin", { clientId: this.clientId, pluginInstanceId: pluginId });
} }
requestBankPresets(bankInstanceId: number): Promise<PresetIndexEntry[]> {
return nullCast(this.webSocket).request<PresetIndexEntry[]>("requestBankPresets", {bankInstanceId: bankInstanceId});
}
importPresetsFromBank(bankInstanceId: number, presets: number[]): Promise<number> {
return nullCast(this.webSocket).request<number>("importPresetsFromBank", {bankInstanceId: bankInstanceId, presets: presets});
}
copyPresetsToBank(bankInstanceId: number, presets: number[]): Promise<number> {
return nullCast(this.webSocket).request<number>("copyPresetsToBank", {bankInstanceId: bankInstanceId, presets: presets});
}
}; };
let instance: PiPedalModel | undefined = undefined; let instance: PiPedalModel | undefined = undefined;
+16 -14
View File
@@ -307,7 +307,7 @@ const PluginControl =
e.stopPropagation(); e.stopPropagation();
} }
isValidPointer(e: PointerEvent<SVGSVGElement>): boolean { isValidPointer(e: PointerEvent): boolean {
if (e.pointerType === "mouse") { if (e.pointerType === "mouse") {
return e.button === 0; return e.button === 0;
} else if (e.pointerType === "pen") { } else if (e.pointerType === "pen") {
@@ -347,7 +347,7 @@ const PluginControl =
pointerId: number = 0; pointerId: number = 0;
pointerType: string = ""; pointerType: string = "";
isCapturedPointer(e: PointerEvent<SVGSVGElement>): boolean { isCapturedPointer(e: PointerEvent): boolean {
return this.mouseDown return this.mouseDown
&& e.pointerId === this.pointerId && e.pointerId === this.pointerId
&& e.pointerType === this.pointerType; && e.pointerType === this.pointerType;
@@ -361,7 +361,7 @@ const PluginControl =
pointersDown: number = 0; pointersDown: number = 0;
captureElement?: SVGSVGElement = undefined; captureElement?: SVGSVGElement = undefined;
onPointerDown(e: PointerEvent<SVGSVGElement>): void { onPointerDown(e: PointerEvent): void {
if (!this.mouseDown && this.isValidPointer(e)) { if (!this.mouseDown && this.isValidPointer(e)) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -387,7 +387,7 @@ const PluginControl =
this.mouseDown = true; this.mouseDown = true;
if (this.pointersDown === 1) { if (this.pointersDown === 1) {
this.isTap = true; this.isTap = true;
this.tapStartMs = Date.now(); this.tapStartMs = e.timeStamp;
} else { } else {
this.isTap = false; this.isTap = false;
} }
@@ -429,7 +429,7 @@ const PluginControl =
} }
onPointerLostCapture(e: PointerEvent<SVGSVGElement>) { onPointerLostCapture(e: PointerEvent) {
if (this.isCapturedPointer(e)) { if (this.isCapturedPointer(e)) {
if (this.pointersDown !== 0) { if (this.pointersDown !== 0) {
--this.pointersDown; --this.pointersDown;
@@ -442,7 +442,7 @@ const PluginControl =
} }
updateRange(e: PointerEvent<SVGSVGElement>): number { updateRange(e: PointerEvent): number {
let ultraHigh = false; let ultraHigh = false;
let high = false; let high = false;
@@ -486,9 +486,11 @@ const PluginControl =
} }
} }
} }
onPointerTap() { onPointerTap(e: PointerEvent) {
let tapTime = Date.now(); let tapTime = e.timeStamp;
let dT = tapTime - this.lastTapMs; let dT = tapTime - this.lastTapMs;
this.lastTapMs = tapTime; this.lastTapMs = tapTime;
if (dT < 500) { if (dT < 500) {
@@ -497,7 +499,7 @@ const PluginControl =
} }
private tapStartMs: number = 0; private tapStartMs: number = 0;
onPointerUp(e: PointerEvent<SVGSVGElement>) { onPointerUp(e: PointerEvent) {
if (this.isCapturedPointer(e)) { if (this.isCapturedPointer(e)) {
if (this.pointersDown !== 0) { if (this.pointersDown !== 0) {
@@ -514,9 +516,9 @@ const PluginControl =
this.releaseCapture(e); this.releaseCapture(e);
if (this.isTap) { if (this.isTap) {
let ms = Date.now() - this.tapStartMs; let ms = e.timeStamp - this.tapStartMs;
if (ms < 200) { if (ms < 200) {
this.onPointerTap(); this.onPointerTap(e);
} }
} }
// prevent click from firing on other elements // prevent click from firing on other elements
@@ -531,7 +533,7 @@ const PluginControl =
} }
} }
releaseCapture(e: PointerEvent<SVGSVGElement>) { releaseCapture(e: PointerEvent) {
let img = this.imgRef.current; let img = this.imgRef.current;
if (img && img.style) { if (img && img.style) {
@@ -556,7 +558,7 @@ const PluginControl =
clickSlop() { clickSlop() {
return 5; // maybe larger on touch devices. return 5; // maybe larger on touch devices.
} }
onPointerMove(e: PointerEvent<SVGSVGElement>): void { onPointerMove(e: PointerEvent): void {
if (this.isCapturedPointer(e)) { if (this.isCapturedPointer(e)) {
e.preventDefault(); e.preventDefault();
let dRange = this.updateRange(e) let dRange = this.updateRange(e)
@@ -1138,7 +1140,7 @@ const PluginControl =
); );
} }
/* /*
isSamePointer(PointerEvent<SVGSVGElement> e): boolean isSamePointer(PointerEvent e): boolean
{ {
return e.pointerId === this.pointerId return e.pointerId === this.pointerId
&& e.pointerType === this.pointerType; && e.pointerType === this.pointerType;
+1
View File
@@ -477,6 +477,7 @@ const PluginControlView =
return (( return ((
<ToobFrequencyResponseView instanceId={this.props.instanceId} <ToobFrequencyResponseView instanceId={this.props.instanceId}
propertyName={frequencyPlot.patchProperty} propertyName={frequencyPlot.patchProperty}
frequencyPlot={frequencyPlot}
width={frequencyPlot.width} width={frequencyPlot.width}
/> />
)); ));
+103 -6
View File
@@ -23,7 +23,6 @@ import WithStyles from './WithStyles';
import {createStyles} from './WithStyles'; import {createStyles} from './WithStyles';
import { withStyles } from "tss-react/mui"; import { withStyles } from "tss-react/mui";
import { PiPedalModelFactory } from "./PiPedalModel";
import { PluginType } from './Lv2Plugin'; 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 FxTerminalIcon from './svg/fx_terminal.svg?react';
import { isDarkMode } from './DarkMode'; 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<typeof styles> { export interface PluginIconProps extends WithStyles<typeof styles> {
size?: number; size?: number;
opacity?: number; opacity?: number;
color?: string;
offsetY?: number; offsetY?: number;
pluginType: PluginType; pluginType: PluginType;
pluginMissing?: boolean; pluginMissing?: boolean;
} }
export function SelectSvgIcon(plugin_type: PluginType, className: string, size: number = 24, opacity: number = 1.0, missingPlugin: boolean = false) { export function SelectSvgIcon(plugin_type: PluginType, className: string, size: number = 24, opacity: number = 1.0, missingPlugin: boolean = false, color = "") {
let color = "";
if (missingPlugin) { if (missingPlugin) {
color = isDarkMode() ? "#C00000" : "#B00000"; color = isDarkMode() ? "#C00000" : "#B00000";
} }
@@ -264,11 +361,11 @@ export function SelectBaseIcon(plugin_type: PluginType): string {
export function SelectIconUri(plugin_type: PluginType) { export function SelectIconUri(plugin_type: PluginType) {
let icon = SelectBaseIcon(plugin_type); let icon = SelectBaseIcon(plugin_type);
return PiPedalModelFactory.getInstance().svgImgUrl(icon); return PiPedalModel.getInstance().svgImgUrl(icon);
} }
const PluginIcon = withStyles((props: PluginIconProps) => { const PluginIcon = withStyles((props: PluginIconProps) => {
const { pluginType, opacity, pluginMissing } = props; const { pluginType, opacity, pluginMissing,color } = props;
const classes = withStyles.getClasses(props); const classes = withStyles.getClasses(props);
let pluginMissing_: boolean = pluginMissing ?? false; let pluginMissing_: boolean = pluginMissing ?? false;
@@ -277,7 +374,7 @@ const PluginIcon = withStyles((props: PluginIconProps) => {
if (props.size) size = props.size; if (props.size) size = props.size;
let topVal: number = (props.offsetY ?? 0); 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) { if (svgIcon) {
return svgIcon; return svgIcon;
} }
+190
View File
@@ -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<PluginNameDialogProps, PluginNameDialogState> {
refText: React.RefObject<HTMLInputElement | null>;
constructor(props: PluginNameDialogProps) {
super(props);
this.state = {
iconColor: this.props.pedalboardItem?.iconColor ?? ""
};
this.refText = React.createRef<HTMLInputElement>();
}
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<HTMLDivElement>): void => {
// 'keypress' event misbehaves on mobile so we track 'Enter' key via 'keydown' event
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
handleOk();
}
};
return (
<DialogEx tag="nameDialog" open={open} maxWidth="md" onClose={handleClose} aria-labelledby="Rename-dialog-title"
style={{ userSelect: "none" }}
onEnterKey={() => { }}
>
<DialogTitle style={{paddingBottom: 0, paddingTop: 8}}>
<div style={{display: "flex",flexFlow: "row nowrap", alignItems: "center"}} >
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={() => onClose()} aria-label="back"
>
<ArrowBackIcon />
</IconButtonEx>
<Typography noWrap variant="h6" >
Plugin Properties
</Typography>
</div>
</DialogTitle>
<DialogContent >
<div style={{ display: "flex", flexFlow: "column nowrap", alignItems: "start", gap: 8, marginBottom: 8 }}>
<TextField
style={{ flex: "1 1 auto", width: 240 }}
autoFocus={!isTouchUi()}
onKeyDown={handleKeyDown}
variant="standard"
autoComplete="off"
autoCorrect="off"
onChange={(e) => { 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}
/>
<div>
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 16 }}>Icon color</InputLabel>
<IconColorDropdownButton colorKey={this.state.iconColor}
onColorChange={(selection) => {
if (this.props.pedalboardItem) {
this.setState({ iconColor: selection.key });
onApply(this.props.pedalboardItem.title, selection.key);
}
}}
pluginType={uiPlugin.plugin_type}
dropdownAlignment={DropdownAlignment.SE}
/>
</div>
</div>
</DialogContent>
</DialogEx>
);
}
}
+315 -136
View File
@@ -17,34 +17,37 @@
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // 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. // 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 { css } from '@emotion/react';
import {isDarkMode} from './DarkMode'; import { isDarkMode } from './DarkMode';
import IconButtonEx from './IconButtonEx'; import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory, PresetIndexEntry, PresetIndex } from './PiPedalModel';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import ButtonBase from "@mui/material/ButtonBase";
import DialogEx from './DialogEx'; import DialogEx from './DialogEx';
import AppBar from '@mui/material/AppBar'; import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import DraggableGrid, { ScrollDirection } from './DraggableGrid'; import DraggableGrid, { ScrollDirection } from './DraggableGrid';
import CloseIcon from '@mui/icons-material/Close';
import MoreVertIcon from '@mui/icons-material/MoreVert'; import MoreVertIcon from '@mui/icons-material/MoreVert';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText'; import ListItemText from '@mui/material/ListItemText';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import Fade from '@mui/material/Fade'; import Fade from '@mui/material/Fade';
import UploadPresetDialog from './UploadPresetDialog'; 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 ArrowBackIcon from '@mui/icons-material/ArrowBack';
import EditIcon from '@mui/icons-material/Edit';
import RenameDialog from './RenameDialog'; import RenameDialog from './RenameDialog';
import Slide, {SlideProps} from '@mui/material/Slide'; import Slide, { SlideProps } from '@mui/material/Slide';
import {createStyles} from './WithStyles'; import { createStyles } from './WithStyles';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles'; 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 DownloadIcon from './svg/file_download_black_24dp.svg?react';
import UploadIcon from './svg/file_upload_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<typeof styles> { interface PresetDialogProps extends WithStyles<typeof styles> {
show: boolean; show: boolean;
isEditDialog: boolean;
onDialogClose: () => void; onDialogClose: () => void;
}; };
@@ -64,11 +70,16 @@ interface PresetDialogProps extends WithStyles<typeof styles> {
interface PresetDialogState { interface PresetDialogState {
presets: PresetIndex; presets: PresetIndex;
showActionBar: boolean; multiSelect: boolean;
showTouchActionBar: boolean;
selectedItem: number;
currentItem: number;
selectedItems: Set<number>;
renameOpen: boolean; renameOpen: boolean;
importOpen: boolean;
copyToOpen: boolean;
moreMenuAnchorEl: HTMLElement | null; moreMenuAnchorEl: HTMLElement | null;
openUploadPresetDialog: boolean; openUploadPresetDialog: boolean;
@@ -79,7 +90,7 @@ interface PresetDialogState {
const styles = (theme: Theme) => createStyles({ const styles = (theme: Theme) => createStyles({
listIcon: css({ 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({ dialogAppBar: css({
position: 'relative', position: 'relative',
@@ -88,7 +99,8 @@ const styles = (theme: Theme) => createStyles({
dialogActionBar: css({ dialogActionBar: css({
position: 'relative', position: 'relative',
top: 0, left: 0, top: 0, left: 0,
background: "black" background: theme.palette.actionBar.main,
color: theme.palette.actionBar.contrastText
}), }),
dialogTitle: css({ dialogTitle: css({
marginLeft: theme.spacing(2), marginLeft: theme.spacing(2),
@@ -106,7 +118,7 @@ const styles = (theme: Theme) => createStyles({
alignItems: "center", alignItems: "center",
textAlign: "left", textAlign: "left",
justifyContent: "center", justifyContent: "center",
paddingLeft: 8 paddibomngLeft: 8
}), }),
iconFrame: css({ iconFrame: css({
flex: "0 0 auto", flex: "0 0 auto",
@@ -131,7 +143,7 @@ const Transition = React.forwardRef(function Transition(
const PresetDialog = withStyles( const PresetDialog = withStyles(
class extends Component<PresetDialogProps, PresetDialogState> { class extends ResizeResponsiveComponent<PresetDialogProps, PresetDialogState> {
model: PiPedalModel; model: PiPedalModel;
@@ -145,9 +157,13 @@ const PresetDialog = withStyles(
let presets = this.model.presets.get(); let presets = this.model.presets.get();
this.state = { this.state = {
presets: presets, presets: presets,
showActionBar: false, multiSelect: false,
selectedItem: presets.selectedInstanceId, showTouchActionBar: false,
currentItem: presets.selectedInstanceId,
selectedItems: new Set<number>([presets.selectedInstanceId]),
renameOpen: false, renameOpen: false,
importOpen: false,
copyToOpen: false,
moreMenuAnchorEl: null, moreMenuAnchorEl: null,
openUploadPresetDialog: false openUploadPresetDialog: false
@@ -156,12 +172,33 @@ const PresetDialog = withStyles(
} }
onWindowSizeChanged(width: number, height: number) {
super.onWindowSizeChanged(width, height);
}
handleSelectionUpdated(instanceIds: Set<number>) {
this.setState({
multiSelect: instanceIds.size > 1
})
}
setSelections(instanceIds: Set<number>) {
this.setState({
selectedItems: instanceIds,
});
this.handleSelectionUpdated(instanceIds);
}
setSelection(instanceId: number) {
let selectedItems = new Set<number>([instanceId]);
this.setState({
currentItem: instanceId,
selectedItems: selectedItems
});
this.handleSelectionUpdated(selectedItems);
}
selectItemAtIndex(index: number) { selectItemAtIndex(index: number) {
let instanceId = this.state.presets.presets[index].instanceId; let instanceId = this.state.presets.presets[index].instanceId;
this.setState({ selectedItem: instanceId }); this.setSelection(instanceId);
}
isEditMode() {
return this.state.showActionBar || this.props.isEditDialog;
} }
onMoreClick(e: SyntheticEvent): void { onMoreClick(e: SyntheticEvent): void {
@@ -170,7 +207,7 @@ const PresetDialog = withStyles(
handleDownloadPreset() { handleDownloadPreset() {
this.handleMoreClose(); this.handleMoreClose();
this.model.download("downloadPreset", this.state.selectedItem); this.model.download("downloadPreset", this.state.currentItem);
} }
handleUploadPreset() { handleUploadPreset() {
this.handleMoreClose(); 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 (!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 we don't have a valid selection, then use the current preset.
if (this.state.presets.getItem(this.state.selectedItem) == null) { if (this.state.presets.getItem(this.state.currentItem) == null) {
this.setState({ presets: presets, selectedItem: presets.selectedInstanceId }); this.setState({
presets: presets, currentItem: presets.selectedInstanceId,
selectedItems: new Set<number>([presets.selectedInstanceId])
});
} else { } else {
this.setState({ presets: presets }); this.setState({ presets: presets });
} }
@@ -207,8 +247,10 @@ const PresetDialog = withStyles(
this.model.presets.removeOnChangedHandler(this.handlePresetsChanged); this.model.presets.removeOnChangedHandler(this.handlePresetsChanged);
} }
getSelectedIndex() { getSelectedIndex(instanceId?: number): number {
let instanceId = this.isEditMode() ? this.state.selectedItem : this.state.presets.selectedInstanceId; if (instanceId === undefined) {
instanceId = this.state.currentItem;
}
let presets = this.state.presets; let presets = this.state.presets;
for (let i = 0; i < presets.presets.length; ++i) { for (let i = 0; i < presets.presets.length; ++i) {
if (presets.presets[i].instanceId === instanceId) return i; if (presets.presets[i].instanceId === instanceId) return i;
@@ -217,33 +259,59 @@ const PresetDialog = withStyles(
} }
handleDeleteClick() { handleDeleteClick() {
if (!this.state.selectedItem) return; if (this.state.selectedItems.size === 0) {
let selectedItem = this.state.selectedItem; return;
if (selectedItem !== -1) {
this.model.deletePresetItem(selectedItem)
.then((selectedItem: number) => {
this.setState({ selectedItem: selectedItem });
})
.catch((error) => {
this.model.showAlert(error);
});
} }
this.model.deletePresetItems(this.state.selectedItems)
.then((currentItem: number) => {
this.setSelection(currentItem);
})
.catch((error) => {
this.model.showAlert(error);
});
} }
handleDialogClose() { handleDialogClose() {
this.props.onDialogClose(); this.props.onDialogClose();
} }
handleItemClick(instanceId: number): void { handleItemClick(e: React.MouseEvent<HTMLElement>, instanceId: number): void {
if (this.isEditMode()) { e.stopPropagation();
this.setState({ selectedItem: instanceId }); if (e.ctrlKey || this.state.showTouchActionBar) {
let selectedItems = new Set<number>(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<number>();
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 { } else {
this.model.loadPreset(instanceId); this.setSelection(instanceId);
this.props.onDialogClose();
} }
} }
showActionBar(show: boolean): void { showTouchActionBar(show: boolean): void {
this.setState({ showActionBar: show }); this.setState({ showTouchActionBar: show });
} }
@@ -251,27 +319,23 @@ const PresetDialog = withStyles(
mapElement(el: any): React.ReactNode { mapElement(el: any): React.ReactNode {
let presetEntry = el as PresetIndexEntry; let presetEntry = el as PresetIndexEntry;
const classes = withStyles.getClasses(this.props); 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 ( return (
<div key={presetEntry.instanceId} className="itemBackground"> <div key={presetEntry.instanceId} id={"psetdlgItem_" + presetEntry.instanceId} className="itemBackground">
<ListItemButton
<ButtonBase style={{ width: "100%", height: 48 }} selected={selected}
onClick={() => this.handleItemClick(presetEntry.instanceId)} onClick={(e) => this.handleItemClick(e, presetEntry.instanceId)}
style={{ height: 48 }}
> >
<SelectHoverBackground selected={presetEntry.instanceId === selectedItem} showHover={true} /> <ListItemIcon>
<div className={classes.itemFrame}> <img
<div className={classes.iconFrame}> src={isDarkMode() ? "img/ic_presets_white.svg" : "img/ic_presets.svg"}
<img className={classes.itemIcon} alt="" />
src={isDarkMode()? "img/ic_presets_white.svg": "img/ic_presets.svg"} </ListItemIcon>
className={classes.itemIcon} alt="" /> <ListItemText primary={presetEntry.name}
</div> />
<div className={classes.itemLabel}>
<Typography> </ListItemButton>
{presetEntry.name}
</Typography>
</div>
</div>
</ButtonBase>
</div> </div>
); );
@@ -287,30 +351,33 @@ const PresetDialog = withStyles(
moveElement(from: number, to: number): void { moveElement(from: number, to: number): void {
let newPresets = this.state.presets.clone(); let newPresets = this.state.presets.clone();
newPresets.movePreset(from, to); newPresets.movePreset(from, to);
let toInstanceId = newPresets.presets[to].instanceId;
this.setState({ this.setState({
presets: newPresets, presets: newPresets,
selectedItem: newPresets.presets[to].instanceId currentItem: toInstanceId,
selectedItems: new Set<number>([toInstanceId])
}); });
this.updateServerPresets(newPresets); this.updateServerPresets(newPresets);
} }
getSelectedName(): string { 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; if (item) return item.name;
return ""; return "";
} }
handleRenameClick() { handleRenameClick() {
let item = this.state.presets.getItem(this.state.selectedItem); let item = this.state.presets.getItem(this.state.currentItem);
if (item) { if (item) {
this.setState({ renameOpen: true }); this.setState({ renameOpen: true });
} }
} }
handleRenameOk(text: string) { 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) return;
if (item.name !== text) { if (item.name !== text) {
this.model.renamePresetItem(this.state.selectedItem, text) this.model.renamePresetItem(this.state.currentItem, text)
.catch((error) => { .catch((error) => {
this.onError(error); this.onError(error);
}); });
@@ -319,11 +386,14 @@ const PresetDialog = withStyles(
this.setState({ renameOpen: false }); this.setState({ renameOpen: false });
} }
handleCopy() { handleCopy() {
let item = this.state.presets.getItem(this.state.selectedItem); let item = this.state.presets.getItem(this.state.currentItem);
if (!item) return; if (!item) return;
this.model.duplicatePreset(this.state.selectedItem) this.model.duplicatePreset(this.state.currentItem)
.then((newId) => { .then((newId) => {
this.setState({ selectedItem: newId }); this.setState({
currentItem: newId,
selectedItems: new Set<number>([newId])
});
}).catch((error) => { }).catch((error) => {
this.onError(error); this.onError(error);
}); });
@@ -333,81 +403,144 @@ const PresetDialog = withStyles(
this.model?.showAlert(error); this.model?.showAlert(error);
} }
handleCloseActionBar(e: SyntheticEvent) {
e.stopPropagation();
this.setState({
showTouchActionBar: false,
selectedItems: new Set<number>([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() { render() {
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
let actionBarClass = this.props.isEditDialog ? classes.dialogAppBar : classes.dialogActionBar; const showActionBar = this.state.multiSelect || this.state.showTouchActionBar;
let defaultSelectedIndex = this.getSelectedIndex();
return ( return (
<DialogEx tag="preset" fullScreen open={this.props.show} <DialogEx tag="preset" fullScreen open={this.props.show}
onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition} onClose={() => { this.handleDialogClose() }} TransitionComponent={Transition}
style={{userSelect: "none"}} style={{ userSelect: "none" }}
onEnterKey={()=>{}} onEnterKey={() => { }}
> >
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}> <div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", width: "100%", height: "100%", overflow: "hidden" }}>
<div style={{ flex: "0 0 auto" }}> <div style={{ flex: "0 0 auto" }}>
<AppBar className={classes.dialogAppBar} style={{ display: this.isEditMode() ? "none" : "block" }} > <AppBar className={classes.dialogActionBar} style={{ display: showActionBar ? "block" : "none" }}
>
<Toolbar> <Toolbar>
<IconButtonEx tooltip="Back" edge="start" color="inherit" onClick={this.handleDialogClose} aria-label="back" <IconButtonEx tooltip="Back" edge="start" color="inherit" aria-label="back"
disabled={this.isEditMode()} onClick={(e) => { this.handleCloseActionBar(e); }}
> >
<ArrowBackIcon /> <CloseIcon />
</IconButtonEx> </IconButtonEx>
<Typography variant="h6" className={classes.dialogTitle}> <Typography variant="h6" className={classes.dialogTitle} noWrap>
Presets {this.state.selectedItems.size.toString() + " items selected"}
</Typography> </Typography>
<IconButtonEx tooltip="Edit"color="inherit" onClick={(e) => this.showActionBar(true)} > <Button color="inherit"
<EditIcon /> onClick={() => {
this.handleCopyPresetsToBank();
}} >
Copy to
</Button>
<IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => { this.handleDeleteClick(); }} >
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButtonEx> </IconButtonEx>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
<AppBar className={actionBarClass} style={{ display: this.isEditMode() ? "block" : "none" }} <AppBar className={classes.dialogAppBar} style={{ display: showActionBar ? "none" : "block" }}
onClick={(e) => { e.stopPropagation(); e.preventDefault(); }} onClick={(e) => { e.stopPropagation(); e.preventDefault(); }}
> >
<Toolbar> <Toolbar>
{(!this.props.isEditDialog) ? ( <IconButtonEx edge="start" tooltip="Back" color="inherit" onClick={this.handleDialogClose} aria-label="back"
<IconButtonEx tooltip="Close" edge="start" color="inherit" onClick={(e) => this.showActionBar(false)} aria-label="close"> >
<CloseIcon /> <ArrowBackIcon />
</IconButtonEx> </IconButtonEx>
) : ( <Typography variant="h6" className={classes.dialogTitle} noWrap>
<IconButtonEx edge="start" tooltip="Back" color="inherit" onClick={this.handleDialogClose} aria-label="back"
>
<ArrowBackIcon />
</IconButtonEx>
)}
<Typography variant="h6" className={classes.dialogTitle}>
Presets Presets
</Typography> </Typography>
{(this.state.presets.getItem(this.state.selectedItem) != null) {(this.state.presets.getItem(this.state.currentItem) != null)
&& ( && (
<div style={{ flex: "0 0 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center" <div style={{
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap", alignItems: "center"
}}>
<Button color="inherit" onClick={(e) => this.handleCopy()}> }}>
Copy {this.state.selectedItems.size === 1 && (
</Button> <Button color="inherit" onClick={(e) => this.handleCopy()}
<Button color="inherit" onClick={() => this.handleRenameClick()}> >
Rename Copy
</Button> </Button>
<RenameDialog )}
title="Rename" {this.state.selectedItems.size === 1 && (
open={this.state.renameOpen}
defaultName={this.getSelectedName()} <Button color="inherit" onClick={() => this.handleRenameClick()}
acceptActionName={"Rename"} >
onClose={() => { this.setState({ renameOpen: false }) }} Rename
onOk={(text: string) => { </Button>
this.handleRenameOk(text); )}
} {this.state.renameOpen && (
} <RenameDialog
/> title="Rename"
<IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()} > open={this.state.renameOpen}
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} /> defaultName={this.getSelectedName()}
</IconButtonEx> acceptActionName={"Rename"}
onClose={() => { this.setState({ renameOpen: false }) }}
onOk={(text: string) => {
this.handleRenameOk(text);
}
}
/>
)}
{this.state.selectedItems.size === 1 && (
<IconButtonEx tooltip="Delete" color="inherit" onClick={(e) => this.handleDeleteClick()}
style={{ opacity: this.state.selectedItems.size !== 1 ? 0.5 : 1.0 }}
>
<img src="/img/old_delete_outline_white_24dp.svg" alt="Delete" style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButtonEx>
)}
<IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} > <IconButtonEx tooltip="More..." color="inherit" onClick={(e) => { this.onMoreClick(e) }} >
<MoreVertIcon /> <MoreVertIcon />
</IconButtonEx> </IconButtonEx>
@@ -419,15 +552,43 @@ const PresetDialog = withStyles(
onClose={() => this.handleMoreClose()} onClose={() => this.handleMoreClose()}
TransitionComponent={Fade} TransitionComponent={Fade}
> >
<MenuItem onClick={() => { this.handleDownloadPreset(); }} > {this.state.selectedItems.size !== 0 && (
<MenuItem onClick={() => {
this.handleMoreClose();
this.handleCopyPresetsToBank();
}}
>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText>
Copy to bank...
</ListItemText>
</MenuItem>
)}
<MenuItem onClick={() => { this.handleImportPresetsFromBank(); }} >
<ListItemIcon> <ListItemIcon>
<DownloadIcon className={classes.listIcon} /> <Icon />
</ListItemIcon> </ListItemIcon>
<ListItemText> <ListItemText>
Download preset Import presets from bank...
</ListItemText> </ListItemText>
</MenuItem> </MenuItem>
<Divider />
{this.state.selectedItems.size !== 0 && (
<MenuItem onClick={() => { this.handleDownloadPreset(); }} >
<ListItemIcon>
<DownloadIcon className={classes.listIcon} />
</ListItemIcon>
<ListItemText>
Download preset
</ListItemText>
</MenuItem>
)}
<MenuItem onClick={() => { this.handleUploadPreset() }}> <MenuItem onClick={() => { this.handleUploadPreset() }}>
<ListItemIcon> <ListItemIcon>
<UploadIcon className={classes.listIcon} /> <UploadIcon className={classes.listIcon} />
@@ -449,12 +610,16 @@ const PresetDialog = withStyles(
</div> </div>
<div style={{ flex: "1 1 auto", position: "relative", overflow: "hidden" }} > <div style={{ flex: "1 1 auto", position: "relative", overflow: "hidden" }} >
<DraggableGrid <DraggableGrid
onLongPress={(item) => this.showActionBar(true)} onLongPress={(e,item) => {
canDrag={this.isEditMode()} if (e.pointerType === "touch")
{
this.showTouchActionBar(true);
}
}}
canDrag={!this.state.multiSelect && !this.state.showTouchActionBar}
onDragStart={(index, x, y) => { this.selectItemAtIndex(index) }} onDragStart={(index, x, y) => { this.selectItemAtIndex(index) }}
moveElement={(from, to) => { this.moveElement(from, to); }} moveElement={(from, to) => { this.moveElement(from, to); }}
scroll={ScrollDirection.Y} scroll={ScrollDirection.Y}
defaultSelectedIndex={defaultSelectedIndex}
> >
{ {
this.state.presets.presets.map((element) => { this.state.presets.presets.map((element) => {
@@ -464,14 +629,28 @@ const PresetDialog = withStyles(
</DraggableGrid> </DraggableGrid>
</div> </div>
</div> </div>
<UploadPresetDialog <UploadPresetDialog
title='Upload preset' title='Upload preset'
extension='.piPreset' extension='.piPreset'
uploadPage='uploadPreset' uploadPage='uploadPreset'
onUploaded={(instanceId) => this.setState({selectedItem: instanceId}) } onUploaded={(instanceId) => this.setSelection(instanceId)}
uploadAfter={this.state.selectedItem} uploadAfter={this.state.currentItem}
open={this.state.openUploadPresetDialog} open={this.state.openUploadPresetDialog}
onClose={() => { this.setState({ openUploadPresetDialog: false }) }} /> onClose={() => { this.setState({ openUploadPresetDialog: false }) }} />
{this.state.importOpen && (
<ImportPresetFromBankDialog
open={this.state.importOpen}
onClose={() => this.setState({ importOpen: false })}
onOk={(bankInstanceId, presets) => this.handleImportDialogOk(bankInstanceId, presets)}
/>
)}
{this.state.copyToOpen && (
<CopyPresetsToBankDialog
open={this.state.copyToOpen}
onClose={() => this.setState({ copyToOpen: false })}
onOk={(bankInstanceId) => this.handleCopyToBankDialogOk(bankInstanceId)}
/>
)}
</DialogEx> </DialogEx>
+129 -33
View File
@@ -17,7 +17,8 @@
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // 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. // 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 IconButtonEx from './IconButtonEx';
import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory, PresetIndex } from './PiPedalModel';
import SaveIconOutline from '@mui/icons-material/Save'; 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 { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui"; import { withStyles } from "tss-react/mui";
import {createStyles} from './WithStyles'; import { createStyles } from './WithStyles';
import PresetDialog from './PresetDialog'; import PresetDialog from './PresetDialog';
import Menu from '@mui/material/Menu'; import Menu from '@mui/material/Menu';
@@ -33,9 +34,13 @@ import MenuItem from '@mui/material/MenuItem';
import Fade from '@mui/material/Fade'; import Fade from '@mui/material/Fade';
import Divider from '@mui/material/Divider'; import Divider from '@mui/material/Divider';
import RenameDialog from './RenameDialog' import RenameDialog from './RenameDialog'
import SavePresetAsDialog from './SavePresetAsDialog';
import ImportPresetFromBankDialog from './ImportPresetFromBankDialog';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import UploadPresetDialog from './UploadPresetDialog'; import UploadPresetDialog from './UploadPresetDialog';
import {isDarkMode} from './DarkMode'; import { isDarkMode } from './DarkMode';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
interface PresetSelectorProps extends WithStyles<typeof styles> { interface PresetSelectorProps extends WithStyles<typeof styles> {
@@ -48,7 +53,12 @@ interface PresetSelectorState {
showPresetsDialog: boolean; showPresetsDialog: boolean;
showEditPresetsDialog: boolean; showEditPresetsDialog: boolean;
presetsMenuAnchorRef: HTMLElement | null; presetsMenuAnchorRef: HTMLElement | null;
presetsSubmenuAnchorRef: HTMLElement | null;
compactHorizontalLayoutMenu: boolean;
saveAsDialogOpen: boolean;
importDialogOpen: boolean;
renameDialogOpen: boolean; renameDialogOpen: boolean;
renameDialogTitle: string; renameDialogTitle: string;
renameDialogDefaultName: string; renameDialogDefaultName: string;
@@ -59,7 +69,7 @@ interface PresetSelectorState {
} }
const selectColor = isDarkMode()? "#888": "#FFFFFF"; const selectColor = isDarkMode() ? "#888" : "#FFFFFF";
const styles = (theme: Theme) => createStyles({ const styles = (theme: Theme) => createStyles({
select: { // fu fu fu.Overrides for white selector on dark background. select: { // fu fu fu.Overrides for white selector on dark background.
@@ -81,8 +91,8 @@ const styles = (theme: Theme) => createStyles({
const PresetSelector = const PresetSelector =
withStyles( withStyles(
class extends Component<PresetSelectorProps, PresetSelectorState> { class extends ResizeResponsiveComponent<PresetSelectorProps, PresetSelectorState> {
private MENU_THRESHOLD: number = 570;
model: PiPedalModel; model: PiPedalModel;
constructor(props: PresetSelectorProps) { constructor(props: PresetSelectorProps) {
@@ -92,10 +102,14 @@ const PresetSelector =
presets: presets:
this.model.presets.get(), this.model.presets.get(),
enabled: false, enabled: false,
compactHorizontalLayoutMenu: this.windowSize.height < this.MENU_THRESHOLD,
showPresetsDialog: false, showPresetsDialog: false,
showEditPresetsDialog: false, showEditPresetsDialog: false,
presetsMenuAnchorRef: null, presetsMenuAnchorRef: null,
presetsSubmenuAnchorRef: null,
renameDialogOpen: false, renameDialogOpen: false,
saveAsDialogOpen: false,
importDialogOpen: false,
renameDialogTitle: "", renameDialogTitle: "",
renameDialogDefaultName: "", renameDialogDefaultName: "",
renameDialogActionName: "", renameDialogActionName: "",
@@ -109,12 +123,34 @@ const PresetSelector =
this.handlePresetsMenuClose = this.handlePresetsMenuClose.bind(this); 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) }); this.setState({ presetsMenuAnchorRef: (event.currentTarget as HTMLElement) });
} }
handlePresetsSubmenuClick(event: SyntheticEvent): void {
this.setState({ presetsSubmenuAnchorRef: (event.currentTarget as HTMLElement) });
}
handlePresetsMenuClose(): void { 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) { handleDownloadPreset(e: SyntheticEvent) {
@@ -144,21 +180,8 @@ const PresetSelector =
let currentPresets = this.model.presets.get(); let currentPresets = this.model.presets.get();
let item = currentPresets.getItem(currentPresets.selectedInstanceId); let item = currentPresets.getItem(currentPresets.selectedInstanceId);
if (item == null) return; 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 { handlePresetsMenuRename(e: SyntheticEvent): void {
this.handlePresetsMenuClose(); this.handlePresetsMenuClose();
@@ -198,7 +221,7 @@ const PresetSelector =
showError(error: string) { showError(error: string) {
this.model.showAlert(error); this.model.showAlert(error);
} }
renameDialogOpen(defaultText: string, title: string,acceptButtonText: string): Promise<string> { renameDialogOpen(defaultText: string, title: string, acceptButtonText: string): Promise<string> {
let result = new Promise<string>( let result = new Promise<string>(
(resolve, reject) => { (resolve, reject) => {
this.setState( this.setState(
@@ -219,6 +242,30 @@ const PresetSelector =
return result; 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 { handleRenameDialogClose(): void {
this.setState({ this.setState({
renameDialogOpen: false, renameDialogOpen: false,
@@ -259,11 +306,13 @@ const PresetSelector =
this.updatePresetState(); this.updatePresetState();
} }
componentDidMount() { componentDidMount() {
super.componentDidMount();
this.model.presets.addOnChangedHandler(this.handlePresetsChanged); this.model.presets.addOnChangedHandler(this.handlePresetsChanged);
this.updatePresetState(); this.updatePresetState();
} }
componentWillUnmount() { componentWillUnmount() {
this.model.presets.removeOnChangedHandler(this.handlePresetsChanged) this.model.presets.removeOnChangedHandler(this.handlePresetsChanged)
super.componentWillUnmount();
} }
showPresetDialog(show: boolean) { showPresetDialog(show: boolean) {
this.setState({ this.setState({
@@ -318,7 +367,7 @@ const PresetSelector =
onChange={(e, extra) => this.handleChange(e, extra)} onChange={(e, extra) => this.handleChange(e, extra)}
onClose={(e) => this.handleSelectClose(e)} onClose={(e) => this.handleSelectClose(e)}
displayEmpty displayEmpty
value={presets.selectedInstanceId === 0? '' : presets.selectedInstanceId} value={presets.selectedInstanceId === 0 ? '' : presets.selectedInstanceId}
inputProps={{ inputProps={{
classes: { icon: classes.icon }, classes: { icon: classes.icon },
'aria-label': "Select preset" 'aria-label': "Select preset"
@@ -339,13 +388,13 @@ const PresetSelector =
} }
</Select> </Select>
</div> </div>
<div style={{ flex: "0 0 auto"}}> <div style={{ flex: "0 0 auto" }}>
<IconButtonEx <IconButtonEx
tooltip="More..." tooltip="More..."
style={{ flex: "0 0 auto", color: "#FFFFFF" }} style={{ flex: "0 0 auto", color: "#FFFFFF" }}
onClick={(e) => this.handlePresetMenuClick(e)} onClick={(e) => this.handlePresetsMenuClick(e)}
size="large" size="large"
> >
<MoreVertIcon style={{ opacity: 0.75 }} color="inherit" /> <MoreVertIcon style={{ opacity: 0.75 }} color="inherit" />
</IconButtonEx> </IconButtonEx>
<Menu <Menu
@@ -356,9 +405,24 @@ const PresetSelector =
TransitionComponent={Fade} TransitionComponent={Fade}
> >
<MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save preset</MenuItem> <MenuItem onClick={(e) => this.handlePresetsMenuSave(e)}>Save preset</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem> {this.state.compactHorizontalLayoutMenu ? (
<MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem> <MenuItem key="a" onClick={(e) => this.handlePresetsSubmenuClick(e)}
style={{ display: 'flex', flexFlow: "row nowrap", alignItems: 'center', justifyContent: 'space-between' }}
>
Edit
<ArrowRightIcon/>
</MenuItem>
)
:(
[
(<MenuItem key="b" onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>),
(<MenuItem key="c" onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>),
(<MenuItem key="d" onClick={(e) => this.handlePresetsMenuImport(e)}>Import from bank...</MenuItem>),
(<MenuItem key="e" onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>),
]
)}
<Divider /> <Divider />
<MenuItem onClick={(e) => { this.handleDownloadPreset(e); }} >Download preset</MenuItem> <MenuItem onClick={(e) => { this.handleDownloadPreset(e); }} >Download preset</MenuItem>
<MenuItem onClick={(e) => { this.handleUploadPreset(e) }}>Upload preset</MenuItem> <MenuItem onClick={(e) => { this.handleUploadPreset(e) }}>Upload preset</MenuItem>
@@ -366,7 +430,39 @@ const PresetSelector =
<MenuItem onClick={(e) => this.handleMenuEditPresets()}>Manage presets...</MenuItem> <MenuItem onClick={(e) => this.handleMenuEditPresets()}>Manage presets...</MenuItem>
</Menu> </Menu>
</div> </div>
<PresetDialog show={this.state.showPresetsDialog} isEditDialog={this.state.showEditPresetsDialog} onDialogClose={() => this.handleDialogClose()} />
{/* Submenu */}
<Menu key="b" onClose={() => this.handlePresetsSubmenuClose()}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
anchorPosition={{left: 48, top: 0}}
anchorEl={this.state.presetsSubmenuAnchorRef}
open={this.state.presetsSubmenuAnchorRef !== null} >
<MenuItem onClick={(e) => this.handlePresetsMenuSaveAs(e)}>Save preset as...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuRename(e)}>Rename...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuImport(e)}>Import from bank...</MenuItem>
<MenuItem onClick={(e) => this.handlePresetsMenuNew(e)}>New...</MenuItem>
</Menu>
{this.state.showPresetsDialog&& (
<PresetDialog show={this.state.showPresetsDialog} onDialogClose={() => this.handleDialogClose()}
/>
)}
{this.state.saveAsDialogOpen && (
<SavePresetAsDialog open={this.state.saveAsDialogOpen}
defaultName={presets.getItem(presets.selectedInstanceId)?.name ?? "My Preset"}
onClose={() => { this.setState({ saveAsDialogOpen: false }) }}
onOk={(bankInstanceId, name) => {
this.handleSaveAsDialogOk(bankInstanceId, name);
}} />
)}
{this.state.importDialogOpen && (
<ImportPresetFromBankDialog open={this.state.importDialogOpen}
onClose={() => { this.setState({ importDialogOpen: false }) }}
onOk={(bankInstanceId, presets) => {
this.handleImportDialogOk(bankInstanceId, presets);
}} />
)}
<RenameDialog open={this.state.renameDialogOpen} <RenameDialog open={this.state.renameDialogOpen}
title={this.state.renameDialogTitle} title={this.state.renameDialogTitle}
defaultName={this.state.renameDialogDefaultName} defaultName={this.state.renameDialogDefaultName}
@@ -387,6 +483,6 @@ const PresetSelector =
} }
}, },
styles); styles);
export default PresetSelector; export default PresetSelector;
+10 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies // Copyright (c) Robin E. R. Davies
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy of // 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 // this software and associated documentation files (the "Software"), to deal in
@@ -136,7 +136,16 @@ export default class RenameDialog extends ResizeResponsiveComponent<RenameDialog
autoFocus={!isTouchUi()} autoFocus={!isTouchUi()}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
variant="standard" variant="standard"
autoComplete="off"
autoCorrect="on"
autoCapitalize="on"
spellCheck={false}
slotProps={{ slotProps={{
inputLabel: {
shrink: true
},
input: { input: {
style: { scrollMargin: 24 } style: { scrollMargin: 24 }
} }
+189
View File
@@ -0,0 +1,189 @@
// 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 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 { nullCast } from './Utility';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel';
//import TextFieldEx from './TextFieldEx';
import TextField from '@mui/material/TextField';
import { BankIndex } from './Banks';
export interface SavePresetAsDialogProps {
open: boolean,
defaultName: string,
onOk: (bankInstanceId: number, text: string) => 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<SavePresetAsDialogProps, SavePresetAsDialogState> {
refText: React.RefObject<HTMLInputElement|null>;
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<HTMLInputElement>();
}
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<HTMLDivElement>): void => {
// 'keypress' event misbehaves on mobile so we track 'Enter' key via 'keydown' event
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
handleOk();
}
};
return (
<DialogEx tag="savePresetAs" open={open} fullWidth maxWidth="xs" onClose={handleClose} aria-labelledby="Rename-dialog-title"
fullScreen={this.state.fullScreen}
style={{userSelect: "none"}}
onEnterKey={()=>{}}
>
<DialogTitle>
Save Preset As
</DialogTitle>
<DialogContent >
<InputLabel style={{ fontSize: "0.75rem", fontWeight: 400, marginTop: 16 }}>Bank</InputLabel>
<Select variant="standard" fullWidth value={this.state.selectedBank} style={{marginBottom: 16}}
onChange={(e) => this.setState({ selectedBank: e.target.value as number })}>
{this.state.banks.entries.map((bankEntry) => {
return (
<MenuItem key={bankEntry.instanceId} value={bankEntry.instanceId}
selected={bankEntry.instanceId === this.state.selectedBank}
>
{bankEntry.name}
</MenuItem>
);
})}
</Select>
<TextField
autoFocus={!isTouchUi()}
onKeyDown={handleKeyDown}
autoComplete="off"
autoCorrect="on"
autoCapitalize="on"
spellCheck={false}
variant="standard"
slotProps={{
inputLabel: {
shrink: true
},
input: {
style: { scrollMargin: 24 }
}
}}
id="name"
type="text"
label={"Name"}
fullWidth
defaultValue={defaultName}
inputRef={this.refText}
/>
</DialogContent>
<DialogActions style={{flexShrink: 1}}>
<Button onClick={handleClose} variant="dialogSecondary" >
Cancel
</Button>
<Button onClick={handleOk} variant="dialogPrimary" >
OK
</Button>
</DialogActions>
</DialogEx>
);
}
}
@@ -57,6 +57,7 @@ interface HoverProps extends WithStyles<typeof styles> {
selected: boolean; selected: boolean;
showHover?: boolean; showHover?: boolean;
borderRadius?: number; borderRadius?: number;
clipChildren?: boolean;
children?: React.ReactNode; children?: React.ReactNode;
} }
+4 -4
View File
@@ -75,15 +75,15 @@ export default class SnapshotButton extends ResizeResponsiveComponent<SnapshotBu
if (this.is2x3Layout(width,height)) { if (this.is2x3Layout(width,height)) {
uiScale = width / 400; uiScale = width / 400;
} else { } else {
uiScale = (width/3)/(400/2); uiScale = (width/3)/(400/2)*1.5;
} }
if (height < width) { if (height < width) {
// 1 at 400, 2 at 600 // 1 at 400, 2 at 600
let hUiScale = (height-340)/(400-340); let hUiScale = (height-200)/(400-200);
hUiScale += (height-400)/(460-400); hUiScale += 1;
uiScale = Math.min(hUiScale,uiScale); uiScale = Math.min(hUiScale,uiScale);
} }
if (uiScale < 1) uiScale = 1; if (uiScale < 1.3) uiScale = 1.3;
if (uiScale > 4) uiScale = 4; if (uiScale > 4) uiScale = 4;
this.setState({uiScale: uiScale}); this.setState({uiScale: uiScale});
} }
+42 -21
View File
@@ -28,6 +28,7 @@ import { withStyles } from "tss-react/mui";
import { PiPedalModelFactory, PiPedalModel, State } from "./PiPedalModel"; import { PiPedalModelFactory, PiPedalModel, State } from "./PiPedalModel";
import Utility from './Utility'; import Utility from './Utility';
import SvgPathBuilder from './SvgPathBuilder'; import SvgPathBuilder from './SvgPathBuilder';
import { UiFrequencyPlot } from './Lv2Plugin';
const StandardItemSize = { width: 80, height: 110 }; const StandardItemSize = { width: 80, height: 110 };
@@ -50,6 +51,7 @@ interface ToobFrequencyResponseProps extends WithStyles<typeof styles> {
instanceId: number; instanceId: number;
propertyName?: string; propertyName?: string;
width?: number; width?: number;
frequencyPlot?: UiFrequencyPlot
} }
interface ToobFrequencyResponseState { interface ToobFrequencyResponseState {
@@ -57,6 +59,8 @@ interface ToobFrequencyResponseState {
maxDb: number; maxDb: number;
minF: number; minF: number;
maxF: number; maxF: number;
logX: boolean;
dbY: boolean;
path: string; path: string;
} }
@@ -74,10 +78,12 @@ const ToobFrequencyResponseView =
this.model = PiPedalModelFactory.getInstance(); this.model = PiPedalModelFactory.getInstance();
this.state = { this.state = {
path: "", path: "",
minDb: -30, minDb: this.props.frequencyPlot?.yBottom ?? -30,
maxDb: 5, maxDb: this.props.frequencyPlot?.yTop ?? 5,
minF: 30, minF: this.props.frequencyPlot?.xLeft ?? 30,
maxF: 20000 maxF: this.props.frequencyPlot?.xRight ?? 20000,
logX: this.props.frequencyPlot?.xLog ?? true,
dbY: this.props.frequencyPlot?.yDb ?? true
}; };
this.pathRef = React.createRef(); this.pathRef = React.createRef();
@@ -140,16 +146,22 @@ const ToobFrequencyResponseView =
} }
toY(value: number): number { toY(value: number): number {
value = Math.abs(value); if (this.state.dbY)
{
value = Math.abs(value);
var db; var db;
if (value < this.MIN_DB_AF) { if (value < this.MIN_DB_AF) {
db = -192.0; 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 { } 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[]) { onFrequencyResponseUpdated(data: number[]) {
@@ -159,6 +171,7 @@ const ToobFrequencyResponseView =
let dbMin = data[2]; let dbMin = data[2];
let dbMax = data[3]; let dbMax = data[3];
let xMax = this.xMax(); let xMax = this.xMax();
let dbY = this.state.dbY;
let n = data.length-4; let n = data.length-4;
@@ -166,16 +179,22 @@ const ToobFrequencyResponseView =
return (xMax - this.xMin) * bin/n; return (xMax - this.xMin) * bin/n;
}; };
let toY_ = (value: number): number => { let toY_ = dbY?
(value: number): number => {
var db; var db;
if (value < this.MIN_DB_AF) { if (value < this.MIN_DB_AF) {
db = -192.0; db = -192.0;
} else { } else {
db = 20 * Math.log10(value); 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[] { grid(): React.ReactNode[] {
let result: React.ReactNode[] = []; let result: React.ReactNode[] = [];
if (!this.state.dbY) return result;
let xMax = this.xMax(); let xMax = this.xMax();
for (var db = Math.ceil(this.dbMax / this.dbTickSpacing) * this.dbTickSpacing; db < this.dbMin; db += this.dbTickSpacing) { 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); var y = (db - this.dbMin) / (this.dbMax - this.dbMin) * (this.yMax - this.yMin);
@@ -277,6 +296,7 @@ const ToobFrequencyResponseView =
dbMin: number = 5; dbMin: number = 5;
dbMax: number = -35; dbMax: number = -35;
dbY: boolean = true;
private nextKey: number = 0; private nextKey: number = 0;
render() { render() {
@@ -284,6 +304,7 @@ const ToobFrequencyResponseView =
this.nextKey = 0; this.nextKey = 0;
this.dbMax = this.state.minDb; this.dbMax = this.state.minDb;
this.dbMin = this.state.maxDb; this.dbMin = this.state.maxDb;
this.dbY = this.state.dbY;
this.fMin = this.state.minF; this.fMin = this.state.minF;
this.fMax = this.state.maxF; this.fMax = this.state.maxF;
+28 -25
View File
@@ -1,25 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="48px" <svg
height="48px" viewBox="0 0 48 48" enable-background="new 0 0 48 48" xml:space="preserve"> version="1.1"
<g id="x"> x="0px"
</g> y="0px"
<g id="Layer_1"> width="48px"
<path d="M47,41c0,3.313-2.688,6-6,6H7c-3.313,0-6-2.688-6-6V7c0-3.313,2.687-6,6-6h34c3.313,0,6,2.687,6,6V41z"/> height="48px"
<line fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="square" x1="10.167" y1="32.584" x2="38.584" y2="32.584"/> viewBox="0 0 48 48"
<line fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="square" x1="12.5" y1="32.584" x2="12.5" y2="12"/> enable-background="new 0 0 48 48"
<line fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="square" x1="20.5" y1="32.584" x2="20.5" y2="19"/> xml:space="preserve"
<line fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="square" x1="28.5" y1="32.584" x2="28.5" y2="15"/> id="svg870"
<line fill="none" stroke="#FFFFFF" stroke-width="3" stroke-linecap="square" x1="36.5" y1="32.584" x2="36.5" y2="23"/> xmlns="http://www.w3.org/2000/svg"
</g> xmlns:svg="http://www.w3.org/2000/svg"><defs
<g id="Compound" display="none"> id="defs874">
<g display="inline">
<path d="M41,1H7C3.687,1,1,3.687,1,7v34c0,3.313,2.687,6,6,6h34c3.313,0,6-2.688,6-6V7C47,3.687,44.313,1,41,1z M24,40.928
c-9.334,0-16.928-7.594-16.928-16.928S14.666,7.072,24,7.072S40.928,14.666,40.928,24S33.334,40.928,24,40.928z"/>
<path d="M24,9.072C15.769,9.072,9.072,15.769,9.072,24c0,8.231,6.697,14.928,14.928,14.928c8.231,0,14.928-6.696,14.928-14.928
C38.928,15.769,32.231,9.072,24,9.072z M31.296,20.515l-2.568-1.528l-4.205,7.069l-2.579-1.533l4.206-7.07l-2.567-1.527
l7.831-4.384L31.296,20.515z"/>
</g> </defs>
</g> <path
</svg> id="path850"
d="M 7 1 C 3.6870033 1 1 3.6870033 1 7 L 1 41 C 1 44.311997 3.6870033 47 7 47 L 41 47 C 44.311997 47 47 44.312997 47 41 L 47 7 C 47 3.6870033 44.312997 1 41 1 L 7 1 z M 11 10.5 L 14 10.5 L 14 12 L 14 31.083984 L 19 31.083984 L 19 19 L 19 17.5 L 22 17.5 L 22 19 L 22 31.083984 L 27 31.083984 L 27 15 L 27 13.5 L 30 13.5 L 30 15 L 30 31.083984 L 35 31.083984 L 35 23 L 35 21.5 L 38 21.5 L 38 23 L 38 31.083984 L 38.583984 31.083984 L 40.083984 31.083984 L 40.083984 34.083984 L 38.583984 34.083984 L 38 34.083984 L 35 34.083984 L 10.167969 34.083984 L 8.6679688 34.083984 L 8.6679688 31.083984 L 10.167969 31.083984 L 11 31.083984 L 11 12 L 11 10.5 z " />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB