Merge pull request #239 from rerdavies/dev

v1.3.62 Merge
This commit is contained in:
Robin Davies
2024-10-28 10:06:40 -04:00
committed by GitHub
54 changed files with 1866 additions and 1098 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
cmake_minimum_required(VERSION 3.16.0) cmake_minimum_required(VERSION 3.16.0)
project(pipedal project(pipedal
VERSION 1.3.61 VERSION 1.3.62
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"
) )
set (DISPLAY_VERSION "PiPedal v1.3.61-Release") set (DISPLAY_VERSION "PiPedal v1.3.62-Beta")
set (PACKAGE_ARCHITECTURE "arm64") set (PACKAGE_ARCHITECTURE "arm64")
set (CMAKE_INSTALL_PREFIX "/usr/") set (CMAKE_INSTALL_PREFIX "/usr/")
+16
View File
@@ -51,6 +51,22 @@ namespace pipedal {
} }
template <typename T>
inline bool contains(const std::vector<T>&vector,const T&value)
{
for (auto i = vector.begin(); i != vector.end(); ++i)
{
if ((*i) == value) {
return true;
}
}
return false;
}
inline bool contains(const std::vector<std::string>&vector,const char*value)
{
return contains(vector,std::string(value));
}
class NoCopy { class NoCopy {
public: public:
NoCopy() { } NoCopy() { }
+2 -2
View File
@@ -6,13 +6,13 @@
<a href="https://rerdavies.github.io/pipedal/LicensePiPedal.html"><img src="https://img.shields.io/badge/MIT-MIT?label=license&color=%23808080"/></a> <a href="https://rerdavies.github.io/pipedal/LicensePiPedal.html"><img src="https://img.shields.io/badge/MIT-MIT?label=license&color=%23808080"/></a>
<a href="https://github.com/rerdavies/pipedal/actions"><img src="https://img.shields.io/github/actions/workflow/status/rerdavies/pipedal/cmake.yml?branch=main"/></a> <a href="https://github.com/rerdavies/pipedal/actions"><img src="https://img.shields.io/github/actions/workflow/status/rerdavies/pipedal/cmake.yml?branch=main"/></a>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.3.61</a> Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.3.62</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).
&nbsp; &nbsp;
#### NEW version 1.3.61 Release, providing [snapshots](https://rerdavies.github.io/pipedal/Snaphots.html), and a new Performance View. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. #### NEW version 1.3.62 Release, providing [snapshots](https://rerdavies.github.io/pipedal/Snaphots.html), and a new Performance View. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
&nbsp; &nbsp;
+3 -3
View File
@@ -13,17 +13,17 @@ 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 (64-bit) v1.3.61](https://github.com/rerdavies/pipedal/releases/download/) - [Raspberry Pi OS bookworm (64-bit) v1.3.62](https://github.com/rerdavies/pipedal/releases/download/)
- [Ubuntu/Raspberry Pi OS bullseyeye (64-bit) v1.2.31](https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb) - [Ubuntu/Raspberry Pi OS bullseyeye (64-bit) v1.2.31](https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb)
Version 1.3.61 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31. Version 1.3.62 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31.
Install the package by running Install the package by running
``` ```
sudo apt update sudo apt update
cd ~/Downloads cd ~/Downloads
sudo apt-get install pipedal_1.3.61_arm64.deb sudo apt-get install pipedal_1.3.62_arm64.deb
``` ```
Adjust accordingly if you have downloaded v1.1.31. Adjust accordingly if you have downloaded v1.1.31.
+26
View File
@@ -1,5 +1,31 @@
# Release Notes # Release Notes
## PiPedal 1.3.62 Beta
Features:
- Plugins can now share uploaded model and IR files by type. For plugins with appropriate mod:fileTypes attributes, uploaded
files will be shared by category (Nam model, IR Files, Audio Tracks, Midi Songs, etc). Sharing of ML models are also supported, although
that requires use of an extended mod:fileType (mlmodel) that MOD does not support.
- TooB Tuner stability issues fixed. The pitch detection algorithm has been improved, and the tuner now does filtering to avoid
displaying a value when the current input isn't a steady note.
- LV2 trigger controls are now displayed as buttons instead of toggle controls (maybe useful for 3rd-party looper plugins).
Known issues:
- There are a number of outstanding minor compatibility issues with Ratatouille. I intend to work with the author to address them.
### Backwards Compatibility With the Previous Upload Directory Scheme
Sharing file types poses a problem with previous versions of PiPedal which did not share uploads by file type, but instead placed uploads in a private directory for each plugin.
In order to accomodate this, Pipedal provides legacy support for this situation. If a private upload directory for a plugin exists, the file property selection dialog will show
both the new shared directories, and the old private directory. The private directory has a name that reflects the name of the plugin (e.g. "Ratatoille.lv2"). This directory only
shows up if you have used the plugin on previous versiouns of Pipedal. If you have nothing of particular value in the old private upload directory, you can delete the plugin's private directory (which can be found in `/var/pipedal/audio_uploads`), and the private directory will no longer be displayed in the PiPedal UI.
I anticipate providing a migration utility in the near future which will clean up and migrate legacy upload directories to the new directory structure (automatically uploading
presets which reference uploaded files that have moved). In the meantime, I think you will find the current occomodation for legacy upload directories perfectly functional.
## PiPedal 1.3.61 Release ## PiPedal 1.3.61 Release
Bug fixes: Bug fixes:
+2 -2
View File
@@ -4,7 +4,7 @@
Download the most recent Debian (.deb) package for your platform: Download the most recent Debian (.deb) package for your platform:
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.3.61/pipedal_1.3.61_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.3.61</a> - <a href="https://github.com/rerdavies/pipedal/releases/download/v1.3.62/pipedal_1.3.62_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.3.62</a>
Install the package by running Install the package by running
@@ -12,7 +12,7 @@ Install the package by running
``` ```
sudo apt update sudo apt update
cd ~/Downloads cd ~/Downloads
sudo apt-get install ./pipedal_1.3.61_arm64.deb sudo apt-get install ./pipedal_1.3.62_arm64.deb
``` ```
Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation.
+2 -2
View File
@@ -1,7 +1,7 @@
<img src="GithubBanner.png" width="100%"/> <img src="GithubBanner.png" width="100%"/>
<a href="Installing.html"><i>v1.3.61</i></a> <a href="Installing.html"><i>v1.3.62</i></a>
&nbsp; &nbsp;
@@ -9,7 +9,7 @@ To download PiPedal, click [here](download.md).
To view PiPedal documentation, click [here](Documentation.md). To view PiPedal documentation, click [here](Documentation.md).
#### NEW version 1.3.61 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. #### NEW version 1.3.62 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
&nbsp; &nbsp;
+4 -4
View File
@@ -55,7 +55,7 @@ cabir:impulseFile
a lv2:Parameter; a lv2:Parameter;
rdfs:label "IR"; rdfs:label "IR";
pg:group cabir:impulseFile1Group ; pg:group cabir:impulseFile1Group ;
mod:fileTypes "wav,flac"; mod:fileTypes "ir,wav,flac";
rdfs:range atom:Path. rdfs:range atom:Path.
@@ -63,14 +63,14 @@ cabir:impulseFile2
a lv2:Parameter; a lv2:Parameter;
rdfs:label "IR"; rdfs:label "IR";
pg:group cabir:impulseFile2Group ; pg:group cabir:impulseFile2Group ;
mod:fileTypes "wav,flac"; mod:fileTypes "ir,wav,flac";
rdfs:range atom:Path. rdfs:range atom:Path.
cabir:impulseFile3 cabir:impulseFile3
a lv2:Parameter; a lv2:Parameter;
rdfs:label "IR"; rdfs:label "IR";
pg:group cabir:impulseFile3Group ; pg:group cabir:impulseFile3Group ;
mod:fileTypes "wav,flac"; mod:fileTypes "ir,wav,flac";
rdfs:range atom:Path. rdfs:range atom:Path.
@@ -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 51 ; lv2:microVersion 52 ;
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
@@ -49,7 +49,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 51 ; lv2:microVersion 52 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "TooB CabSim"; mod:label "TooB CabSim";
@@ -32,7 +32,7 @@
toobimpulse:impulseFile toobimpulse:impulseFile
a lv2:Parameter; a lv2:Parameter;
rdfs:label "Reverb Impulse File"; rdfs:label "Reverb Impulse File";
mod:fileTypes "wav,flac"; mod:fileTypes "ir,wav,flac";
rdfs:range atom:Path. rdfs:range atom:Path.
@@ -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 51 ; lv2:microVersion 52 ;
rdfs:comment """ rdfs:comment """
Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to Convolution reverb is a notoriously compute-intensive effect. If you are having performance issues, use the Max T control to constrain the length of the impulse file to
@@ -31,7 +31,7 @@
toobimpulse:impulseFile toobimpulse:impulseFile
a lv2:Parameter; a lv2:Parameter;
rdfs:label "File"; rdfs:label "File";
mod:fileTypes "wav,flac"; mod:fileTypes "ir,wav,flac";
rdfs:range atom:Path. rdfs:range atom:Path.
@@ -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 51 ; lv2:microVersion 52 ;
rdfs:comment """ rdfs:comment """
+1 -1
View File
@@ -65,7 +65,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 51 ; lv2:microVersion 52 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "TooB Input"; mod:label "TooB Input";
+1 -1
View File
@@ -67,7 +67,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 51 ; lv2:microVersion 52 ;
mod:brand "TooB"; mod:brand "TooB";
mod:label "Power Stage"; mod:label "Power Stage";
+1 -1
View File
@@ -58,7 +58,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 51 ; lv2:microVersion 52 ;
rdfs:comment "TooB spectrum analyzer" ; rdfs:comment "TooB spectrum analyzer" ;
mod:brand "TooB"; mod:brand "TooB";
+1 -1
View File
@@ -55,7 +55,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 51 ; lv2:microVersion 52 ;
uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>; uiext:ui <http://two-play.com/plugins/toob-tone-stack-ui>;
Binary file not shown.
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 51 ; lv2:microVersion 52 ;
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 51 ; lv2:microVersion 52 ;
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 51 ; lv2:microVersion 52 ;
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 51 ; lv2:microVersion 52 ;
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 51 ; lv2:microVersion 52 ;
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.
+2 -2
View File
@@ -31,7 +31,7 @@
toobml:modelFile toobml:modelFile
a lv2:Parameter; a lv2:Parameter;
rdfs:label "Model"; rdfs:label "Model";
mod:fileTypes "nam,nammodel"; mod:fileTypes "json,mlmodels";
rdfs:range atom:Path. rdfs:range atom:Path.
toobml:filterGroup toobml:filterGroup
@@ -65,7 +65,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 51 ; lv2:microVersion 52 ;
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.
@@ -61,7 +61,7 @@ toobNam: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 51 ; lv2:microVersion 52 ;
rdfs:comment """ rdfs:comment """
A port of Steven Atkinson's Neural Amp Modeler to LV2. A port of Steven Atkinson's Neural Amp Modeler to LV2.
+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 51 ; lv2:microVersion 52 ;
rdfs:comment """ rdfs:comment """
TooB Tuner is a chromatic guitar tuner. TooB Tuner is a chromatic guitar tuner.
""" ; """ ;
+1
View File
@@ -198,6 +198,7 @@ class DialogEx extends React.Component<DialogExProps,DialogExState> implements I
{ {
this.onEnterKey(); this.onEnterKey();
} }
evt.stopPropagation();
} }
render() { render() {
let { tag,onClose,...extra} = this.props; let { tag,onClose,...extra} = this.props;
File diff suppressed because it is too large Load Diff
+24 -19
View File
@@ -38,28 +38,18 @@ import { isDarkMode } from './DarkMode';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
function pathConcat(l: string, r: string): string
{
if (l.length === 0) return r;
if (r.length === 0) return l;
return l +'/' + r;
}
class DirectoryTree { class DirectoryTree {
name: string = ""; name: string = "";
path: string = ""; path: string = "";
expanded: boolean = false; expanded: boolean = false;
isProtected: boolean = true;
children: DirectoryTree[] = []; children: DirectoryTree[] = [];
constructor(tree: FilePropertyDirectoryTree, parentTree: (DirectoryTree | null) = null) { constructor(tree: FilePropertyDirectoryTree, parentTree: (DirectoryTree | null) = null) {
this.name = tree.directoryName; this.name = tree.directoryName;
if (parentTree) { this.path = tree.directoryName;
this.path = pathConcat(parentTree.path,tree.directoryName); this.name = tree.displayName;
} else { this.isProtected = tree.isProtected;
this.name = "Home";
this.path = tree.directoryName;
this.expanded = true;
}
for (var treeChild of tree.children) { for (var treeChild of tree.children) {
this.children.push(new DirectoryTree(treeChild, this)); this.children.push(new DirectoryTree(treeChild, this));
} }
@@ -143,6 +133,7 @@ export interface FilePropertyDirectorySelectDialogState {
selectedPath: string; selectedPath: string;
directoryTree?: DirectoryTree; directoryTree?: DirectoryTree;
hasSelection: boolean; hasSelection: boolean;
isProtected: boolean;
directoryTreeInvalidatecount: number; directoryTreeInvalidatecount: number;
}; };
@@ -160,6 +151,7 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
selectedPath: props.defaultPath, selectedPath: props.defaultPath,
directoryTree: undefined, directoryTree: undefined,
hasSelection: false, hasSelection: false,
isProtected: true,
directoryTreeInvalidatecount: 0 directoryTreeInvalidatecount: 0
}; };
@@ -192,14 +184,19 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty) this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty)
.then((filePropertyDirectoryTree) => { .then((filePropertyDirectoryTree) => {
let myTree = new DirectoryTree(filePropertyDirectoryTree); let myTree = new DirectoryTree(filePropertyDirectoryTree);
if (this.props.excludeDirectory) if (this.props.excludeDirectory.length !== 0)
{ {
myTree.remove(this.props.excludeDirectory); myTree.remove(this.props.excludeDirectory);
} }
myTree.expand(this.state.selectedPath); myTree.expand(this.state.selectedPath);
let hasSelection = myTree.find(this.state.selectedPath) != null; let selection = myTree.find(this.state.selectedPath);
this.setState({ directoryTree: myTree, hasSelection: hasSelection }); let hasSelection = !!selection;
this.setState({
directoryTree: myTree,
hasSelection: hasSelection,
isProtected: selection ? selection.isProtected : false
});
this.requestScroll = true; this.requestScroll = true;
}) })
.catch((e) => { .catch((e) => {
@@ -223,6 +220,9 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
onOK() { onOK() {
if (this.state.isProtected) {
return;
}
if (this.state.hasSelection) { if (this.state.hasSelection) {
this.props.onOk(this.state.selectedPath); this.props.onOk(this.state.selectedPath);
} }
@@ -238,7 +238,11 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
private onTreeClick(directoryTree: DirectoryTree) { private onTreeClick(directoryTree: DirectoryTree) {
if (!this.state.directoryTree) return; if (!this.state.directoryTree) return;
this.state.directoryTree.expand(directoryTree.path); this.state.directoryTree.expand(directoryTree.path);
this.setState({ selectedPath: directoryTree.path, hasSelection: true, directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount + 1 }); this.setState({
selectedPath: directoryTree.path,
hasSelection: true,
isProtected: directoryTree.isProtected,
directoryTreeInvalidatecount: this.state.directoryTreeInvalidatecount + 1 });
} }
private renderTree_(directoryTree: DirectoryTree) { private renderTree_(directoryTree: DirectoryTree) {
let ref: ((element: HTMLButtonElement | null) => void) | undefined = undefined; let ref: ((element: HTMLButtonElement | null) => void) | undefined = undefined;
@@ -338,7 +342,8 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
<Button variant="dialogSecondary" onClick={() => { this.onClose(); }} color="primary"> <Button variant="dialogSecondary" onClick={() => { this.onClose(); }} color="primary">
Cancel Cancel
</Button> </Button>
<Button variant="dialogPrimary" onClick={() => { this.onOK(); }} color="secondary" disabled={!this.state.hasSelection} > <Button variant="dialogPrimary" onClick={() => { this.onOK(); }} color="secondary"
disabled={(!this.state.hasSelection) || this.state.isProtected} >
OK OK
</Button> </Button>
</DialogActions> </DialogActions>
+4
View File
@@ -21,6 +21,8 @@
export default class FilePropertyDirectoryTree { export default class FilePropertyDirectoryTree {
deserialize(input: any) : FilePropertyDirectoryTree { deserialize(input: any) : FilePropertyDirectoryTree {
this.directoryName = input.directoryName; this.directoryName = input.directoryName;
this.displayName = input.displayName;
this.isProtected = input.isProtected;
this.children = FilePropertyDirectoryTree.deserialize_array(input.children); this.children = FilePropertyDirectoryTree.deserialize_array(input.children);
return this; return this;
} }
@@ -35,5 +37,7 @@ export default class FilePropertyDirectoryTree {
directoryName: string = ""; directoryName: string = "";
displayName: string = "";
isProtected: boolean = false;
children: FilePropertyDirectoryTree[] = []; children: FilePropertyDirectoryTree[] = [];
} }
+15 -3
View File
@@ -246,6 +246,8 @@ export class UiFileProperty {
this.index = input.index; this.index = input.index;
this.portGroup = input.portGroup; this.portGroup = input.portGroup;
this.resourceDirectory = input.resourceDirectory ?? ""; this.resourceDirectory = input.resourceDirectory ?? "";
this.modDirectories = input.modDirectories;
this.useLegacyModDirectory = input.useLegacyModDirectory;
return this; return this;
} }
static deserialize_array(input: any): UiFileProperty[] { static deserialize_array(input: any): UiFileProperty[] {
@@ -297,6 +299,8 @@ export class UiFileProperty {
index: number = -1; index: number = -1;
portGroup: string = ""; portGroup: string = "";
resourceDirectory: string = ""; resourceDirectory: string = "";
modDirectories: string[] = [];
useLegacyModDirectory: boolean = false;
}; };
export class Lv2Plugin implements Deserializable<Lv2Plugin> { export class Lv2Plugin implements Deserializable<Lv2Plugin> {
@@ -436,6 +440,7 @@ export enum ControlType {
OnOffSwitch, OnOffSwitch,
ABSwitch, ABSwitch,
Select, Select,
Trigger,
Tuner, Tuner,
Vu, Vu,
@@ -444,7 +449,7 @@ export enum ControlType {
} }
export class UiControl implements Deserializable<UiControl> { export class UiControl implements Deserializable<UiControl> {
deserialize(input: any): UiControl { deserialize(input: any): UiControl {
this.symbol = input.symbol; this.symbol = input.symbol;
this.name = input.name; this.name = input.name;
this.index = input.index; this.index = input.index;
@@ -458,7 +463,7 @@ export class UiControl implements Deserializable<UiControl> {
this.integer_property = input.integer_property; this.integer_property = input.integer_property;
this.enumeration_property = input.enumeration_property; this.enumeration_property = input.enumeration_property;
this.toggled_property = input.toggled_property; this.toggled_property = input.toggled_property;
this.trigger = input.trigger; this.trigger_property = input.trigger_property;
this.not_on_gui = input.not_on_gui; this.not_on_gui = input.not_on_gui;
this.scale_points = ScalePoint.deserialize_array(input.scale_points); this.scale_points = ScalePoint.deserialize_array(input.scale_points);
this.port_group = input.port_group; this.port_group = input.port_group;
@@ -499,6 +504,10 @@ export class UiControl implements Deserializable<UiControl> {
this.controlType = ControlType.OnOffSwitch; this.controlType = ControlType.OnOffSwitch;
} }
} }
if (this.is_input && this.trigger_property)
{
this.controlType = ControlType.Trigger;
}
return this; return this;
} }
applyProperties(properties: Partial<UiControl>): UiControl { applyProperties(properties: Partial<UiControl>): UiControl {
@@ -546,7 +555,7 @@ export class UiControl implements Deserializable<UiControl> {
range_steps: number = 0; range_steps: number = 0;
integer_property: boolean = false; integer_property: boolean = false;
enumeration_property: boolean = false; enumeration_property: boolean = false;
trigger: boolean = false; trigger_property: boolean = false;
not_on_gui: boolean = false; not_on_gui: boolean = false;
toggled_property: boolean = false; toggled_property: boolean = false;
scale_points: ScalePoint[] = []; scale_points: ScalePoint[] = [];
@@ -590,6 +599,9 @@ export class UiControl implements Deserializable<UiControl> {
isSelect(): boolean { isSelect(): boolean {
return this.controlType === ControlType.Select; return this.controlType === ControlType.Select;
} }
isTrigger(): boolean {
return this.controlType === ControlType.Trigger;
}
isOutputSelect(): boolean { isOutputSelect(): boolean {
return !this.is_input && this.controlType === ControlType.OutputSelect; return !this.is_input && this.controlType === ControlType.OutputSelect;
} }
+1 -1
View File
@@ -160,7 +160,7 @@ const MidiBindingView =
isBinaryControl = (port.isAbToggle() || port.isOnOffSwitch()); isBinaryControl = (port.isAbToggle() || port.isOnOffSwitch());
if (midiBinding.bindingType !== MidiBinding.BINDING_TYPE_NONE) { if (midiBinding.bindingType !== MidiBinding.BINDING_TYPE_NONE) {
canLatch = isBinaryControl; canLatch = isBinaryControl;
canTrigger = port.trigger; canTrigger = port.trigger_property;
showLinearControlTypeSelect = !(canLatch || canTrigger); showLinearControlTypeSelect = !(canLatch || canTrigger);
showLinearRange = showLinearControlTypeSelect && midiBinding.linearControlType === MidiBinding.LINEAR_CONTROL_TYPE; showLinearRange = showLinearControlTypeSelect && midiBinding.linearControlType === MidiBinding.LINEAR_CONTROL_TYPE;
canRotaryScale = showLinearControlTypeSelect && midiBinding.linearControlType === MidiBinding.CIRCULAR_CONTROL_TYPE; canRotaryScale = showLinearControlTypeSelect && midiBinding.linearControlType === MidiBinding.CIRCULAR_CONTROL_TYPE;
+60
View File
@@ -0,0 +1,60 @@
// Copyright (c) 2022 Robin 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.
export interface ModDirectory {
modType: string;
pipedalPath: string;
displayName: string;
defaultFileExtensions: string[];
};
export const modDirectories: ModDirectory[] = [
{modType: "audioloop", pipedalPath: "shared/audio/Loops",displayName: "Loops", defaultFileExtensions: ["audio/*"]}, // Audio Loops, meant to be used for looper-style plugins
//"audiorecording","shared/audio/Audio Recordings", defaultFileExtensions: ["audio/*"]}, : Audio Recordings, triggered by plugins and stored in the unit
{modType: "audiosample", pipedalPath: "shared/audio/Samples", displayName: "Samples", defaultFileExtensions: ["audio/*"]}, // One-shot Audio Samples, meant to be used for sampler-style plugins
{modType: "audiotrack", pipedalPath: "shared/audio/Tracks",displayName: "Tracks", defaultFileExtensions: ["audio/*"]}, // Audio Tracks, meant to be used as full-performance/song or backtrack
{modType: "cabsim", pipedalPath: "CabIR", displayName: "Cab IRs", defaultFileExtensions: ["audio/*"]}, // Speaker Cabinets, meant as small IR audio files
/// - h2drumkit: Hydrogen Drumkits, must use h2drumkit file extension
{modType: "ir", pipedalPath: "ReverbImpulseFiles", displayName: "Impulse Responses", defaultFileExtensions: ["audio/*"]}, // Impulse Responses
{modType: "midiclip", pipedalPath: "shared/midiClips",displayName: "MIDI Clips", defaultFileExtensions: [".mid", ".midi"]}, // MIDI Clips, to be used in sync with host tempo, must have mid or midi file extension
{modType: "midisong", pipedalPath: "shared/midiSongs", displayName: "MIDI Songs", defaultFileExtensions: [".mid", ".midi"]}, // MIDI Songs, meant to be used as full-performance/song or backtrack
{modType: "sf2", pipedalPath: "shared/sf2",displayName: "Sound Fonts", defaultFileExtensions: ["sf2", "sf3"]}, // SF2 Instruments, must have sf2 or sf3 file extension
{modType: "sfz", pipedalPath: "shared/sfz",displayName: "Sfz Files", defaultFileExtensions: ["sfz"]}, // SFZ Instruments, must have sfz file extension
// extensions observed in the field.
{modType: "audio", pipedalPath: "shared/audio", displayName: "Audio", defaultFileExtensions: ["audio/*"]}, // all audio files (Ratatoille)
{modType: "nammodel", pipedalPath: "NeuralAmpModels", displayName: "Neural Amp Models", defaultFileExtensions: [".nam"]}, // Ratatoille, Mike's NAM.
{modType: "aidadspmodel", pipedalPath: "shared/aidaaix", displayName: "AIDA IAX Models", defaultFileExtensions: [".json", ".aidaiax"]}, // Ratatoille
{modType: "mlmodel", pipedalPath: "ToobMlModels", displayName: "ML Models", defaultFileExtensions: [".json"]}, //
];
export function getModDirectory(modFileType: string): ModDirectory | undefined {
for (let i = 0; i < modDirectories.length; ++i)
{
if (modDirectories[i].modType === modFileType)
{
return modDirectories[i];
}
}
return undefined;
}
+28 -6
View File
@@ -73,8 +73,20 @@ export enum ReconnectReason {
export type ControlValueChangedHandler = (key: string, value: number) => void; export type ControlValueChangedHandler = (key: string, value: number) => void;
export interface FileEntry { export interface FileEntry {
filename: string; pathname: string;
displayName: string;
isDirectory: boolean; isDirectory: boolean;
isProtected: boolean;
};
export interface BreadcrumbEntry {
pathname: string;
displayName: string;
};
export class FileRequestResult {
files: FileEntry[] = [];
isProtected: boolean = false;
breadcrumbs: BreadcrumbEntry[] = [];
}; };
export type PluginPresetsChangedHandler = (pluginUri: string) => void; export type PluginPresetsChangedHandler = (pluginUri: string) => void;
@@ -1989,9 +2001,9 @@ export class PiPedalModel //implements PiPedalModel
return nullCast(this.webSocket) return nullCast(this.webSocket)
.request<string[]>('requestFileList', piPedalFileProperty); .request<string[]>('requestFileList', piPedalFileProperty);
} }
requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise<FileEntry[]> { requestFileList2(relativeDirectoryPath: string, piPedalFileProperty: UiFileProperty): Promise<FileRequestResult> {
return nullCast(this.webSocket) return nullCast(this.webSocket)
.request<FileEntry[]>('requestFileList2', .request<FileRequestResult>('requestFileList2',
{ relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty } { relativePath: relativeDirectoryPath, fileProperty: piPedalFileProperty }
); );
} }
@@ -2498,7 +2510,7 @@ export class PiPedalModel //implements PiPedalModel
link.click(); link.click();
} }
uploadFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> { uploadUserFile(uploadPage: string, file: File, contentType: string = "application/octet-stream", abortController?: AbortController): Promise<string> {
let result = new Promise<string>((resolve, reject) => { let result = new Promise<string>((resolve, reject) => {
try { try {
if (file.size > this.maxFileUploadSize) { if (file.size > this.maxFileUploadSize) {
@@ -2539,10 +2551,20 @@ export class PiPedalModel //implements PiPedalModel
} }
}) })
.then((json) => { .then((json) => {
resolve(json as string); let response = json as {errorMessage: string, path: string};
if (response.errorMessage !== "")
{
throw new Error(response.errorMessage);
}
resolve(response.path);
}) })
.catch((error) => { .catch((error) => {
reject("Upload failed. " + error); if (error instanceof Error)
{
reject("Upload failed. " + (error as Error).message);
} else {
reject("Upload failed. " + error);
}
}) })
; ;
} catch (error) { } catch (error) {
+127 -117
View File
@@ -18,6 +18,7 @@
// 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, { TouchEvent, PointerEvent, ReactNode, Component, SyntheticEvent } from 'react'; import React, { TouchEvent, PointerEvent, ReactNode, Component, SyntheticEvent } from 'react';
import Button from '@mui/material/Button';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import { WithStyles } from '@mui/styles'; import { WithStyles } from '@mui/styles';
import createStyles from '@mui/styles/createStyles'; import createStyles from '@mui/styles/createStyles';
@@ -27,10 +28,11 @@ import Typography from '@mui/material/Typography';
import Input from '@mui/material/Input'; import Input from '@mui/material/Input';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import Switch from '@mui/material/Switch'; import Switch from '@mui/material/Switch';
import Utility, {nullCast} from './Utility'; import Utility, { nullCast } from './Utility';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import {PiPedalModel,PiPedalModelFactory} from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import {ReactComponent as DialIcon} from './svg/fx_dial.svg'; import { ReactComponent as DialIcon } from './svg/fx_dial.svg';
import { isDarkMode } from './DarkMode';
const MIN_ANGLE = -135; const MIN_ANGLE = -135;
const MAX_ANGLE = 135; const MAX_ANGLE = 135;
@@ -94,8 +96,7 @@ type PluginControlState = {
const PluginControl = const PluginControl =
withStyles(styles, { withTheme: true })( withStyles(styles, { withTheme: true })(
class extends Component<PluginControlProps, PluginControlState> class extends Component<PluginControlProps, PluginControlState> {
{
frameRef: React.RefObject<HTMLDivElement>; frameRef: React.RefObject<HTMLDivElement>;
imgRef: React.RefObject<SVGSVGElement>; imgRef: React.RefObject<SVGSVGElement>;
@@ -140,23 +141,18 @@ const PluginControl =
return Utility.needsZoomedControls(); return Utility.needsZoomedControls();
} }
showZoomedControl() showZoomedControl() {
{ if (this.props.uiControl && this.frameRef.current) {
if (this.props.uiControl && this.frameRef.current) this.model.zoomUiControl(this.frameRef.current, this.props.instanceId, this.props.uiControl);
{
this.model.zoomUiControl(this.frameRef.current,this.props.instanceId,this.props.uiControl);
} }
} }
hideZoomedControl() hideZoomedControl() {
{ if (this.frameRef.current && this.model.zoomedUiControl.get()?.source === this.frameRef.current) {
if (this.frameRef.current && this.model.zoomedUiControl.get()?.source === this.frameRef.current)
{
this.model.clearZoomedControl(); this.model.clearZoomedControl();
} }
} }
componentWillUnmount() componentWillUnmount() {
{
this.hideZoomedControl(); this.hideZoomedControl();
} }
inputChanged: boolean = false; inputChanged: boolean = false;
@@ -172,12 +168,11 @@ const PluginControl =
} }
onInputFocus(event: SyntheticEvent): void { onInputFocus(event: SyntheticEvent): void {
this.displayValueRef.current!.style.display = "none"; this.displayValueRef.current!.style.display = "none";
if (Utility.hasIMEKeyboard()) if (Utility.hasIMEKeyboard()) {
{
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
this.inputRef.current?.blur(); this.inputRef.current?.blur();
this.props.requestIMEEdit(nullCast(this.props.uiControl),this.props.value) this.props.requestIMEEdit(nullCast(this.props.uiControl), this.props.value)
} }
} }
onInputKeyPress(e: any): void { onInputKeyPress(e: any): void {
@@ -221,7 +216,7 @@ const PluginControl =
// clamp and quantize. // clamp and quantize.
let range = this.valueToRange(result); let range = this.valueToRange(result);
result = this.rangeToValue(range); result = this.rangeToValue(range);
let displayVal = this.props.uiControl?.formatShortValue(result)??""; let displayVal = this.props.uiControl?.formatShortValue(result) ?? "";
if (event.currentTarget) { if (event.currentTarget) {
event.currentTarget.value = displayVal; event.currentTarget.value = displayVal;
} }
@@ -270,11 +265,9 @@ const PluginControl =
capturedPointers: number[] = []; capturedPointers: number[] = [];
onBodyPointerDownCapture(e_: any): any onBodyPointerDownCapture(e_: any): any {
{
let e = e_ as PointerEvent; let e = e_ as PointerEvent;
if (this.isExtraTouch(e)) if (this.isExtraTouch(e)) {
{
this.isTap = false; this.isTap = false;
this.captureElement!.setPointerCapture(e.pointerId); this.captureElement!.setPointerCapture(e.pointerId);
this.capturedPointers.push(e.pointerId); this.capturedPointers.push(e.pointerId);
@@ -305,10 +298,8 @@ const PluginControl =
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
if (this.isTouchDevice()) if (this.isTouchDevice()) {
{ if (this.props.uiControl?.isDial() ?? false) {
if (this.props.uiControl?.isDial()??false)
{
this.isTap = false; this.isTap = false;
this.showZoomedControl(); this.showZoomedControl();
return; return;
@@ -318,8 +309,7 @@ const PluginControl =
++this.pointersDown; ++this.pointersDown;
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 = Date.now();
} else { } else {
@@ -340,8 +330,8 @@ const PluginControl =
this.captureElement = img; this.captureElement = img;
document.body.addEventListener( document.body.addEventListener(
"pointerdown", "pointerdown",
this.onBodyPointerDownCapture,true this.onBodyPointerDownCapture, true
); );
img.setPointerCapture(e.pointerId); img.setPointerCapture(e.pointerId);
if (img.style) { if (img.style) {
@@ -350,11 +340,10 @@ const PluginControl =
} }
} else { } else {
if (this.isExtraTouch(e)) if (this.isExtraTouch(e)) {
{
++this.pointersDown; ++this.pointersDown;
this.isTap = false; this.isTap = false;
} }
} }
} }
@@ -367,7 +356,7 @@ const PluginControl =
if (this.isCapturedPointer(e)) { if (this.isCapturedPointer(e)) {
--this.pointersDown; --this.pointersDown;
this.isTap = false; this.isTap = false;
this.releaseCapture(e); this.releaseCapture(e);
} }
@@ -378,21 +367,16 @@ const PluginControl =
let ultraHigh = false; let ultraHigh = false;
let high = false; let high = false;
if (e.ctrlKey) if (e.ctrlKey) {
{
ultraHigh = true; ultraHigh = true;
} }
if (e.shiftKey) if (e.shiftKey) {
{
high = true; high = true;
} }
if (e.pointerType === "touch") if (e.pointerType === "touch") {
{ if (this.pointersDown >= 3) {
if (this.pointersDown >= 3)
{
ultraHigh = true; ultraHigh = true;
} else if (this.pointersDown === 2) } else if (this.pointersDown === 2) {
{
high = true; high = true;
} }
} }
@@ -411,28 +395,24 @@ const PluginControl =
} }
private lastTapMs = 0; private lastTapMs = 0;
resetToDefaultValue(uiControl: UiControl): void resetToDefaultValue(uiControl: UiControl): void {
{
let value = uiControl.default_value; let value = uiControl.default_value;
this.model.setPedalboardControl(this.props.instanceId,uiControl.symbol,value); this.model.setPedalboardControl(this.props.instanceId, uiControl.symbol, value);
} }
onPointerDoubleTap() { onPointerDoubleTap() {
let uiControl = this.props.uiControl; let uiControl = this.props.uiControl;
if (uiControl) if (uiControl) {
{ if (uiControl.isDial()) {
if (uiControl.isDial())
{
this.resetToDefaultValue(uiControl); this.resetToDefaultValue(uiControl);
} }
} }
} }
onPointerTap() { onPointerTap() {
let tapTime = Date.now(); let tapTime = Date.now();
let dT = tapTime-this.lastTapMs; let dT = tapTime - this.lastTapMs;
this.lastTapMs = tapTime; this.lastTapMs = tapTime;
if (dT < 500) if (dT < 500) {
{
this.onPointerDoubleTap(); this.onPointerDoubleTap();
} }
} }
@@ -442,32 +422,29 @@ const PluginControl =
if (this.isCapturedPointer(e)) { if (this.isCapturedPointer(e)) {
--this.pointersDown; --this.pointersDown;
e.preventDefault(); e.preventDefault();
let dRange = this.updateRange(e) let dRange = this.updateRange(e)
this.previewRange(dRange, true); this.previewRange(dRange, true);
this.releaseCapture(e); this.releaseCapture(e);
if (this.isTap) if (this.isTap) {
{ let ms = Date.now() - this.tapStartMs;
let ms = Date.now()-this.tapStartMs; if (ms < 200) {
if (ms < 200)
{
this.onPointerTap(); this.onPointerTap();
} }
} }
} else { } else {
--this.pointersDown; --this.pointersDown;
} }
} }
releaseCapture(e: PointerEvent<SVGSVGElement>) releaseCapture(e: PointerEvent<SVGSVGElement>) {
{
let img = this.imgRef.current; let img = this.imgRef.current;
if (img && img.style) { if (img && img.style) {
img.releasePointerCapture(e.pointerId); img.releasePointerCapture(e.pointerId);
img.style.opacity = "" + DEFAULT_OPACITY; img.style.opacity = "" + DEFAULT_OPACITY;
@@ -480,14 +457,14 @@ const PluginControl =
} }
document.body.removeEventListener( document.body.removeEventListener(
"pointerdown", "pointerdown",
this.onBodyPointerDownCapture,true this.onBodyPointerDownCapture, true
); );
this.mouseDown = false; this.mouseDown = false;
} }
clickSlop() { clickSlop() {
return 3.5; // maybe larger on touch devices. return 3.5; // maybe larger on touch devices.
} }
onPointerMove(e: PointerEvent<SVGSVGElement>): void { onPointerMove(e: PointerEvent<SVGSVGElement>): void {
@@ -499,15 +476,27 @@ const PluginControl =
let x = e.clientX; let x = e.clientX;
let y = e.clientY; let y = e.clientY;
let dx = x - this.startX; let dx = x - this.startX;
let dy = y-this.startY; let dy = y - this.startY;
let distance = Math.sqrt(dx*dx+dy*dy); let distance = Math.sqrt(dx * dx + dy * dy);
if (distance >= this.clickSlop()) if (distance >= this.clickSlop()) {
{
this.isTap = false; this.isTap = false;
} }
} }
} }
handleTriggerMouseDown() {
let uiControl = this.props.uiControl;
if (uiControl)
{
this.model.setPedalboardControl(this.props.instanceId,uiControl.symbol,1);
}
}
handleTriggerMouseUp() {
let uiControl = this.props.uiControl;
if (uiControl)
{
this.model.setPedalboardControl(this.props.instanceId,uiControl.symbol,0);
}
}
previewInputValue(value: number, commitValue: boolean) { previewInputValue(value: number, commitValue: boolean) {
let range = this.valueToRange(value); let range = this.valueToRange(value);
value = this.rangeToValue(range); value = this.rangeToValue(range);
@@ -547,13 +536,12 @@ const PluginControl =
} }
let inputElement = this.inputRef.current; let inputElement = this.inputRef.current;
if (inputElement) { if (inputElement) {
let v = this.props.uiControl?.formatShortValue(value)??""; let v = this.props.uiControl?.formatShortValue(value) ?? "";
inputElement.value = v; inputElement.value = v;
} }
let displayValue = this.displayValueRef.current; let displayValue = this.displayValueRef.current;
if (displayValue) if (displayValue) {
{ let v = this.formatDisplayValue(this.props.uiControl, value);
let v = this.formatDisplayValue(this.props.uiControl,value);
displayValue.childNodes[0].textContent = v; displayValue.childNodes[0].textContent = v;
} }
let selectElement = this.selectRef.current; let selectElement = this.selectRef.current;
@@ -577,9 +565,9 @@ const PluginControl =
} }
onSelectChanged(e: any, value: any) { onSelectChanged(e: any, value: any) {
let target = e.target; let target = e.target;
setTimeout(()=> { setTimeout(() => {
this.props.onChange(target.value); this.props.onChange(target.value);
},0); }, 0);
} }
@@ -610,7 +598,7 @@ const PluginControl =
); );
} else { } else {
return ( return (
<Select variant="standard" <Select variant="standard"
ref={this.selectRef} ref={this.selectRef}
value={control.clampSelectValue(value)} value={control.clampSelectValue(value)}
onChange={this.onSelectChanged} onChange={this.onSelectChanged}
@@ -619,7 +607,7 @@ const PluginControl =
id: 'id' + control.symbol, id: 'id' + control.symbol,
style: { fontSize: FONT_SIZE } style: { fontSize: FONT_SIZE }
}} }}
style={{ marginLeft: 4, marginRight: 4, width: 140,fontSize: 14 }} style={{ marginLeft: 4, marginRight: 4, width: 140, fontSize: 14 }}
> >
{control.scale_points.map((scale_point: ScalePoint) => ( {control.scale_points.map((scale_point: ScalePoint) => (
<MenuItem key={scale_point.value} value={scale_point.value}>{scale_point.label}</MenuItem> <MenuItem key={scale_point.value} value={scale_point.value}>{scale_point.label}</MenuItem>
@@ -645,11 +633,10 @@ const PluginControl =
value = Math.round(value); value = Math.round(value);
} }
let range: number; let range: number;
if (uiControl.is_logarithmic) if (uiControl.is_logarithmic) {
{ range = Math.log(value / uiControl.min_value) / Math.log(uiControl.max_value / uiControl.min_value);
range = Math.log(value/uiControl.min_value)/Math.log(uiControl.max_value/uiControl.min_value); if (uiControl.range_steps > 1) {
if (uiControl.range_steps > 1) { range = Math.round(range * (uiControl.range_steps - 1)) / (uiControl.range_steps - 1);
range = Math.round(range*(uiControl.range_steps-1))/(uiControl.range_steps-1);
} }
} else { } else {
range = (value - uiControl.min_value) / (uiControl.max_value - uiControl.min_value); range = (value - uiControl.min_value) / (uiControl.max_value - uiControl.min_value);
@@ -666,17 +653,14 @@ const PluginControl =
let uiControl = this.props.uiControl; let uiControl = this.props.uiControl;
if (uiControl) { if (uiControl) {
if (uiControl.range_steps > 1) { if (uiControl.range_steps > 1) {
range = Math.round(range * (uiControl.range_steps-1)) / (uiControl.range_steps-1); range = Math.round(range * (uiControl.range_steps - 1)) / (uiControl.range_steps - 1);
} }
let value: number; let value: number;
if (uiControl.min_value === uiControl.max_value) if (uiControl.min_value === uiControl.max_value) {
{
value = uiControl.min_value; value = uiControl.min_value;
} else } else {
{ if (uiControl.is_logarithmic) {
if (uiControl.is_logarithmic) value = uiControl.min_value * Math.pow(uiControl.max_value / uiControl.min_value, range);
{
value = uiControl.min_value*Math.pow(uiControl.max_value/uiControl.min_value,range);
} else { } else {
value = range * (uiControl.max_value - uiControl.min_value) + uiControl.min_value; value = range * (uiControl.max_value - uiControl.min_value) + uiControl.min_value;
} }
@@ -724,45 +708,71 @@ const PluginControl =
let isSelect = control.isSelect(); let isSelect = control.isSelect();
let isAbSwitch = control.isAbToggle(); let isAbSwitch = control.isAbToggle();
let isOnOffSwitch = control.isOnOffSwitch(); let isOnOffSwitch = control.isOnOffSwitch();
let isTrigger = control.isTrigger();
if (isAbSwitch) { if (isAbSwitch) {
switchText = control.scale_points[0].value === value ? control.scale_points[0].label : control.scale_points[1].label; switchText = control.scale_points[0].value === value ? control.scale_points[0].label : control.scale_points[1].label;
} }
let item_width = isSelect ? 160 : 80; let item_width: number | undefined = isSelect ? 160 : 80;
if (isTrigger)
{
item_width = undefined;
}
return ( return (
<div ref={this.frameRef} style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-start", width: item_width, margin: 8 }}> <div ref={this.frameRef} style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "flex-start", width: item_width, margin: 8 }}>
{/* TITLE SECTION */} {/* TITLE SECTION */}
<div style={{ flex: "0 0 auto", width: "100%", marginBottom: 8,marginLeft: isSelect? 16: 0, marginRight: 0 }}> <div style={{ flex: "0 0 auto", alignSelf:"stretch", marginBottom: 8, marginLeft: isSelect ? 16 : 0, marginRight: 0 }}>
<Typography variant="caption" display="block" noWrap style={{ <Typography variant="caption" display="block" noWrap style={{
width: "100%", width: "100%",
textAlign: isSelect ? "left" : "center" textAlign: isSelect ? "left" : "center"
}}> {control.name}</Typography> }}> {isTrigger ? "\u00A0" : control.name}</Typography>
</div> </div>
{/* CONTROL SECTION */} {/* CONTROL SECTION */}
<div style={{ flex: "0 0 auto" }}> <div style={{ flex: "0 0 auto" }}>
{(isSelect || isAbSwitch || isOnOffSwitch) ? ( {isTrigger ? (
this.makeSelect(control, value) <Button variant="contained" color="primary" size="small"
) : ( onMouseDown={
<div style={{ flex: "0 1 auto" }}> (evt)=> { this.handleTriggerMouseDown(); }
<DialIcon ref={this.imgRef} }
style={{ overscrollBehavior: "none", touchAction: "none", fill: dialColor, onMouseUp={
width: 36, height: 36, opacity: DEFAULT_OPACITY, transform: this.getRotationTransform() }} (evt)=> { this.handleTriggerMouseUp(); }
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove} }
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} onPointerMoveCapture={this.onPointerMove} onDrag={this.onDrag} style={{
textTransform: "none",
/> background: (isDarkMode() ? "#6750A4" : undefined),
</div> marginLeft: 8, marginRight: 8,minWidth:60
}}
>
{control.name}
</Button>
) )
: ((isSelect || isAbSwitch || isOnOffSwitch) ? (
this.makeSelect(control, value)
) : (
<div style={{ flex: "0 1 auto" }}>
<DialIcon ref={this.imgRef}
style={{
overscrollBehavior: "none", touchAction: "none", fill: dialColor,
width: 36, height: 36, opacity: DEFAULT_OPACITY, transform: this.getRotationTransform()
}}
onTouchStart={this.onTouchStart} onTouchMove={this.onTouchMove}
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} onPointerMoveCapture={this.onPointerMove} onDrag={this.onDrag}
/>
</div>
)
)
} }
</div> </div>
{/* LABEL/EDIT SECTION*/} {/* LABEL/EDIT SECTION*/}
<div style={{ flex: "0 0 auto", position: "relative", width: 60 }}> <div style={{ flex: "0 0 auto", position: "relative", width: 60 }}>
{(!(isSelect || isOnOffSwitch)) && {(!(isSelect || isOnOffSwitch || isTrigger)) &&
( (
(isAbSwitch) ? ( (isAbSwitch) ? (
<Typography variant="caption" display="block" noWrap style={{ <Typography variant="caption" display="block" noWrap style={{
@@ -770,7 +780,7 @@ const PluginControl =
}}> {switchText} </Typography> }}> {switchText} </Typography>
) : ( ) : (
<div> <div>
<Input key={value} <Input key={value}
type="number" type="number"
defaultValue={control.formatShortValue(value)} defaultValue={control.formatShortValue(value)}
error={this.state.error} error={this.state.error}
@@ -785,10 +795,10 @@ const PluginControl =
onKeyPress={this.onInputKeyPress} /> onKeyPress={this.onInputKeyPress} />
<div className={classes.displayValue} ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }} > <div className={classes.displayValue} ref={this.displayValueRef} onClick={(e) => { this.inputRef.current!.focus(); }} >
<Typography noWrap color="inherit" style={{ fontSize: "12.8px", paddingTop:4, paddingBottom: 6 }} <Typography noWrap color="inherit" style={{ fontSize: "12.8px", paddingTop: 4, paddingBottom: 6 }}
> >
{this.formatDisplayValue(control, value)}</Typography> {this.formatDisplayValue(control, value)}</Typography>
</div> </div>
</div> </div>
) )
+12
View File
@@ -37,6 +37,7 @@ export const SplitTypeControl: UiControl = new UiControl().deserialize({
enumeration_property: true, enumeration_property: true,
not_on_gui: false, not_on_gui: false,
toggled_property: false, toggled_property: false,
trigger_property: false,
scale_points: ScalePoint.deserialize_array( scale_points: ScalePoint.deserialize_array(
[ [
{ value: 0, label: "A/B" }, { value: 0, label: "A/B" },
@@ -60,6 +61,7 @@ export const SplitAbControl: UiControl = new UiControl().deserialize({
enumeration_property: true, enumeration_property: true,
not_on_gui: false, not_on_gui: false,
toggled_property: false, toggled_property: false,
trigger_property: false,
scale_points: ScalePoint.deserialize_array( scale_points: ScalePoint.deserialize_array(
[ [
{ value: 0, label: "A" }, { value: 0, label: "A" },
@@ -83,6 +85,8 @@ export const SplitMixControl: UiControl = new UiControl().deserialize({
enumeration_property: false, enumeration_property: false,
not_on_gui: false, not_on_gui: false,
toggled_property: false, toggled_property: false,
trigger_property: false,
scale_points: [] scale_points: []
}); });
@@ -100,6 +104,8 @@ export const SplitPanLeftControl: UiControl = new UiControl().deserialize({
enumeration_property: false, enumeration_property: false,
not_on_gui: false, not_on_gui: false,
toggled_property: false, toggled_property: false,
trigger_property: false,
scale_points: [] scale_points: []
}); });
@@ -117,6 +123,8 @@ export const SplitVolLeftControl: UiControl = new UiControl().deserialize({
enumeration_property: false, enumeration_property: false,
not_on_gui: false, not_on_gui: false,
toggled_property: false, toggled_property: false,
trigger_property: false,
units: Units.db, units: Units.db,
scale_points: [ scale_points: [
new ScalePoint().deserialize({ new ScalePoint().deserialize({
@@ -141,6 +149,8 @@ export const SplitPanRightControl: UiControl = new UiControl().deserialize({
enumeration_property: false, enumeration_property: false,
not_on_gui: false, not_on_gui: false,
toggled_property: false, toggled_property: false,
trigger_property: false,
scale_points: [] scale_points: []
}); });
@@ -158,6 +168,8 @@ export const SplitVolRightControl: UiControl = new UiControl().deserialize({
enumeration_property: false, enumeration_property: false,
not_on_gui: false, not_on_gui: false,
toggled_property: false, toggled_property: false,
trigger_property: false,
units: Units.db, units: Units.db,
scale_points: [ scale_points: [
new ScalePoint().deserialize({ new ScalePoint().deserialize({
+1 -1
View File
@@ -208,7 +208,7 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
{ {
throw new Error("Invalid file extension."); throw new Error("Invalid file extension.");
} }
let filename = await this.model.uploadFile( let filename = await this.model.uploadUserFile(
this.props.uploadPage, this.props.uploadPage,
this.uploadList[i].file, this.uploadList[i].file,
"application/octet-stream", "application/octet-stream",
+2
View File
@@ -157,6 +157,7 @@ else()
endif() endif()
set (PIPEDAL_SOURCES set (PIPEDAL_SOURCES
ModFileTypes.cpp ModFileTypes.hpp
PatchPropertyWriter.hpp PatchPropertyWriter.hpp
PresetBundle.cpp PresetBundle.hpp PresetBundle.cpp PresetBundle.hpp
RealtimeMidiEventType.hpp RealtimeMidiEventType.hpp
@@ -637,6 +638,7 @@ add_executable(pipedalconfig
CommandLineParser.hpp CommandLineParser.hpp
PiPedalException.hpp PiPedalException.hpp
ConfigMain.cpp ConfigMain.cpp
ModFileTypes.cpp ModFileTypes.hpp
PiPedalConfiguration.hpp PiPedalConfiguration.cpp PiPedalConfiguration.hpp PiPedalConfiguration.cpp
JackServerSettings.hpp JackServerSettings.cpp JackServerSettings.hpp JackServerSettings.cpp
SystemConfigFile.hpp SystemConfigFile.cpp SystemConfigFile.hpp SystemConfigFile.cpp
+4
View File
@@ -23,6 +23,7 @@
#include <unistd.h> #include <unistd.h>
#include "CommandLineParser.hpp" #include "CommandLineParser.hpp"
#include "SystemConfigFile.hpp" #include "SystemConfigFile.hpp"
#include "ModFileTypes.hpp"
#include <filesystem> #include <filesystem>
#include <stdlib.h> #include <stdlib.h>
@@ -951,6 +952,7 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress)
} }
try try
{ {
// apply policy changes we dropped into the polkit configuration files (allows pipedal_d to use NetworkManager dbus apis). // apply policy changes we dropped into the polkit configuration files (allows pipedal_d to use NetworkManager dbus apis).
silentSysExec(SYSTEMCTL_BIN " restart polkit.service"); silentSysExec(SYSTEMCTL_BIN " restart polkit.service");
@@ -1158,6 +1160,8 @@ void Install(const fs::path &programPrefix, const std::string endpointAddress)
sysExec(SYSTEMCTL_BIN " daemon-reload"); sysExec(SYSTEMCTL_BIN " daemon-reload");
FixPermissions(); FixPermissions();
ModFileTypes::CreateDefaultDirectories("/var/pipedal/audio_uploads");
StopService(false); StopService(false);
AvahiInstall(); AvahiInstall();
+15 -2
View File
@@ -22,7 +22,20 @@
using namespace pipedal; using namespace pipedal;
JSON_MAP_BEGIN(FileEntry) JSON_MAP_BEGIN(FileEntry)
JSON_MAP_REFERENCE(FileEntry,filename) JSON_MAP_REFERENCE(FileEntry,pathname)
JSON_MAP_REFERENCE(FileEntry,displayName)
JSON_MAP_REFERENCE(FileEntry,isProtected)
JSON_MAP_REFERENCE(FileEntry,isDirectory) JSON_MAP_REFERENCE(FileEntry,isDirectory)
JSON_MAP_END()
JSON_MAP_BEGIN(BreadcrumbEntry)
JSON_MAP_REFERENCE(BreadcrumbEntry,pathname)
JSON_MAP_REFERENCE(BreadcrumbEntry,displayName)
JSON_MAP_END()
JSON_MAP_BEGIN(FileRequestResult)
JSON_MAP_REFERENCE(FileRequestResult,files)
JSON_MAP_REFERENCE(FileRequestResult,isProtected)
JSON_MAP_REFERENCE(FileRequestResult,breadcrumbs)
JSON_MAP_END() JSON_MAP_END()
+21 -3
View File
@@ -26,15 +26,33 @@ namespace pipedal {
class FileEntry { class FileEntry {
public: public:
FileEntry() { } FileEntry() { }
FileEntry(std::string filename,bool isDirectory) FileEntry(const std::string&pathname,const std::string &displayName,bool isDirectory, bool isProtected = false)
:filename_(filename), isDirectory_(isDirectory) :pathname_(pathname), displayName_(displayName), isDirectory_(isDirectory),isProtected_(isProtected)
{ {
} }
std::string filename_; std::string pathname_;
std::string displayName_;
bool isDirectory_ = false; bool isDirectory_ = false;
bool isProtected_ = false;
DECLARE_JSON_MAP(FileEntry); DECLARE_JSON_MAP(FileEntry);
}; };
class BreadcrumbEntry {
public:
std::string pathname_;
std::string displayName_;
DECLARE_JSON_MAP(BreadcrumbEntry);
};
class FileRequestResult {
public:
std::vector<FileEntry> files_;
bool isProtected_ = false;
std::vector<BreadcrumbEntry> breadcrumbs_;
DECLARE_JSON_MAP(FileRequestResult);
};
} }
+11 -1
View File
@@ -20,16 +20,26 @@
#include "FilePropertyDirectoryTree.hpp" #include "FilePropertyDirectoryTree.hpp"
using namespace pipedal; using namespace pipedal;
namespace fs = std::filesystem;
FilePropertyDirectoryTree::FilePropertyDirectoryTree() FilePropertyDirectoryTree::FilePropertyDirectoryTree()
{ {
} }
FilePropertyDirectoryTree::FilePropertyDirectoryTree(const std::string &directoryName) FilePropertyDirectoryTree::FilePropertyDirectoryTree(const std::string &directoryName)
: directoryName_(directoryName) : directoryName_(directoryName),
displayName_(fs::path(directoryName).filename().string())
{
}
FilePropertyDirectoryTree::FilePropertyDirectoryTree(const std::string &directoryName, const std::string &displayName)
: directoryName_(directoryName),
displayName_(displayName)
{ {
} }
JSON_MAP_BEGIN(FilePropertyDirectoryTree) JSON_MAP_BEGIN(FilePropertyDirectoryTree)
JSON_MAP_REFERENCE(FilePropertyDirectoryTree, directoryName) JSON_MAP_REFERENCE(FilePropertyDirectoryTree, directoryName)
JSON_MAP_REFERENCE(FilePropertyDirectoryTree, displayName)
JSON_MAP_REFERENCE(FilePropertyDirectoryTree, isProtected)
JSON_MAP_REFERENCE(FilePropertyDirectoryTree, children) JSON_MAP_REFERENCE(FilePropertyDirectoryTree, children)
JSON_MAP_END() JSON_MAP_END()
+3
View File
@@ -29,7 +29,10 @@ namespace pipedal {
FilePropertyDirectoryTree(); FilePropertyDirectoryTree();
FilePropertyDirectoryTree(const std::string&directoryName); FilePropertyDirectoryTree(const std::string&directoryName);
FilePropertyDirectoryTree(const std::string&directoryName,const std::string&displayName);
std::string directoryName_; std::string directoryName_;
std::string displayName_;
bool isProtected_ = false;
std::vector<std::unique_ptr<FilePropertyDirectoryTree>> children_; std::vector<std::unique_ptr<FilePropertyDirectoryTree>> children_;
DECLARE_JSON_MAP(FilePropertyDirectoryTree); DECLARE_JSON_MAP(FilePropertyDirectoryTree);
+156
View File
@@ -0,0 +1,156 @@
// Copyright (c) 2024 Robin 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.
#include "ModFileTypes.hpp"
#include "util.hpp"
#include <stdexcept>
#include <iostream>
#include "ss.hpp"
using namespace pipedal;
const std::vector<ModFileTypes::ModDirectory> ModFileTypes::ModDirectories =
{
{"audioloop", "shared/audio/Loops", "Loops", {"audio/*"}}, // Audio Loops, meant to be used for looper-style plugins
//"audiorecording","shared/audio/Audio Recordings", {"audio/*"}}, : Audio Recordings, triggered by plugins and stored in the unit
{"audiosample", "shared/audio/Samples", "Samples", {"audio/*"}}, // One-shot Audio Samples, meant to be used for sampler-style plugins
{"audiotrack", "shared/audio/Tracks", "Tracks", {"audio/*"}}, // Audio Tracks, meant to be used as full-performance/song or backtrack
{"cabsim", "CabIR", "Cab IRs", {"audio/*"}}, // Speaker Cabinets, meant as small IR audio files
/// - h2drumkit: Hydrogen Drumkits, must use h2drumkit file extension
{"ir", "ReverbImpulseFiles", "Impulse Responses", {"audio/*"}}, // Impulse Responses
{"midiclip", "shared/midiClips", "MIDI Clips", {".mid", ".midi"}}, // MIDI Clips, to be used in sync with host tempo, must have mid or midi file extension
{"midisong", "shared/midiSongs", "MIDI Songs", {".mid", ".midi"}}, // MIDI Songs, meant to be used as full-performance/song or backtrack
{"sf2", "shared/sf2", "Sound Fonts", {"sf2", "sf3"}}, // SF2 Instruments, must have sf2 or sf3 file extension
{"sfz", "shared/sfz", "Sfz Files", {"sfz"}}, // SFZ Instruments, must have sfz file extension
// extensions observed in the field.
{"audio", "shared/audio", "Audio", {"audio/*"}}, // all audio files (Ratatoille)
{"nammodel", "NeuralAmpModels", "Neural Amp Models", {".nam"}}, // Ratatoille, Mike's NAM.
{"aidadspmodel", "shared/aidaaix", "AIDA IAX Models", {".json", ".aidaiax"}}, // Ratatoille
{"mlmodel", "ToobMlModels", "ML Models", {".json"}}, //
//
};
const ModFileTypes::ModDirectory *ModFileTypes::GetModDirectory(const std::string &type)
{
for (const ModFileTypes::ModDirectory &modType : ModFileTypes::ModDirectories)
{
if (modType.modType == type)
{
return &modType;
}
}
return nullptr;
}
ModFileTypes::ModFileTypes(const std::string &fileTypes)
{
std::vector<std::string> types = split(fileTypes, ',');
for (const auto &type : types)
{
const ModDirectory *wellKnownType = GetModDirectory(type);
if (wellKnownType)
{
rootDirectories_.push_back(type);
}
else
{
fileTypes_.push_back(type);
}
}
// for Ratatoille.lv2.
// If rootDirectories contains "nammodel" and fileTypes contains "json", add "mlmodel" too.
// if (contains(rootDirectories_,"nammodel") && contains(fileTypes_,"json"))
// {
// if (!contains(rootDirectories_,"mlmodel"))
// {
// rootDirectories_.push_back("mlmodel");
// }
// }
if (fileTypes_.empty())
{
for (const auto &type : types)
{
const ModDirectory *wellKnownType = GetModDirectory(type);
if (wellKnownType)
{
for (const std::string &newType : wellKnownType->defaultFileExtensions)
{
bool found = false;
for (const std::string &oldType : fileTypes_)
{
if (newType == oldType)
{
found = true;
break;
}
}
if (!found)
{
fileTypes_.push_back(newType);
}
}
}
}
}
}
static std::filesystem::path getModDirectoryPath(
const std::filesystem::path &rootDirectory,
const std::string &modType)
{
auto wellKnownType = ModFileTypes::GetModDirectory(modType);
if (!wellKnownType)
{
throw std::runtime_error(SS("Can't find modFileType " << modType));
}
return rootDirectory / wellKnownType->pipedalPath;
}
void ModFileTypes::CreateDefaultDirectories(const std::filesystem::path &rootDirectory)
{
namespace fs = std::filesystem;
using namespace std;
try
{
for (const auto &modType : ModDirectories)
{
fs::path path = rootDirectory / modType.pipedalPath;
fs::create_directories(path);
}
if (!fs::exists(rootDirectory / "shared" / "audio" / "Cab IR Files"))
{
fs::create_symlink(
getModDirectoryPath(rootDirectory, "cabsim"),
rootDirectory / "shared" / "audio" / "Cab IR Files");
}
if (!fs::exists(rootDirectory / "shared" / "audio" / "Impulse Responses"))
{
fs::create_symlink(
getModDirectoryPath(rootDirectory, "ir"),
rootDirectory / "shared" / "audio" / "Impulse Responses");
}
}
catch (const std::exception &e)
{
cout << "Can't create default MOD directories. " << e.what() << endl;
}
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright (c) 2024 Robin 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.
#pragma once
#include <string>
#include <vector>
#include <filesystem>
namespace pipedal {
class ModFileTypes {
public:
ModFileTypes(const std::string&fileTypes);
const std::vector<std::string> &rootDirectories() { return rootDirectories_; }
const std::vector<std::string> &fileTypes() { return fileTypes_;}
private:
std::vector<std::string> rootDirectories_;
std::vector<std::string> fileTypes_;
public:
class ModDirectory {
public:
const std::string modType;
const std::string pipedalPath;
const std::string displayName;
const std::vector<std::string> defaultFileExtensions;
};
const static std::vector<ModDirectory> ModDirectories;
static const ModDirectory* GetModDirectory(const std::string&modType);
static void CreateDefaultDirectories(const std::filesystem::path&rootDirectory);
};
}
+2 -2
View File
@@ -2296,7 +2296,7 @@ std::vector<std::string> PiPedalModel::GetFileList(const UiFileProperty &filePro
return std::vector<std::string>(); // don't disclose to users what the problem is. return std::vector<std::string>(); // don't disclose to users what the problem is.
} }
} }
std::vector<FileEntry> PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty) FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty)
{ {
try try
{ {
@@ -2305,7 +2305,7 @@ std::vector<FileEntry> PiPedalModel::GetFileList2(const std::string &relativePat
catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::warning("GetFileList() failed: (%s)", e.what()); Lv2Log::warning("GetFileList() failed: (%s)", e.what());
return std::vector<FileEntry>(); // don't disclose to users what the problem is. throw;
} }
} }
+1 -1
View File
@@ -432,7 +432,7 @@ namespace pipedal
void SetUpdatePolicy(UpdatePolicyT updatePolicy); void SetUpdatePolicy(UpdatePolicyT updatePolicy);
void ForceUpdateCheck(); void ForceUpdateCheck();
std::vector<std::string> GetFileList(const UiFileProperty &fileProperty); std::vector<std::string> GetFileList(const UiFileProperty &fileProperty);
std::vector<FileEntry> GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty); FileRequestResult GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty);
void DeleteSampleFile(const std::filesystem::path &fileName); void DeleteSampleFile(const std::filesystem::path &fileName);
std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty); std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty);
+2 -2
View File
@@ -1589,8 +1589,8 @@ public:
{ {
FileRequestArgs requestArgs; FileRequestArgs requestArgs;
pReader->read(&requestArgs); pReader->read(&requestArgs);
std::vector<FileEntry> list = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_); FileRequestResult result = this->model.GetFileList2(requestArgs.relativePath_, requestArgs.fileProperty_);
this->Reply(replyTo, "requestFileList2", list); this->Reply(replyTo, "requestFileList2", result);
} }
else if (message == "newPreset") else if (message == "newPreset")
{ {
+2
View File
@@ -465,6 +465,8 @@ JSON_MAP_REFERENCE(UiFileProperty, patchProperty)
JSON_MAP_REFERENCE(UiFileProperty, fileTypes) JSON_MAP_REFERENCE(UiFileProperty, fileTypes)
JSON_MAP_REFERENCE(UiFileProperty, portGroup) JSON_MAP_REFERENCE(UiFileProperty, portGroup)
JSON_MAP_REFERENCE(UiFileProperty, resourceDirectory) JSON_MAP_REFERENCE(UiFileProperty, resourceDirectory)
JSON_MAP_REFERENCE(UiFileProperty, modDirectories)
JSON_MAP_REFERENCE(UiFileProperty, useLegacyModDirectory)
JSON_MAP_END() JSON_MAP_END()
JSON_MAP_BEGIN(UiFrequencyPlot) JSON_MAP_BEGIN(UiFrequencyPlot)
+10
View File
@@ -114,6 +114,8 @@ namespace pipedal {
std::string patchProperty_; std::string patchProperty_;
std::string portGroup_; std::string portGroup_;
std::string resourceDirectory_; std::string resourceDirectory_;
std::vector<std::string> modDirectories_;
bool useLegacyModDirectory_= false;
public: public:
using ptr = std::shared_ptr<UiFileProperty>; using ptr = std::shared_ptr<UiFileProperty>;
UiFileProperty() { } UiFileProperty() { }
@@ -121,9 +123,17 @@ namespace pipedal {
UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory); UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory);
std::vector<std::string>& modDirectories() { return modDirectories_; }
const std::vector<std::string>& modDirectories() const { return modDirectories_; }
bool useLegacyModDirectory() const { return useLegacyModDirectory_; }
void useLegacyModDirectory(bool value) { useLegacyModDirectory_ = value; }
const std::string &label() const { return label_; } const std::string &label() const { return label_; }
int32_t index() const { return index_; } int32_t index() const { return index_; }
const std::string &directory() const { return directory_; } const std::string &directory() const { return directory_; }
void directory(const std::string &path) { directory_ = path; }
const std::string&portGroup() const { return portGroup_; } const std::string&portGroup() const { return portGroup_; }
const std::vector<UiFileType> &fileTypes() const { return fileTypes_; } const std::vector<UiFileType> &fileTypes() const { return fileTypes_; }
+92 -54
View File
@@ -46,6 +46,7 @@
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
#include "StdErrorCapture.hpp" #include "StdErrorCapture.hpp"
#include "util.hpp" #include "util.hpp"
#include "ModFileTypes.hpp"
#include "Locale.hpp" #include "Locale.hpp"
@@ -118,6 +119,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
core__toggled = lilv_new_uri(pWorld, LV2_CORE__toggled); core__toggled = lilv_new_uri(pWorld, LV2_CORE__toggled);
core__connectionOptional = lilv_new_uri(pWorld, LV2_CORE__connectionOptional); core__connectionOptional = lilv_new_uri(pWorld, LV2_CORE__connectionOptional);
portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI); portprops__not_on_gui_property_uri = lilv_new_uri(pWorld, LV2_PORT_PROPS__notOnGUI);
portprops__trigger = lilv_new_uri(pWorld, LV2_PORT_PROPS__trigger);
midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent); midi__event = lilv_new_uri(pWorld, LV2_MIDI__MidiEvent);
core__designation = lilv_new_uri(pWorld, LV2_CORE__designation); core__designation = lilv_new_uri(pWorld, LV2_CORE__designation);
portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group); portgroups__group = lilv_new_uri(pWorld, LV2_PORT_GROUPS__group);
@@ -191,8 +193,7 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format"); dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format");
mod__fileTypes = lilv_new_uri(pWorld,"http://moddevices.com/ns/mod#fileTypes"); mod__fileTypes = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#fileTypes");
} }
void PluginHost::LilvUris::Free() void PluginHost::LilvUris::Free()
@@ -477,8 +478,7 @@ void PluginHost::Load(const char *lv2Path)
const char *pb1 = left->name().c_str(); const char *pb1 = left->name().c_str();
const char *pb2 = right->name().c_str(); const char *pb2 = right->name().c_str();
return collator->Compare( return collator->Compare(
left->name(),right->name()) left->name(), right->name()) < 0;
< 0;
}; };
std::sort(this->plugins_.begin(), this->plugins_.end(), compare); std::sort(this->plugins_.begin(), this->plugins_.end(), compare);
@@ -624,15 +624,47 @@ std::shared_ptr<PiPedalUI> Lv2PluginInfo::FindWritablePathProperties(PluginHost
strLabel, propertyUri.AsUri(), lv2DirectoryName); strLabel, propertyUri.AsUri(), lv2DirectoryName);
AutoLilvNodes mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr); AutoLilvNodes mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr);
LILV_FOREACH(nodes,i,mod__fileTypes) LILV_FOREACH(nodes, i, mod__fileTypes)
{ {
// "nam,nammodel" // "nam,nammodel"
AutoLilvNode lilvfileType {lilv_nodes_get(mod__fileTypes, i)}; AutoLilvNode lilvfileType{lilv_nodes_get(mod__fileTypes, i)};
std::string fileTypes = lilvfileType.AsString(); std::string fileTypes = lilvfileType.AsString();
ModFileTypes modFileTypes(fileTypes);
for (std::string&type: split(fileTypes,',')) for (const std::string &rootDirectory : modFileTypes.rootDirectories())
{ {
fileProperty->fileTypes().push_back(UiFileType(SS(type << " file"),SS('.' << type))); fileProperty->modDirectories().push_back(rootDirectory);
}
for (const std::string &type : modFileTypes.fileTypes())
{
fileProperty->fileTypes().push_back(UiFileType(SS(type << " file"), SS('.' << type)));
}
// Legacy case: audio_uploads/<plugin_directory> exists.
std::filesystem::path bundleDirectoryName = std::filesystem::path(bundle_path()).parent_path().filename();
std::filesystem::path legacyUploadPath = lv2Host->MapPath(bundleDirectoryName.string());
if (std::filesystem::exists(legacyUploadPath))
{
if (!std::filesystem::exists(legacyUploadPath / ".migrated"))
{
fileProperty->useLegacyModDirectory(true);
fileProperty->directory(bundleDirectoryName);
}
}
if (fileProperty->modDirectories().size() == 0)
{
fileProperty->directory(bundleDirectoryName);
}
else if (fileProperty->modDirectories().size() == 1 && !fileProperty->useLegacyModDirectory()) // no synthetic root.
{
auto modType = ModFileTypes::GetModDirectory(fileProperty->modDirectories()[0]);
if (modType)
{
fileProperty->directory(modType->pipedalPath);
}
} else {
// handled at request time.
} }
} }
if (!mod__fileTypes) if (!mod__fileTypes)
@@ -932,6 +964,7 @@ Lv2PortInfo::Lv2PortInfo(PluginHost *host, const LilvPlugin *plugin, const LilvP
this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled); this->toggled_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__toggled);
this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri); this->not_on_gui_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__not_on_gui_property_uri);
this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional); this->connection_optional_ = lilv_port_has_property(plugin, pPort, host->lilvUris->core__connectionOptional);
this->trigger_property_ = lilv_port_has_property(plugin, pPort, host->lilvUris->portprops__trigger);
LilvScalePoints *pScalePoints = lilv_port_get_scale_points(plugin, pPort); LilvScalePoints *pScalePoints = lilv_port_get_scale_points(plugin, pPort);
LILV_FOREACH(scale_points, iSP, pScalePoints) LILV_FOREACH(scale_points, iSP, pScalePoints)
@@ -1387,33 +1420,36 @@ std::shared_ptr<HostWorkerThread> PluginHost::GetHostWorkerThread()
return pHostWorkerThread; return pHostWorkerThread;
} }
class ResourceInfo { class ResourceInfo
{
public: public:
ResourceInfo(const std::string&filePropertyDirectory,const std::string&resourceDirectory) ResourceInfo(const std::string &filePropertyDirectory, const std::string &resourceDirectory)
:filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {} : filePropertyDirectory(filePropertyDirectory), resourceDirectory(resourceDirectory) {}
std::string filePropertyDirectory; std::string filePropertyDirectory;
std::string resourceDirectory; std::string resourceDirectory;
bool operator==(const ResourceInfo&other) const { bool operator==(const ResourceInfo &other) const
{
return this->filePropertyDirectory == other.filePropertyDirectory && this->resourceDirectory == other.resourceDirectory; return this->filePropertyDirectory == other.filePropertyDirectory && this->resourceDirectory == other.resourceDirectory;
} }
bool operator<(const ResourceInfo&other) const { bool operator<(const ResourceInfo &other) const
{
if (this->filePropertyDirectory < other.filePropertyDirectory) if (this->filePropertyDirectory < other.filePropertyDirectory)
return true; return true;
if (this->filePropertyDirectory > other.filePropertyDirectory) if (this->filePropertyDirectory > other.filePropertyDirectory)
return false; return false;
return this->resourceDirectory < other.resourceDirectory; return this->resourceDirectory < other.resourceDirectory;
} }
}; };
static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, const std::filesystem::path&targetDirectory) static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, const std::filesystem::path &targetDirectory)
{ {
namespace fs = std::filesystem; namespace fs = std::filesystem;
try { try
if (!fs::exists(targetDirectory)) return false; {
for (auto&directoryEntry : fs::directory_iterator(sourceDirectory)) if (!fs::exists(targetDirectory))
return false;
for (auto &directoryEntry : fs::directory_iterator(sourceDirectory))
{ {
fs::path thisPath = directoryEntry.path(); fs::path thisPath = directoryEntry.path();
if (directoryEntry.is_directory()) if (directoryEntry.is_directory())
@@ -1421,11 +1457,13 @@ static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, co
auto name = thisPath.filename(); auto name = thisPath.filename();
fs::path childSource = sourceDirectory / name; fs::path childSource = sourceDirectory / name;
fs::path childTarget = targetDirectory / name; fs::path childTarget = targetDirectory / name;
if (anyTargetFilesExist(childSource,childTarget)) if (anyTargetFilesExist(childSource, childTarget))
{ {
return true; return true;
} }
} else { }
else
{
fs::path targetPath = targetDirectory / thisPath.filename(); fs::path targetPath = targetDirectory / thisPath.filename();
if (fs::exists(targetPath)) if (fs::exists(targetPath))
{ {
@@ -1433,9 +1471,9 @@ static bool anyTargetFilesExist(const std::filesystem::path &sourceDirectory, co
} }
} }
} }
}
} catch (const std::exception&) { catch (const std::exception &)
{
} }
return false; return false;
} }
@@ -1450,48 +1488,52 @@ static void createTargetLinks(const std::filesystem::path &sourceDirectory, cons
{ {
fs::path childSource = dirEntry.path(); fs::path childSource = dirEntry.path();
fs::path childTarget = targetDirectory / childSource.filename(); fs::path childTarget = targetDirectory / childSource.filename();
if (dirEntry.is_directory()) { if (dirEntry.is_directory())
createTargetLinks(childSource,childTarget); {
} else { createTargetLinks(childSource, childTarget);
fs::create_symlink(childSource,childTarget); }
else
{
fs::create_symlink(childSource, childTarget);
} }
} }
} }
void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,const std::filesystem::path&pluginUploadDirectory) void PluginHost::CheckForResourceInitialization(const std::string &pluginUri, const std::filesystem::path &pluginUploadDirectory)
{ {
auto plugin = GetPluginInfo(pluginUri); auto plugin = GetPluginInfo(pluginUri);
if (plugin) if (plugin)
{ {
std::filesystem::path bundlePath = plugin->bundle_path(); std::filesystem::path bundlePath = plugin->bundle_path();
if (!plugin->piPedalUI()) if (!plugin->piPedalUI())
return; return;
const auto& fileProperties = plugin->piPedalUI()->fileProperties(); const auto &fileProperties = plugin->piPedalUI()->fileProperties();
if (fileProperties.size() != 0 && !pluginsThatHaveBeenCheckedForResources.contains(pluginUri)) if (fileProperties.size() != 0 && !pluginsThatHaveBeenCheckedForResources.contains(pluginUri))
{ {
pluginsThatHaveBeenCheckedForResources.insert(pluginUri); pluginsThatHaveBeenCheckedForResources.insert(pluginUri);
// eliminate duplicates. // eliminate duplicates.
std::set<ResourceInfo> resourceInfoSet; std::set<ResourceInfo> resourceInfoSet;
for (const auto&fileProperty: fileProperties) for (const auto &fileProperty : fileProperties)
{ {
if (!fileProperty->resourceDirectory().empty() && !fileProperty->directory().empty()) if (!fileProperty->resourceDirectory().empty() && !fileProperty->directory().empty())
{ {
resourceInfoSet.insert(ResourceInfo(fileProperty->directory(),fileProperty->resourceDirectory())); resourceInfoSet.insert(ResourceInfo(fileProperty->directory(), fileProperty->resourceDirectory()));
} }
} }
try { try
for (const ResourceInfo&resourceInfo: resourceInfoSet)
{ {
std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory; for (const ResourceInfo &resourceInfo : resourceInfoSet)
std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory;
if (!anyTargetFilesExist(sourcePath,targetPath))
{ {
createTargetLinks(sourcePath,targetPath); std::filesystem::path sourcePath = bundlePath / resourceInfo.resourceDirectory;
std::filesystem::path targetPath = pluginUploadDirectory / resourceInfo.filePropertyDirectory;
if (!anyTargetFilesExist(sourcePath, targetPath))
{
createTargetLinks(sourcePath, targetPath);
}
} }
} }
} catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::error(SS("CheckForResourceInitialization: " << e.what())); Lv2Log::error(SS("CheckForResourceInitialization: " << e.what()));
} }
@@ -1499,20 +1541,17 @@ void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,con
} }
} }
json_variant PluginHost::MapPath(const json_variant&json) json_variant PluginHost::MapPath(const json_variant &json)
{ {
AtomConverter converter(GetMapFeature()); AtomConverter converter(GetMapFeature());
return converter.MapPath(json,GetPluginStoragePath()); return converter.MapPath(json, GetPluginStoragePath());
} }
json_variant PluginHost::AbstractPath(const json_variant&json) json_variant PluginHost::AbstractPath(const json_variant &json)
{ {
AtomConverter converter(GetMapFeature()); AtomConverter converter(GetMapFeature());
return converter.AbstractPath(json,GetPluginStoragePath()); return converter.AbstractPath(json, GetPluginStoragePath());
} }
std::string PluginHost::MapPath(const std::string &abstractPath) std::string PluginHost::MapPath(const std::string &abstractPath)
{ {
auto storagePath = GetPluginStoragePath(); auto storagePath = GetPluginStoragePath();
@@ -1521,10 +1560,8 @@ std::string PluginHost::MapPath(const std::string &abstractPath)
return ""; return "";
} }
return SS(storagePath << '/' << abstractPath); return SS(storagePath << '/' << abstractPath);
} }
std::string PluginHost::AbstractPath(const std::string&path) std::string PluginHost::AbstractPath(const std::string &path)
{ {
auto storagePath = GetPluginStoragePath(); auto storagePath = GetPluginStoragePath();
if (path.starts_with(storagePath)) if (path.starts_with(storagePath))
@@ -1593,7 +1630,7 @@ json_map::storage_type<Lv2PortInfo> Lv2PortInfo::jmap{
MAP_REF(Lv2PortInfo, is_logarithmic), MAP_REF(Lv2PortInfo, is_logarithmic),
MAP_REF(Lv2PortInfo, display_priority), MAP_REF(Lv2PortInfo, display_priority),
MAP_REF(Lv2PortInfo, range_steps), MAP_REF(Lv2PortInfo, range_steps),
MAP_REF(Lv2PortInfo, trigger), MAP_REF(Lv2PortInfo, trigger_property),
MAP_REF(Lv2PortInfo, integer_property), MAP_REF(Lv2PortInfo, integer_property),
MAP_REF(Lv2PortInfo, enumeration_property), MAP_REF(Lv2PortInfo, enumeration_property),
MAP_REF(Lv2PortInfo, toggled_property), MAP_REF(Lv2PortInfo, toggled_property),
@@ -1662,6 +1699,7 @@ json_map::storage_type<Lv2PluginUiPort> Lv2PluginUiPort::jmap{{
MAP_REF(Lv2PluginUiPort, enumeration_property), MAP_REF(Lv2PluginUiPort, enumeration_property),
MAP_REF(Lv2PluginUiPort, not_on_gui), MAP_REF(Lv2PluginUiPort, not_on_gui),
MAP_REF(Lv2PluginUiPort, toggled_property), MAP_REF(Lv2PluginUiPort, toggled_property),
MAP_REF(Lv2PluginUiPort, trigger_property),
MAP_REF(Lv2PluginUiPort, scale_points), MAP_REF(Lv2PluginUiPort, scale_points),
MAP_REF(Lv2PluginUiPort, port_group), MAP_REF(Lv2PluginUiPort, port_group),
+6 -2
View File
@@ -214,7 +214,7 @@ namespace pipedal
bool is_logarithmic_ = false; bool is_logarithmic_ = false;
int display_priority_ = -1; int display_priority_ = -1;
int range_steps_ = 0; int range_steps_ = 0;
bool trigger_ = false; bool trigger_property_ = false;
bool integer_property_ = false; bool integer_property_ = false;
bool enumeration_property_ = false; bool enumeration_property_ = false;
bool toggled_property_ = false; bool toggled_property_ = false;
@@ -285,7 +285,7 @@ namespace pipedal
LV2_PROPERTY_GETSET_SCALAR(is_logarithmic); LV2_PROPERTY_GETSET_SCALAR(is_logarithmic);
LV2_PROPERTY_GETSET_SCALAR(display_priority); LV2_PROPERTY_GETSET_SCALAR(display_priority);
LV2_PROPERTY_GETSET_SCALAR(range_steps); LV2_PROPERTY_GETSET_SCALAR(range_steps);
LV2_PROPERTY_GETSET_SCALAR(trigger); LV2_PROPERTY_GETSET_SCALAR(trigger_property);
LV2_PROPERTY_GETSET_SCALAR(integer_property); LV2_PROPERTY_GETSET_SCALAR(integer_property);
LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(enumeration_property);
LV2_PROPERTY_GETSET_SCALAR(toggled_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property);
@@ -496,6 +496,7 @@ namespace pipedal
default_value_(pPort->default_value()), range_steps_(pPort->range_steps()), display_priority_(pPort->display_priority()), default_value_(pPort->default_value()), range_steps_(pPort->range_steps()), display_priority_(pPort->display_priority()),
is_logarithmic_(pPort->is_logarithmic()), integer_property_(pPort->integer_property()), enumeration_property_(pPort->enumeration_property()), is_logarithmic_(pPort->is_logarithmic()), integer_property_(pPort->integer_property()), enumeration_property_(pPort->enumeration_property()),
toggled_property_(pPort->toggled_property()), not_on_gui_(pPort->not_on_gui()), scale_points_(pPort->scale_points()), toggled_property_(pPort->toggled_property()), not_on_gui_(pPort->not_on_gui()), scale_points_(pPort->scale_points()),
trigger_property_(pPort->trigger_property()),
comment_(pPort->comment()), units_(pPort->units()), comment_(pPort->comment()), units_(pPort->units()),
connection_optional_(pPort->connection_optional()) connection_optional_(pPort->connection_optional())
{ {
@@ -536,6 +537,7 @@ namespace pipedal
bool enumeration_property_ = false; bool enumeration_property_ = false;
bool not_on_gui_ = false; bool not_on_gui_ = false;
bool toggled_property_ = false; bool toggled_property_ = false;
bool trigger_property_ = false;
std::vector<Lv2ScalePoint> scale_points_; std::vector<Lv2ScalePoint> scale_points_;
std::string port_group_; std::string port_group_;
@@ -561,6 +563,7 @@ namespace pipedal
LV2_PROPERTY_GETSET_SCALAR(integer_property); LV2_PROPERTY_GETSET_SCALAR(integer_property);
LV2_PROPERTY_GETSET_SCALAR(enumeration_property); LV2_PROPERTY_GETSET_SCALAR(enumeration_property);
LV2_PROPERTY_GETSET_SCALAR(toggled_property); LV2_PROPERTY_GETSET_SCALAR(toggled_property);
LV2_PROPERTY_GETSET_SCALAR(trigger_property);
LV2_PROPERTY_GETSET_SCALAR(not_on_gui); LV2_PROPERTY_GETSET_SCALAR(not_on_gui);
LV2_PROPERTY_GETSET(scale_points); LV2_PROPERTY_GETSET(scale_points);
LV2_PROPERTY_GETSET(units); LV2_PROPERTY_GETSET(units);
@@ -664,6 +667,7 @@ namespace pipedal
AutoLilvNode core__toggled; AutoLilvNode core__toggled;
AutoLilvNode core__connectionOptional; AutoLilvNode core__connectionOptional;
AutoLilvNode portprops__not_on_gui_property_uri; AutoLilvNode portprops__not_on_gui_property_uri;
AutoLilvNode portprops__trigger;
AutoLilvNode midi__event; AutoLilvNode midi__event;
AutoLilvNode core__designation; AutoLilvNode core__designation;
AutoLilvNode portgroups__group; AutoLilvNode portgroups__group;
+276 -69
View File
@@ -22,6 +22,7 @@
#include "Storage.hpp" #include "Storage.hpp"
#include "AudioConfig.hpp" #include "AudioConfig.hpp"
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
#include <stdexcept>
#include <sstream> #include <sstream>
#include "json.hpp" #include "json.hpp"
#include <fstream> #include <fstream>
@@ -32,14 +33,50 @@
#include "PluginHost.hpp" #include "PluginHost.hpp"
#include "ss.hpp" #include "ss.hpp"
#include "ofstream_synced.hpp" #include "ofstream_synced.hpp"
#include "ModFileTypes.hpp"
using namespace pipedal; using namespace pipedal;
namespace fs = std::filesystem;
const char *BANK_EXTENSION = ".bank"; const char *BANK_EXTENSION = ".bank";
const char *BANKS_FILENAME = "index.banks"; const char *BANKS_FILENAME = "index.banks";
#define USER_SETTINGS_FILENAME "userSettings.json"; #define USER_SETTINGS_FILENAME "userSettings.json";
static bool isSubdirectory(const fs::path&path, const fs::path&basePath)
{
auto iPath = path.begin();
for (auto i = basePath.begin(); i != basePath.end(); ++i)
{
if (iPath == path.end())
{
return false;
}
if ((*i) != (*iPath))
{
return false;
}
++iPath;
}
while (iPath != path.end())
{
if (iPath->string() == "..")
{
return false;
}
++iPath;
}
return true;
}
static bool hasSyntheticModRoot(const UiFileProperty &fileProperty)
{
return (fileProperty.modDirectories().size() > 1 || (fileProperty.modDirectories().size() == 1 && fileProperty.useLegacyModDirectory()));
}
Storage::Storage() Storage::Storage()
{ {
SetConfigRoot("~/var/Config"); SetConfigRoot("~/var/Config");
@@ -1648,63 +1685,190 @@ static bool ensureNoDotDot(const std::filesystem::path&path)
return true; return true;
} }
static void AddFilesToResult(
std::vector<FileEntry> Storage::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty) FileRequestResult&result,
const UiFileProperty &fileProperty,
const fs::path&rootPath)
{ {
if (!ensureNoDotDot(relativePath)) if (!fs::exists(rootPath))
{ {
ThrowPermissionDeniedError(); return; // silently without error.
} }
if (!UiFileProperty::IsDirectoryNameValid(fileProperty.directory())) auto & resultFiles = result.files_;
try
{ {
ThrowPermissionDeniedError(); for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
}
std::vector<FileEntry> result;
// if fileProperty has a user-accessible directory, push the entire file path.
if (fileProperty.directory().size() != 0)
{
std::filesystem::path audioFileDirectory = this->GetPluginUploadDirectory() / fileProperty.directory() / relativePath;
try
{ {
for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory)) const auto &path = dir_entry.path();
auto name = path.filename().string();
if (dir_entry.is_regular_file())
{ {
if (dir_entry.is_regular_file()) if (name.length() > 0 && name[0] != '.') // don't show hidden files.
{ {
auto &path = dir_entry.path(); if (fileProperty.IsValidExtension(path.extension().string()))
auto name = path.filename().string();
if (name.length() > 0 && name[0] != '.') // don't show hidden files.
{ {
if (fileProperty.IsValidExtension(path.extension().string())) resultFiles.push_back(
{ FileEntry(path,name,false,false)
// a relative path! );
result.push_back(FileEntry(path,false));
}
} }
} else if (dir_entry.is_directory()) {
result.push_back(FileEntry(dir_entry.path(),true));
} }
} else if (dir_entry.is_directory()) {
resultFiles.push_back(FileEntry{path,name,true,fs::is_symlink(path)});
} }
} }
catch (const std::exception &error) }
{ catch (const std::exception &error)
throw std::logic_error("GetFileList failed. Directory not found: " + audioFileDirectory.string()); {
} throw std::logic_error("GetFileList failed. Directory not found: " + rootPath.string());
} }
// sort lexicographically // sort lexicographically
auto collator = Locale::GetInstance()->GetCollator(); auto collator = Locale::GetInstance()->GetCollator();
std::sort(result.begin(), result.end(),[&collator](const FileEntry&l, const FileEntry&r) { std::sort(resultFiles.begin(), resultFiles.end(),[&collator](const FileEntry&l, const FileEntry&r) {
if (l.isDirectory_ != r.isDirectory_) if (l.isDirectory_ != r.isDirectory_)
{ {
return l.isDirectory_ > r.isDirectory_; return l.isDirectory_ > r.isDirectory_;
} }
return collator->Compare(l.filename_,r.filename_) < 0; return collator->Compare(l.displayName_,r.displayName_) < 0;
}); });
}
FileRequestResult Storage::GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty)
{
FileRequestResult result;
fs::path uploadsDirectory = GetPluginUploadDirectory();
if (relativePath.empty())
{
// return the synthetic root.
result.isProtected_ = true;
for (const auto&modDirectory: fileProperty.modDirectories())
{
const auto directoryInfo = ModFileTypes::GetModDirectory(modDirectory);
if (directoryInfo)
{
result.files_.push_back(
FileEntry(uploadsDirectory / directoryInfo->pipedalPath,directoryInfo->displayName,true,true)
);
}
}
if (fileProperty.useLegacyModDirectory())
{
result.files_.push_back(
FileEntry(uploadsDirectory / fileProperty.directory(),fs::path(fileProperty.directory()).filename().string(),true,true)
);
}
result.breadcrumbs_.push_back({"","Home"});
return result;
}
fs::path modDirectoryPath;
result.breadcrumbs_.push_back({"","Home"});
fs::path fsRelativePath { relativePath};
for (const auto &modDirectory: fileProperty.modDirectories())
{
const ModFileTypes::ModDirectory*modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory);
if (modDirectoryInfo)
{
if (isSubdirectory(fsRelativePath, uploadsDirectory / modDirectoryInfo->pipedalPath))
{
modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath;
result.breadcrumbs_.push_back({modDirectoryPath.string(),modDirectoryInfo->displayName});
break;
}
}
}
if (modDirectoryPath.empty() && fileProperty.useLegacyModDirectory())
{
if (isSubdirectory(fsRelativePath, uploadsDirectory / fileProperty.directory()))
{
modDirectoryPath = uploadsDirectory / fileProperty.directory();
result.breadcrumbs_.push_back({modDirectoryPath.string(),fs::path(fileProperty.directory()).filename().string()});
} else {
ThrowPermissionDeniedError();
}
}
// add remaing path segements as breadcrumbs.
{
fs::path modPath {modDirectoryPath};
fs::path rp { relativePath};
auto iRp = rp.begin();
// skip past one or more segments in the modDiretoryPath.
for (auto iModPath = modPath.begin(); iModPath != modPath.end(); ++iModPath)
{
if (iRp != rp.end()) {
++iRp;
}
}
while (iRp != rp.end())
{
result.breadcrumbs_.push_back({*iRp,*iRp});
++iRp;
}
}
AddFilesToResult(result,fileProperty,relativePath);
return result;
}
FileRequestResult Storage::GetFileList2(const std::string &relativePath_,const UiFileProperty &fileProperty)
{
std::string absolutePath = relativePath_;
if (!ensureNoDotDot(absolutePath))
{
ThrowPermissionDeniedError();
}
if (hasSyntheticModRoot(fileProperty))
{
return Storage::GetModFileList2(absolutePath,fileProperty);
}
FileRequestResult result;
if (fileProperty.directory().empty())
{
throw std::runtime_error("fileProperty.directory() not specified.");
}
std::filesystem::path pluginRootDirectory = this->GetPluginUploadDirectory() / fileProperty.directory();
if (absolutePath == "")
{
absolutePath = pluginRootDirectory.string();
}
{
result.breadcrumbs_.push_back({"","Home"});
fs::path fsAbsolutePath {absolutePath};
auto iAbsolutePath = fsAbsolutePath.begin();
for (auto i = pluginRootDirectory.begin(); i != pluginRootDirectory.end(); ++i)
{
if (iAbsolutePath == fsAbsolutePath.end() || (*i) != *iAbsolutePath)
{
throw std::runtime_error("Directory is not a subdirectory of the plugin root directory.");
}
++iAbsolutePath;
}
auto cumulativePath = pluginRootDirectory;
while (iAbsolutePath != fsAbsolutePath.end())
{
cumulativePath /= (*iAbsolutePath);
result.breadcrumbs_.push_back({cumulativePath.string(),iAbsolutePath->string()});
++iAbsolutePath;
}
}
if (!isSubdirectory(absolutePath,pluginRootDirectory))
{
throw std::runtime_error(SS("Improper location. " << absolutePath));
}
AddFilesToResult(result,fileProperty,absolutePath);
return result; return result;
} }
@@ -1715,29 +1879,32 @@ bool Storage::IsValidSampleFileName(const std::filesystem::path &fileName)
{ {
return false; return false;
} }
if (!ensureNoDotDot(fileName))
{
return false;
}
std::filesystem::path audioFilePath = this->GetPluginUploadDirectory(); std::filesystem::path audioFilePath = this->GetPluginUploadDirectory();
std::filesystem::path parentDirectory = fileName.parent_path(); std::filesystem::path parentDirectory = fileName.parent_path();
while (true)
auto iTarget = parentDirectory.begin();
for (auto i = audioFilePath.begin(); i != audioFilePath.end(); ++i)
{ {
if (!parentDirectory.has_parent_path()) if (iTarget == parentDirectory.end()) {
{
return false; return false;
} }
std::string name = parentDirectory.filename().string(); if (*i != *iTarget) {
if (parentDirectory == audioFilePath)
return true;
parentDirectory = parentDirectory.parent_path();
if (parentDirectory.string().length() < audioFilePath.string().length())
{
return false; return false;
} }
++iTarget;
} }
while (iTarget != parentDirectory.end())
{
if (iTarget->string() == "..")
{
return false;
}
++iTarget;
}
return true;
} }
void Storage::DeleteSampleFile(const std::filesystem::path &fileName) void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
{ {
@@ -1753,9 +1920,18 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
{ {
if (std::filesystem::is_directory(fileName)) if (std::filesystem::is_directory(fileName))
{ {
if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience) if (fileName.string().length() <= 1) // guard against rm -rf / (bitter experience)
{ {
std::filesystem::remove_all(fileName); throw std::logic_error("Invalid filename.");
}
if (std::filesystem::is_symlink(fileName))
{
std::filesystem::remove(fileName);
} else {
if (fileName.string().length() > 1) // guard against rm -rf / (bitter experience)
{
std::filesystem::remove_all(fileName);
}
} }
} }
else { else {
@@ -1769,13 +1945,13 @@ void Storage::DeleteSampleFile(const std::filesystem::path &fileName)
} }
std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, const std::string &filename) std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, const std::string &filename)
{ {
if (!ensureNoDotDot(directory))
{
throw std::logic_error(SS("Invalide filename: " << filename));
}
std::filesystem::path filePath{filename}; std::filesystem::path filePath{filename};
std::filesystem::path result = this->GetPluginUploadDirectory() / directory / filename; std::filesystem::path result = fs::path(directory) / filename;
if (!result.is_absolute())
{
result = this->GetPluginUploadDirectory() / result;
}
if (!this->IsValidSampleFileName(result)) if (!this->IsValidSampleFileName(result))
{ {
throw std::logic_error(SS("Invalid upload path: " << result)); throw std::logic_error(SS("Invalid upload path: " << result));
@@ -1898,8 +2074,8 @@ void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std:
{ {
if (child.is_directory()) if (child.is_directory())
{ {
const auto& childPath = child.path(); const auto& childPath = child.path();
FilePropertyDirectoryTree::ptr childTree = std::make_unique<FilePropertyDirectoryTree>(childPath.filename()); FilePropertyDirectoryTree::ptr childTree = std::make_unique<FilePropertyDirectoryTree>(childPath);
FillSampleDirectoryTree(childTree.get(),childPath); FillSampleDirectoryTree(childTree.get(),childPath);
node->children_.push_back(std::move(childTree)); node->children_.push_back(std::move(childTree));
} }
@@ -1911,22 +2087,53 @@ void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std:
return collator->Compare(left->directoryName_,right->directoryName_) < 0; return collator->Compare(left->directoryName_,right->directoryName_) < 0;
}); });
} }
FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) const FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty)
{ {
FilePropertyDirectoryTree::ptr result = std::make_unique<FilePropertyDirectoryTree>(""); fs::path uploadDirectory = this->GetPluginUploadDirectory();
if (uiFileProperty.directory().empty())
{
throw std::runtime_error("Invalid uiFileProperty");
}
if (!ensureNoDotDot(uiFileProperty.directory()))
{
throw std::runtime_error("Invalid uiFileProperty");
}
std::filesystem::path rootDirectory = this->GetPluginUploadDirectory() / uiFileProperty.directory();
FillSampleDirectoryTree(result.get(),rootDirectory); if (hasSyntheticModRoot(uiFileProperty))
{
FilePropertyDirectoryTree::ptr result = std::make_unique<FilePropertyDirectoryTree>("","Home");
result->isProtected_ = true;
for (const auto& modDirectory: uiFileProperty.modDirectories())
{
auto modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory);
if (modDirectoryInfo)
{
auto childPath = uploadDirectory / modDirectoryInfo->pipedalPath;
FilePropertyDirectoryTree::ptr child = std::make_unique<FilePropertyDirectoryTree>(
childPath,modDirectoryInfo->displayName
);
FillSampleDirectoryTree(child.get(),childPath);
result->children_.push_back(std::move(child));
}
}
if (uiFileProperty.useLegacyModDirectory()) {
auto childPath = uploadDirectory / uiFileProperty.directory();
FilePropertyDirectoryTree::ptr child = std::make_unique<FilePropertyDirectoryTree>(
childPath
);
FillSampleDirectoryTree(child.get(),childPath);
result->children_.push_back(std::move(child));
}
return result;
} else {
if (uiFileProperty.directory().empty())
{
throw std::runtime_error("Invalid uiFileProperty");
}
if (!ensureNoDotDot(uiFileProperty.directory()))
{
throw std::runtime_error("Invalid uiFileProperty");
}
std::filesystem::path rootDirectory = uploadDirectory / uiFileProperty.directory();
FilePropertyDirectoryTree::ptr result = std::make_unique<FilePropertyDirectoryTree>(rootDirectory.string(),"Home");
FillSampleDirectoryTree(result.get(),rootDirectory);
return result;
}
return result;
} }
+4 -2
View File
@@ -152,7 +152,9 @@ public:
int64_t DeleteBank(int64_t bankId); int64_t DeleteBank(int64_t bankId);
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty); std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
std::vector<FileEntry> GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty); FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
FileRequestResult GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty);
void SetJackChannelSelection(const JackChannelSelection&channelSelection); void SetJackChannelSelection(const JackChannelSelection&channelSelection);
@@ -224,7 +226,7 @@ public:
const std::string&oldRelativePath, const std::string&oldRelativePath,
const std::string&newRelativePath, const std::string&newRelativePath,
const UiFileProperty&uiFileProperty); const UiFileProperty&uiFileProperty);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty) const; FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty);
}; };
+152 -119
View File
@@ -1,4 +1,4 @@
// Copyright (c) 2024 Robin Davies // Copyright (c) 2024 Robin 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
@@ -32,7 +32,7 @@
#include "UpdaterSecurity.hpp" #include "UpdaterSecurity.hpp"
#include "TemporaryFile.hpp" #include "TemporaryFile.hpp"
#include "PresetBundle.hpp" #include "PresetBundle.hpp"
#include "json.hpp"
#define OLD_PRESET_EXTENSION ".piPreset" #define OLD_PRESET_EXTENSION ".piPreset"
#define PRESET_EXTENSION ".piPreset" #define PRESET_EXTENSION ".piPreset"
@@ -50,40 +50,52 @@ using namespace pipedal;
using namespace boost::system; using namespace boost::system;
namespace fs = std::filesystem; namespace fs = std::filesystem;
class UserUploadResponse
{
public:
std::string errorMessage_;
std::string path_;
DECLARE_JSON_MAP(UserUploadResponse);
};
JSON_MAP_BEGIN(UserUploadResponse)
JSON_MAP_REFERENCE(UserUploadResponse, errorMessage)
JSON_MAP_REFERENCE(UserUploadResponse, path)
JSON_MAP_END()
static bool IsZipFile(const std::filesystem::path &path) static bool IsZipFile(const std::filesystem::path &path)
{ {
std::ifstream f(path); std::ifstream f(path);
if (!f.is_open()) return false; if (!f.is_open())
return false;
char c[4]; char c[4];
memset(c,0,sizeof(c)); memset(c, 0, sizeof(c));
f >> c[0] >> c[1] >> c[2] >> c[3]; f >> c[0] >> c[1] >> c[2] >> c[3];
// official file header according to PKware documetnation. // official file header according to PKware documetnation.
return c[0] == 0x50 && c[1] == 0x4B && c[2] == 0x03 && c[3] == 0x04; return c[0] == 0x50 && c[1] == 0x4B && c[2] == 0x03 && c[3] == 0x04;
} }
class ExtensionChecker { class ExtensionChecker
{
public: public:
ExtensionChecker(const std::string&extensionList) ExtensionChecker(const std::string &extensionList)
:extensions(split(extensionList,',')) : extensions(split(extensionList, ','))
{ {
} }
bool IsValidExtension(const std::string&extension) bool IsValidExtension(const std::string &extension)
{ {
if (extensions.size() == 0) if (extensions.size() == 0)
return true; return true;
for (const auto&ext: extensions) for (const auto &ext : extensions)
{ {
if (ext == extension) if (ext == extension)
return true; return true;
} }
return false; return false;
} }
private: private:
std::vector<std::string> extensions; std::vector<std::string> extensions;
}; };
@@ -133,7 +145,8 @@ public:
else if (segment == "uploadBank") else if (segment == "uploadBank")
{ {
return true; return true;
} else if (segment == "uploadUserFile") }
else if (segment == "uploadUserFile")
{ {
return true; return true;
} }
@@ -159,7 +172,7 @@ public:
PluginPresets pluginPresets = model->GetPluginPresets(pluginUri); PluginPresets pluginPresets = model->GetPluginPresets(pluginUri);
std::stringstream s; std::stringstream s;
json_writer writer(s,false); json_writer writer(s, false);
writer.write(pluginPresets); writer.write(pluginPresets);
*pContent = s.str(); *pContent = s.str();
} }
@@ -176,7 +189,7 @@ public:
file.selectedPreset(newInstanceId); file.selectedPreset(newInstanceId);
std::stringstream s; std::stringstream s;
json_writer writer(s,false); json_writer writer(s, false);
writer.write(file); writer.write(file);
*pContent = s.str(); *pContent = s.str();
*pName = pedalboard.name(); *pName = pedalboard.name();
@@ -210,12 +223,11 @@ public:
std::string content; std::string content;
GetPluginPresets(request_uri, &name, &content); GetPluginPresets(request_uri, &name, &content);
TemporaryFile tmpFile { WEB_TEMP_DIR}; TemporaryFile tmpFile{WEB_TEMP_DIR};
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model),content); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model), content);
presetbundleWriter->WriteToFile(tmpFile.Path()); presetbundleWriter->WriteToFile(tmpFile.Path());
size_t contentLength = std::filesystem::file_size(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path());
res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE); res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE);
res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::cache_control, "no-cache");
res.setContentLength(content.length()); res.setContentLength(content.length());
@@ -228,8 +240,8 @@ public:
std::string content; std::string content;
GetPreset(request_uri, &name, &content); GetPreset(request_uri, &name, &content);
TemporaryFile tmpFile { WEB_TEMP_DIR}; TemporaryFile tmpFile{WEB_TEMP_DIR};
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content);
presetbundleWriter->WriteToFile(tmpFile.Path()); presetbundleWriter->WriteToFile(tmpFile.Path());
size_t contentLength = std::filesystem::file_size(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path());
@@ -245,14 +257,11 @@ public:
std::string content; std::string content;
GetBank(request_uri, &name, &content); GetBank(request_uri, &name, &content);
TemporaryFile tmpFile { WEB_TEMP_DIR}; TemporaryFile tmpFile{WEB_TEMP_DIR};
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content);
presetbundleWriter->WriteToFile(tmpFile.Path()); presetbundleWriter->WriteToFile(tmpFile.Path());
size_t contentLength = std::filesystem::file_size(tmpFile.Path()); size_t contentLength = std::filesystem::file_size(tmpFile.Path());
res.set(HttpField::content_type, BANK_MIME_TYPE); res.set(HttpField::content_type, BANK_MIME_TYPE);
res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::cache_control, "no-cache");
res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION)); res.set(HttpField::content_disposition, GetContentDispositionHeader(name, BANK_EXTENSION));
@@ -290,12 +299,11 @@ public:
std::string content; std::string content;
GetPluginPresets(request_uri, &name, &content); GetPluginPresets(request_uri, &name, &content);
std::shared_ptr<TemporaryFile> tmpFile =std::make_shared<TemporaryFile>(WEB_TEMP_DIR); std::shared_ptr<TemporaryFile> tmpFile = std::make_shared<TemporaryFile>(WEB_TEMP_DIR);
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model),content); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePluginPresetsFile(*(this->model), content);
presetbundleWriter->WriteToFile(tmpFile->Path()); presetbundleWriter->WriteToFile(tmpFile->Path());
size_t contentLength = std::filesystem::file_size(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path());
res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE); res.set(HttpField::content_type, PLUGIN_PRESETS_MIME_TYPE);
res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::cache_control, "no-cache");
res.setContentLength(contentLength); res.setContentLength(contentLength);
@@ -308,12 +316,11 @@ public:
std::string content; std::string content;
GetPreset(request_uri, &name, &content); GetPreset(request_uri, &name, &content);
std::shared_ptr<TemporaryFile> tmpFile =std::make_shared<TemporaryFile>(WEB_TEMP_DIR); std::shared_ptr<TemporaryFile> tmpFile = std::make_shared<TemporaryFile>(WEB_TEMP_DIR);
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content);
presetbundleWriter->WriteToFile(tmpFile->Path()); presetbundleWriter->WriteToFile(tmpFile->Path());
size_t contentLength = std::filesystem::file_size(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path());
res.set(HttpField::content_type, PRESET_MIME_TYPE); res.set(HttpField::content_type, PRESET_MIME_TYPE);
res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::cache_control, "no-cache");
res.setContentLength(contentLength); res.setContentLength(contentLength);
@@ -326,13 +333,11 @@ public:
std::string content; std::string content;
GetBank(request_uri, &name, &content); GetBank(request_uri, &name, &content);
std::shared_ptr<TemporaryFile> tmpFile =std::make_shared<TemporaryFile>(WEB_TEMP_DIR); std::shared_ptr<TemporaryFile> tmpFile = std::make_shared<TemporaryFile>(WEB_TEMP_DIR);
PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model),content); PresetBundleWriter::ptr presetbundleWriter = PresetBundleWriter::CreatePresetsFile(*(this->model), content);
presetbundleWriter->WriteToFile(tmpFile->Path()); presetbundleWriter->WriteToFile(tmpFile->Path());
size_t contentLength = std::filesystem::file_size(tmpFile->Path()); size_t contentLength = std::filesystem::file_size(tmpFile->Path());
res.set(HttpField::content_type, BANK_MIME_TYPE); res.set(HttpField::content_type, BANK_MIME_TYPE);
res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::cache_control, "no-cache");
res.setContentLength(contentLength); res.setContentLength(contentLength);
@@ -356,40 +361,49 @@ public:
} }
} }
} }
static std::string GetFirstFolderOrFile(const std::vector<std::string>&fileNames) static std::string GetFirstFolderOrFile(const std::vector<std::string> &fileNames)
{ {
for (const auto &fileName: fileNames) for (const auto &fileName : fileNames)
{ {
size_t nPos = fileName.find('/'); size_t nPos = fileName.find('/');
if (nPos != std::string::npos) { if (nPos != std::string::npos)
return fileName.substr(0,nPos); {
return fileName.substr(0, nPos);
} }
} }
if (fileNames.size() == 0) if (fileNames.size() == 0)
{ {
return 0; return 0;
} else { }
else
{
return fileNames[0]; return fileNames[0];
} }
} }
static bool HasSingleRootDirectory(ZipFileReader&zipFile) { static bool HasSingleRootDirectory(ZipFileReader &zipFile)
{
bool hasDirectory = false; bool hasDirectory = false;
std::string previousDirectory; std::string previousDirectory;
for (auto& file: zipFile.GetFiles()) for (auto &file : zipFile.GetFiles())
{ {
auto pos = file.find('/'); auto pos = file.find('/');
if (pos == std::string::npos) { if (pos == std::string::npos)
{
// a file in the root. // a file in the root.
return false; return false;
} else }
else
{ {
std::string currentRootDirectory = file.substr(0,pos); std::string currentRootDirectory = file.substr(0, pos);
if (hasDirectory) { if (hasDirectory)
{
if (currentRootDirectory != previousDirectory) if (currentRootDirectory != previousDirectory)
{ {
return false; return false;
} }
} else { }
else
{
hasDirectory = true; hasDirectory = true;
previousDirectory = currentRootDirectory; previousDirectory = currentRootDirectory;
} }
@@ -412,26 +426,26 @@ public:
{ {
PluginPresets presets; PluginPresets presets;
fs::path filePath = req.get_body_temporary_file(); fs::path filePath = req.get_body_temporary_file();
if (filePath.empty()) if (filePath.empty())
{ {
throw std::runtime_error("Unexpected."); throw std::runtime_error("Unexpected.");
} }
if (IsZipFile(filePath)) if (IsZipFile(filePath))
{ {
auto presetReader = PresetBundleReader::LoadPluginPresetsFile(*(this->model),filePath); auto presetReader = PresetBundleReader::LoadPluginPresetsFile(*(this->model), filePath);
presetReader->ExtractMediaFiles(); presetReader->ExtractMediaFiles();
std::stringstream ss(presetReader->GetPluginPresetsJson()); std::stringstream ss(presetReader->GetPluginPresetsJson());
json_reader reader(ss); json_reader reader(ss);
reader.read(&presets); reader.read(&presets);
}
} else { else
{
json_reader reader(req.get_body_input_stream()); json_reader reader(req.get_body_input_stream());
reader.read(&presets); reader.read(&presets);
} }
model->UploadPluginPresets(presets); model->UploadPluginPresets(presets);
res.set(HttpField::content_type, "application/json"); res.set(HttpField::content_type, "application/json");
res.set(HttpField::cache_control, "no-cache"); res.set(HttpField::cache_control, "no-cache");
std::stringstream sResult; std::stringstream sResult;
@@ -447,19 +461,21 @@ public:
BankFile bankFile; BankFile bankFile;
fs::path filePath = req.get_body_temporary_file(); fs::path filePath = req.get_body_temporary_file();
if (filePath.empty()) if (filePath.empty())
{ {
throw std::runtime_error("Unexpected."); throw std::runtime_error("Unexpected.");
} }
if (IsZipFile(filePath)) if (IsZipFile(filePath))
{ {
auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model),filePath); auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model), filePath);
presetReader->ExtractMediaFiles(); presetReader->ExtractMediaFiles();
std::stringstream ss(presetReader->GetPresetJson()); std::stringstream ss(presetReader->GetPresetJson());
json_reader reader(ss); json_reader reader(ss);
reader.read(&bankFile); reader.read(&bankFile);
} else { }
else
{
// legacy json format, no zip, no media files. // legacy json format, no zip, no media files.
json_reader reader(req.get_body_input_stream()); json_reader reader(req.get_body_input_stream());
reader.read(&bankFile); reader.read(&bankFile);
@@ -487,19 +503,21 @@ public:
BankFile bankFile; BankFile bankFile;
fs::path filePath = req.get_body_temporary_file(); fs::path filePath = req.get_body_temporary_file();
if (filePath.empty()) if (filePath.empty())
{ {
throw std::runtime_error("Unexpected."); throw std::runtime_error("Unexpected.");
} }
if (IsZipFile(filePath)) if (IsZipFile(filePath))
{ {
auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model),filePath); auto presetReader = PresetBundleReader::LoadPresetsFile(*(this->model), filePath);
presetReader->ExtractMediaFiles(); presetReader->ExtractMediaFiles();
std::stringstream ss(presetReader->GetPresetJson()); std::stringstream ss(presetReader->GetPresetJson());
json_reader reader(ss); json_reader reader(ss);
reader.read(&bankFile); reader.read(&bankFile);
} else { }
else
{
// legacy json format, no zip, no media files. // legacy json format, no zip, no media files.
json_reader reader(req.get_body_input_stream()); json_reader reader(req.get_body_input_stream());
reader.read(&bankFile); reader.read(&bankFile);
@@ -522,70 +540,88 @@ public:
res.setContentLength(result.length()); res.setContentLength(result.length());
res.setBody(result); res.setBody(result);
} else if (segment == "uploadUserFile") }
else if (segment == "uploadUserFile")
{ {
res.set(HttpField::content_type, "application/json"); UserUploadResponse result;
res.set(HttpField::cache_control, "no-cache"); try
std::string instanceId = request_uri.query("id");
std::string directory = request_uri.query("directory");
std::string filename = request_uri.query("filename");
std::string patchProperty = request_uri.query("property");
if (patchProperty.length() == 0 && directory.length() == 0)
{ {
throw PiPedalException("Malformed request.");
} res.set(HttpField::content_type, "application/json");
res.set(HttpField::cache_control, "no-cache");
std::string instanceId = request_uri.query("id");
std::string directory = request_uri.query("directory");
std::string filename = request_uri.query("filename");
std::string patchProperty = request_uri.query("property");
res.set(HttpField::content_type, "application/json"); if (patchProperty.length() == 0 && directory.length() == 0)
res.set(HttpField::cache_control, "no-cache"); {
throw PiPedalException("Malformed request.");
}
std::string outputFileName = std::filesystem::path(directory) / filename; res.set(HttpField::content_type, "application/json");
res.set(HttpField::cache_control, "no-cache");
if (filename.ends_with(".zip")) fs::path outputFileName = std::filesystem::path(directory) / filename;
{
ExtensionChecker extensionChecker { request_uri.query("ext") };
namespace fs = std::filesystem;
try { if (filename.ends_with(".zip"))
auto zipFile = ZipFileReader::Create(req.get_body_temporary_file()); {
std::vector<std::string> files = zipFile->GetFiles(); ExtensionChecker extensionChecker{request_uri.query("ext")};
bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile); namespace fs = std::filesystem;
if (!hasSingleRootDirectory) {
directory = (fs::path(directory) / fs::path(filename).filename().replace_extension("")).string(); try
}
for (const auto&inputFile : files)
{ {
if (!inputFile.ends_with("/")) // don't process directory entries. auto zipFile = ZipFileReader::Create(req.get_body_temporary_file());
std::vector<std::string> files = zipFile->GetFiles();
bool hasSingleRootDirectory = HasSingleRootDirectory(*zipFile);
if (!hasSingleRootDirectory)
{ {
fs::path inputPath { inputFile}; directory = (fs::path(directory) / fs::path(filename).filename().replace_extension("")).string();
std::string extension = inputPath.extension(); }
if (extensionChecker.IsValidExtension(extension)) for (const auto &inputFile : files)
{
if (!inputFile.ends_with("/")) // don't process directory entries.
{ {
auto si = zipFile->GetFileInputStream(inputFile); fs::path inputPath{inputFile};
std::string path = this->model->UploadUserFile(directory,patchProperty,inputFile, si,zipFile->GetFileSize(inputFile)); std::string extension = inputPath.extension();
if (extensionChecker.IsValidExtension(extension))
{
auto si = zipFile->GetFileInputStream(inputFile);
std::string path = this->model->UploadUserFile(directory, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile));
}
} }
} }
// set outputPath to the file or folder we would like focus to go to.
// almost always a single folder in the root.
std::string returnPath = GetFirstFolderOrFile(files);
outputFileName = fs::path(directory) / returnPath;
} }
// set outputPath to the file or folder we would like focus to go to. catch (const std::exception &e)
// almost always a single folder in the root. {
std::string returnPath = GetFirstFolderOrFile(files); Lv2Log::error(SS("Unzip failed. " << e.what()));
outputFileName = this->model->GetPluginUploadDirectory() / directory / returnPath; throw;
} catch (const std::exception &e) }
{ FileSystemSync();
Lv2Log::error(SS("Unzip failed. " << e.what())); }
throw; else
{
outputFileName = this->model->UploadUserFile(directory, patchProperty, filename, req.get_body_input_stream(), req.content_length());
} }
FileSystemSync();
} else { if (outputFileName.is_relative())
outputFileName = this->model->UploadUserFile(directory,patchProperty,filename,req.get_body_input_stream(), req.content_length()); {
outputFileName = this->model->GetPluginUploadDirectory() / outputFileName;
}
result.path_ = outputFileName.string();
}
catch (const std::exception &e)
{
result.errorMessage_ = e.what();
} }
std::stringstream ss; std::stringstream ss;
json_writer writer(ss); json_writer writer(ss);
writer.write(outputFileName); writer.write(result);
std::string response = ss.str(); std::string response = ss.str();
res.setContentLength(response.length()); res.setContentLength(response.length());
@@ -598,7 +634,7 @@ public:
} }
catch (const std::exception &e) catch (const std::exception &e)
{ {
Lv2Log::error(SS("Error uploading file: " << e.what()) ); Lv2Log::error(SS("Error uploading file: " << e.what()));
if (strcmp(e.what(), "Not found") == 0) if (strcmp(e.what(), "Not found") == 0)
{ {
ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory); ec = boost::system::errc::make_error_code(boost::system::errc::no_such_file_or_directory);
@@ -611,7 +647,6 @@ public:
} }
}; };
static std::string StripPortNumber(const std::string &fromAddress) static std::string StripPortNumber(const std::string &fromAddress)
{ {
std::string address = fromAddress; std::string address = fromAddress;
@@ -663,10 +698,10 @@ public:
std::stringstream s; std::stringstream s;
s << "{ \"socket_server_port\": " << portNumber s << "{ \"socket_server_port\": " << portNumber
<< ", \"socket_server_address\": \"" << webSocketAddress << ", \"socket_server_address\": \"" << webSocketAddress
<< "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize << "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize
<< ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true": "false") << ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false")
<< " }"; << " }";
return s.str(); return s.str();
} }
@@ -720,16 +755,14 @@ public:
}; };
void pipedal::ConfigureWebServer( void pipedal::ConfigureWebServer(
WebServer&server, WebServer &server,
PiPedalModel&model, PiPedalModel &model,
int port, int port,
size_t maxUploadSize) size_t maxUploadSize)
{ {
std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(port, maxUploadSize)}; std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(port, maxUploadSize)};
server.AddRequestHandler(interceptConfig); server.AddRequestHandler(interceptConfig);
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
server.AddRequestHandler(downloadIntercept);
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
server.AddRequestHandler(downloadIntercept);
} }