Improvements to file uploads. UI improvements for future looper and audio capture plugins.

This commit is contained in:
Robin E. R. Davies
2025-02-15 12:29:53 -05:00
parent b07f590b49
commit 09f73e26d6
41 changed files with 1714 additions and 626 deletions
+1
View File
@@ -142,6 +142,7 @@
//"[pipedal_alsa_test]"
// "[wifi_channels_test]"
// "[locale]"
"[modFileTypes]"
],
"stopAtEntry": false,
+21 -2
View File
@@ -105,15 +105,34 @@
"text_encoding": "cpp"
},
"cSpell.words": [
"Alsa",
"flac",
"Guitarix",
"hostapd",
"Hotspot",
"invada",
"jmap",
"Lilv",
"midiclip",
"midisong",
"mlmodel",
"Pedalboard",
"pipedal",
"pipedal_0",
"pipedalconfig",
"pipedald",
"pipedalshutdownd",
"pipedaltest",
"ToobAmp"
"pipedalui",
"rdfs",
"ToobAmp",
"urids",
"Writables",
"xruns"
],
"C_Cpp.default.compilerPath": "/usr/bin/g++-13"
"C_Cpp.default.compilerPath": "/usr/bin/g++-13",
"cSpell.ignoreWords": [
"nammodel"
],
"cSpell.enabled": false
}
+57 -22
View File
@@ -20,67 +20,102 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
#pragma once
#pragma once
#include <string>
#include <filesystem>
#include <vector>
#include <barrier>
#include <atomic>
#include <set>
namespace pipedal {
namespace pipedal
{
inline bool endsWith(const std::string& str, const std::string& suffix)
inline bool endsWith(const std::string &str, const std::string &suffix)
{
return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix);
return str.size() >= suffix.size() && 0 == str.compare(str.size() - suffix.size(), suffix.size(), suffix);
}
void SetThreadName(const std::string &name);
std::u32string ToUtf32(const std::string &s);
inline bool isPathSeperator(char c) { return c == '/' || c == std::filesystem::path::preferred_separator;}
inline bool isPathSeparator(char c) { return c == '/' || c == std::filesystem::path::preferred_separator; }
std::string GetHostName();
std::vector<std::string> split(const std::string &value, char delimiter);
inline void ReadBarrier() {
inline void ReadBarrier()
{
std::atomic_thread_fence(std::memory_order::acquire);
}
inline void WriteBarrier() {
inline void WriteBarrier()
{
std::atomic_thread_fence(std::memory_order::release);
}
template <typename T>
inline bool contains(const std::vector<T>&vector,const T&value)
inline bool contains(const std::vector<T> &vector, const T &value)
{
for (auto i = vector.begin(); i != vector.end(); ++i)
{
if ((*i) == value) {
if ((*i) == value)
{
return true;
}
}
return false;
}
inline bool contains(const std::vector<std::string>&vector,const char*value)
inline bool contains(const std::vector<std::string> &vector, const char *value)
{
return contains(vector,std::string(value));
return contains(vector, std::string(value));
}
class NoCopy {
class NoCopy
{
public:
NoCopy() { }
NoCopy(const NoCopy&) = delete;
NoCopy&operator=(const NoCopy&) = delete;
NoCopy() {}
NoCopy(const NoCopy &) = delete;
NoCopy &operator=(const NoCopy &) = delete;
~NoCopy() {}
};
class NoMove {
class NoMove
{
public:
NoMove() { }
NoMove(NoMove&&) = delete;
NoMove&operator=(NoMove&&) = delete;
~NoMove() { }
NoMove() {}
NoMove(NoMove &&) = delete;
NoMove &operator=(NoMove &&) = delete;
~NoMove() {}
};
bool IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path&rootPath);
template <typename T>
std::set<T> Intersect(const std::set<T> &left, const std::set<T> &right)
{
std::set<T> result;
for (const T &v : left)
{
if (right.contains(v))
{
result.insert(v);
}
}
return result;
}
template <typename T>
bool IsSubset(const std::set<T> &subset, const std::set<T> &set)
{
std::set<T> result;
for (const T &v : subset)
{
if (!set.contains(v)) return false;
}
return true;
}
// C locale to lower. Only does 'A'-'Z'.
std::string ToLower(const std::string&value);
std::filesystem::path MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath);
}
+53
View File
@@ -21,6 +21,7 @@
* SOFTWARE.
*/
#include "util.hpp"
#include "ss.hpp"
#include <pthread.h>
#include <thread>
@@ -136,3 +137,55 @@ std::vector<std::string> pipedal::split(const std::string &value, char delimiter
}
return result;
}
bool pipedal::IsChildDirectory(const std::filesystem::path &path, const std::filesystem::path&rootPath)
{
auto iter = path.begin();
for (auto i = rootPath.begin(); i != rootPath.end(); ++i)
{
if (iter == path.end() || *iter != *i)
{
return false;
}
++iter;
}
return true;
}
std::string pipedal::ToLower(const std::string&value)
{
std::string result;
result.resize(value.length());
for (size_t i = 0; i < value.length(); ++i)
{
result[i] = (char)tolower(value[i]);
}
return result;
}
std::filesystem::path pipedal::MakeRelativePath(const std::filesystem::path &path, const std::filesystem::path&parentPath)
{
if (path.is_relative()) return path;
auto iter = path.begin();
for (auto i = parentPath.begin(); i != parentPath.end(); ++i)
{
if (iter == path.end() || *iter != *i) {
// not a child directory of parent directory.
throw std::runtime_error("Not a child directory.");
}
++iter;
}
std::filesystem::path remander;
while (iter != path.end())
{
remander /= *iter++;
}
return remander;
}
+3 -3
View File
@@ -1,6 +1,4 @@
<img src="GithubBanner.png" width="100%"/>
<a href="Installing.html"><i>v1.3.69</i></a>
&nbsp;
@@ -8,7 +6,9 @@
To download PiPedal, click [here](download.md).
To view PiPedal documentation, click [here](Documentation.md).
#### Important notice: PiPedal has a submitted a request to support sharing of Pipedal presets at [PatchStorage.com](https:///patchstorage.com). We urgently need your support! Please vote for PiPedal [here](https://patchstorage.com/requests/pipedal/).
#### Important notice: [PatchStorage.com](https:///patchstorage.com) has added support for PiPedal. We urgently need your support! I you could please upload patches [here](https://patchstorage.com/platform/pipedal/) I would
be enormously grateful. Note that media and model files are included in patch files, so you can share patches that
include model files or audio samples. If you make use of plugins that are not included in the PiPedal distribution, please include a link to the plugin in the patch description.
#### NEW version 1.3.69 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
+69
View File
@@ -0,0 +1,69 @@
/*
* MIT License
*
* Copyright (c) 2024 Robin E. R. Davies
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as React from 'react';
import { Theme } from '@mui/material/styles';
import { WithStyles } from '@mui/styles';
import createStyles from '@mui/styles/createStyles';
import withStyles from '@mui/styles/withStyles';
const styles = (theme: Theme) => createStyles({
frame: {
position: "relative",
margin: "12px"
}
});
export interface MiniSpinnerProps extends WithStyles<typeof styles> {
theme: Theme
}
export interface MiniSpinnerProps {
size?: number;
};
const MiniSpinner = withStyles(styles)((props: MiniSpinnerProps) => {
const { classes } = props;
let size: number = 24;
if (props.size)
{
size = props.size;
}
let thickness = 3;
let r = size-thickness/2;
return (
<div style={{width:size, height: size}}>
<svg viewBox={ (size/2) + " " + (size/2) + " " + size + " " + size}> width={size} height={size}
<circle
cx="0" cy="0" r={r} fill="none" stroke={props.theme.palette.primary.main} strokeWidth={thickness}
/>
</svg>
</div>
);
});
+3 -1
View File
@@ -12,7 +12,9 @@ import JackHostStatus from './JackHostStatus';
import { PiPedalError } from './PiPedalError';
import DialogEx from './DialogEx';
import { Theme, createStyles } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import { Theme } from '@mui/material/styles';
import { withStyles, WithStyles } from '@mui/styles';
import Slide, { SlideProps } from '@mui/material/Slide';
+5 -3
View File
@@ -27,7 +27,9 @@ import ButtonBase from "@mui/material/ButtonBase";
import Slide, { SlideProps } from '@mui/material/Slide';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import { Theme, createStyles } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import { Theme } from '@mui/material/styles';
import { WithStyles, withStyles } from "@mui/styles";
import DraggableGrid, { ScrollDirection } from './DraggableGrid';
import Fade from '@mui/material/Fade';
@@ -304,8 +306,8 @@ const BankDialog = withStyles(styles, { withTheme: true })(
>
<SelectHoverBackground selected={bankEntry.instanceId === selectedItem} showHover={true} />
<div className={classes.itemFrame}>
<div className={classes.ListItemIcon}>
<BankIcon className={classes.listIcon}/>
<div className={classes.listIcon}>
<BankIcon />
</div>
<div className={classes.itemLabel}>
<Typography noWrap variant="body2" color="textPrimary">
+10 -3
View File
@@ -19,7 +19,9 @@
import React from 'react';
import { Theme, createStyles } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import { Theme } from '@mui/material/styles';
import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder';
import RenameDialog from './RenameDialog';
import Divider from '@mui/material/Divider';
@@ -32,7 +34,7 @@ import Button from '@mui/material/Button';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import AudioFileIcon from '@mui/icons-material/AudioFile';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
import FolderIcon from '@mui/icons-material/Folder';
import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
import DialogActions from '@mui/material/DialogActions';
@@ -92,6 +94,7 @@ function isAudioFile(filename: string) {
export interface FilePropertyDialogProps extends WithStyles<typeof styles> {
open: boolean,
fileProperty: UiFileProperty,
instanceId: number,
selectedFile: string,
onOk: (fileProperty: UiFileProperty, selectedItem: string) => void,
onCancel: () => void
@@ -557,7 +560,7 @@ export default withStyles(styles, { withTheme: true })(
}
if (fileEntry.isDirectory) {
return (
<FolderOutlinedIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
<FolderIcon style={{ flex: "0 0 auto", opacity: 0.7, marginRight: 8, marginLeft: 8 }} />
);
}
if (isAudioFile(fileEntry.pathname)) {
@@ -750,6 +753,8 @@ export default withStyles(styles, { withTheme: true })(
}
uploadPage={
"uploadUserFile?directory=" + encodeURIComponent(this.state.uploadDirectory)
+ "&id=" + this.props.instanceId.toString()
+ "&property=" + encodeURIComponent(this.props.fileProperty.patchProperty)
+ "&ext="
+ encodeURIComponent(this.getFileExtensionList(this.props.fileProperty))
}
@@ -794,8 +799,10 @@ export default withStyles(styles, { withTheme: true })(
(
<FilePropertyDirectorySelectDialog
open={this.state.moveDialogOpen}
dialogTitle="Move to"
uiFileProperty={this.props.fileProperty}
defaultPath={this.getDefaultPath()}
selectedFile={this.state.selectedFile}
excludeDirectory={
this.isDirectory(this.state.selectedFile)
? this.state.selectedFile : ""}
@@ -121,9 +121,11 @@ class DirectoryTree {
export interface FilePropertyDirectorySelectDialogProps {
open: boolean,
dialogTitle: string,
uiFileProperty: UiFileProperty;
defaultPath: string,
excludeDirectory: string,
selectedFile: string,
onOk: (path: string) => void,
onClose: () => void
};
@@ -181,7 +183,7 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
requestDirectoryTree() {
if (!this.props.open) return;
this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty)
this.model.getFilePropertyDirectoryTree(this.props.uiFileProperty,this.props.selectedFile)
.then((filePropertyDirectoryTree) => {
let myTree = new DirectoryTree(filePropertyDirectoryTree);
if (this.props.excludeDirectory.length !== 0)
@@ -327,7 +329,7 @@ export default class FilePropertyDirectorySelectDialog extends ResizeResponsiveC
PaperProps={{ style: { minHeight: "80%", maxHeight: "80%", minWidth: "75%", maxWidth: "75%", overflowY: "visible" } }}
>
<DialogTitle>
<Typography variant="body1">Select folder</Typography>
<Typography variant="body1">{this.props.dialogTitle}</Typography>
</DialogTitle>
<Divider />
<DialogContent style={{marginLeft: 0, paddingLeft: 0}} >
+2 -1
View File
@@ -43,7 +43,8 @@ import { FixedSizeGrid } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import StarBorderIcon from '@mui/icons-material/StarBorder';
import StarIcon from '@mui/icons-material/Star';
import { createStyles, Theme } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import { Theme } from '@mui/material/styles';
import { WithStyles, withStyles } from '@mui/styles';
import FilterListIcon from '@mui/icons-material/FilterList';
import FilterListOffIcon from '@mui/icons-material/FilterListOff';
+2 -2
View File
@@ -38,8 +38,8 @@ export const modDirectories: ModDirectory[] = [
{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
{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.
+2 -2
View File
@@ -2610,7 +2610,7 @@ export class PiPedalModel //implements PiPedalModel
});
}
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty): Promise<FilePropertyDirectoryTree> {
getFilePropertyDirectoryTree(uiFileProperty: UiFileProperty,selectedPath: string): Promise<FilePropertyDirectoryTree> {
return new Promise<FilePropertyDirectoryTree>((resolve, reject) => {
let ws = this.webSocket;
if (!ws) {
@@ -2619,7 +2619,7 @@ export class PiPedalModel //implements PiPedalModel
}
ws.request<FilePropertyDirectoryTree>(
"getFilePropertyDirectoryTree",
uiFileProperty
{fileProperty: uiFileProperty, selectedPath: selectedPath}
).then((result) => {
resolve(new FilePropertyDirectoryTree().deserialize(result));
}).catch((e) => {
+99 -36
View File
@@ -73,7 +73,7 @@ export const pluginControlStyles = (theme: Theme) => createStyles({
},
titleSection: {
flex: "0 0 auto", alignSelf:"stretch", marginBottom: 8, marginLeft: 0, marginRight: 0
flex: "0 0 auto", alignSelf: "stretch", marginBottom: 8, marginLeft: 0, marginRight: 0
},
displayValue: {
position: "absolute",
@@ -87,10 +87,10 @@ export const pluginControlStyles = (theme: Theme) => createStyles({
// zIndex: -1,
},
midSection: {
flex: "1 1 1", display: "flex",flexFlow: "column nowrap",alignContent: "center",justifyContent: "center"
flex: "1 1 1", display: "flex", flexFlow: "column nowrap", alignContent: "center", justifyContent: "center"
},
editSection: {
flex: "0 0 0", display: "flex", flexFlow: "column nowrap", justifyContent: "center",position: "relative", width: 60, height: 28,minHeight: 28
flex: "0 0 0", display: "flex", flexFlow: "column nowrap", justifyContent: "center", position: "relative", width: 60, height: 28, minHeight: 28
}
});
@@ -500,14 +500,12 @@ const PluginControl =
}
handleTriggerMouseDown() {
let uiControl = this.props.uiControl;
if (uiControl)
{
if (uiControl) {
let value = uiControl.max_value;
if (uiControl.max_value === uiControl.default_value)
{
if (uiControl.max_value === uiControl.default_value) {
value = uiControl.min_value;
}
this.model.sendPedalboardControlTrigger(this.props.instanceId,uiControl.symbol,value);
this.model.sendPedalboardControlTrigger(this.props.instanceId, uiControl.symbol, value);
}
}
handleTriggerMouseUp() {
@@ -650,13 +648,39 @@ const PluginControl =
}
let range: number;
if (uiControl.is_logarithmic) {
range = Math.log(value / uiControl.min_value) / Math.log(uiControl.max_value / uiControl.min_value);
let minValue = uiControl.min_value;
if (minValue === 0) // LSP plugins do this.
{
minValue = 0.0001;
}
range = Math.log(value / minValue) / Math.log(uiControl.max_value / minValue);
if (!isFinite(range)) {
if (range < 0)
{
range = 0;
} else {
range = 1.0;
}
} else if (isNaN(range)) {
range = 0;
}
if (uiControl.range_steps > 1) {
range = Math.round(range * (uiControl.range_steps - 1)) / (uiControl.range_steps - 1);
}
} else {
range = (value - uiControl.min_value) / (uiControl.max_value - uiControl.min_value);
if (!isFinite(range)) {
if (range < 0)
{
range = 0;
} else {
range = 1.0;
}
} else if (isNaN(range)) {
range = 0;
}
}
if (range > 1) range = 1;
if (range < 0) range = 0;
return range;
@@ -676,7 +700,17 @@ const PluginControl =
value = uiControl.min_value;
} else {
if (uiControl.is_logarithmic) {
value = uiControl.min_value * Math.pow(uiControl.max_value / uiControl.min_value, range);
let minValue = uiControl.min_value;
if (minValue === 0) // LSP controls.
{
minValue = 0.0001;
}
value = minValue * Math.pow(uiControl.max_value / minValue, range);
if (!isFinite(value)) {
value = uiControl.max_value;
} else if (isNaN(value)) {
value = uiControl.min_value;
}
} else {
value = range * (uiControl.max_value - uiControl.min_value) + uiControl.min_value;
}
@@ -731,23 +765,23 @@ const PluginControl =
}
let item_width: number | undefined = isSelect ? 160 : 80;
if (isTrigger)
{
if (isTrigger) {
item_width = undefined;
}
return (
<div ref={this.frameRef}
<div ref={this.frameRef}
className={this.props.classes.controlFrame}
style={{ width: item_width }}
>
>
{/* TITLE SECTION */}
<div className={this.props.classes.titleSection}
style={
{ alignSelf:"stretch", marginBottom: 8, marginLeft: isSelect ? 8 : 0, marginRight: 0
style={
{
alignSelf: "stretch", marginBottom: 8, marginLeft: isSelect ? 8 : 0, marginRight: 0
}}>
}}>
<ControlTooltip uiControl={control} >
<Typography variant="caption" display="block" noWrap style={{
width: "100%",
@@ -758,27 +792,56 @@ const PluginControl =
{/* CONTROL SECTION */}
<div className={this.props.classes.midSection}>
{isTrigger ? (
<Button variant="contained" color="primary" size="small"
onMouseDown={
(evt)=> { this.handleTriggerMouseDown(); }
}
onMouseUp={
(evt)=> { this.handleTriggerMouseUp(); }
}
style={{
textTransform: "none",
background: (isDarkMode() ? "#6750A4" : undefined),
marginLeft: 8, marginRight: 8,minWidth:60,
marginTop: 0
}}
{isTrigger ?
(
control.name.length !== 1 ? (
<Button variant="contained" color="primary" size="small"
onMouseDown={
(evt) => { this.handleTriggerMouseDown(); }
}
onMouseUp={
(evt) => { this.handleTriggerMouseUp(); }
}
style={{
textTransform: "none",
background: (isDarkMode() ? "#6750A4" : undefined),
marginLeft: 8, marginRight: 8, minWidth: 60,
marginTop: 0
}}
>
{control.name}
</Button>
) : (
<Button variant="contained" color="primary" size="small"
onMouseDown={
(evt) => { this.handleTriggerMouseDown(); }
}
onMouseUp={
(evt) => { this.handleTriggerMouseUp(); }
}
style={{
textTransform: "none",
background: (isDarkMode() ? "#6750A4" : undefined),
marginLeft: 8, marginRight: 8,
paddingLeft: 0, paddingRight: 0,
width: 36, height: 36,
marginTop: 0,
borderRadius: 8,
minWidth: 0,
fontSize: "1.2em"
}}
>
{control.name}
</Button>
)
)
>
{control.name}
</Button>
)
: ((isSelect || isAbSwitch || isOnOffSwitch) ? (
this.makeSelect(control, value)
) : (
+1
View File
@@ -767,6 +767,7 @@ const PluginControlView =
<FilePropertyDialog open={this.state.showFileDialog}
fileProperty={this.state.dialogFileProperty}
instanceId={this.props.instanceId}
selectedFile={this.state.dialogFileValue}
onCancel={() => {
this.setState({ showFileDialog: false });
+2 -1
View File
@@ -43,7 +43,8 @@ import EditIcon from '@mui/icons-material//Edit';
import RenameDialog from './RenameDialog';
import Slide, {SlideProps} from '@mui/material/Slide';
import { createStyles, Theme } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import { Theme } from '@mui/material/styles';
import { WithStyles, withStyles} from '@mui/styles';
import {ReactComponent as PluginPresetIcon} from "./svg/ic_pluginpreset2.svg";
+2 -1
View File
@@ -43,7 +43,8 @@ import EditIcon from '@mui/icons-material//Edit';
import RenameDialog from './RenameDialog';
import Slide, {SlideProps} from '@mui/material/Slide';
import { createStyles, Theme } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import { Theme } from '@mui/material/styles';
import { WithStyles, withStyles} from '@mui/styles';
import { ReactComponent as DownloadIcon} from './svg/file_download_black_24dp.svg';
import { ReactComponent as UploadIcon} from './svg/file_upload_black_24dp.svg';
+2 -1
View File
@@ -49,7 +49,8 @@ import SystemMidiBindingsDialog from './SystemMidiBindingsDialog';
import SelectThemeDialog from './SelectThemeDialog';
import Slide, { SlideProps } from '@mui/material/Slide';
import { createStyles, Theme } from '@mui/material/styles';
import createStyles from '@mui/styles/createStyles';
import { Theme } from '@mui/material/styles';
import { WithStyles, withStyles } from '@mui/styles';
import { canScaleWindow, getWindowScaleOptions, getWindowScaleText, setWindowScale, getWindowScale } from './WindowScale';
import OptionsDialog from './OptionsDialog';
+31 -11
View File
@@ -284,23 +284,39 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
return this.state.files.length !== 0;
}
getIcon(status: FileUploadStatus) {
let size = 14;
private innerGetIcon(status: FileUploadStatus)
{
let size=16;
let style = { width: size, height: size, opacity: 0.75 };
let top = 0;
switch (status) {
case FileUploadStatus.Error:
return (<div style={{ position: "relative", top: top }}><ErrorIcon color="error" style={style} /></div>);
return (<ErrorIcon color="error" style={style} />);
case FileUploadStatus.Uploaded:
return (<div style={{ position: "relative", top: top }}><CheckCircleOutlineIcon color="success" style={style}/></div >);
return (
<CheckCircleOutlineIcon color="success" style={style}/>);
case FileUploadStatus.Uploading:
return (<CircularProgress size={size } style={{ margin: 2 }} />);
return (<CircularProgress size={size + "px"} />);
default:
return (<SvgIcon style={{ width: size, height: size, }} />);
}
return (<SvgIcon style={{ width: size, height: size, }} />)
}
}
getIcon(status: FileUploadStatus) {
let size = 16;
//let top = 0;
return (
<div style={{ width: size, position: "relative", top: 1 }}>
{
this.innerGetIcon(status)
}
</div>
);
}
makeBrowserAcceptList() {
let result = UiFileType.MergeMimeTypes(this.props.fileProperty.fileTypes);
@@ -368,9 +384,13 @@ export default class UploadFileDialog extends ResizeResponsiveComponent<UploadFi
return (
<tr>
<td style={{ verticalAlign: "middle" }}>
<div ref={myRef} style={{ display: "flex", flexFlow: "row nowrap", alignItems: "center" }}>
<div ref={myRef} style={{
display: "flex",
flexFlow: "row nowrap",
alignItems: "center",
}}>
{this.getIcon(upload.status)}
<Typography variant="caption" noWrap display="block" style={{ marginLeft: 4 }}> {upload.name} </Typography>
<Typography variant="caption" noWrap display="inline-block" style={{ marginLeft: 4 }}> {upload.name} </Typography>
</div>
</td>
<td style={{ verticalAlign: "middle" }}>
+6 -3
View File
@@ -190,6 +190,7 @@ set (PIPEDAL_SOURCES
CpuTemperatureMonitor.cpp CpuTemperatureMonitor.hpp
SchedulerPriority.hpp SchedulerPriority.cpp
ModFileTypes.cpp ModFileTypes.hpp
MimeTypes.cpp MimeTypes.hpp
PatchPropertyWriter.hpp
PresetBundle.cpp PresetBundle.hpp
RealtimeMidiEventType.hpp
@@ -208,9 +209,8 @@ set (PIPEDAL_SOURCES
FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp
FileEntry.cpp FileEntry.hpp
atom_object.hpp atom_object.cpp
FileBrowserFiles.h
lv2ext/pipedal.lv2/ext/fileBrowser.h
FileBrowserFilesFeature.hpp FileBrowserFilesFeature.cpp
MimeTypes.cpp MimeTypes.hpp
inverting_mutex.hpp
DbDezipper.hpp DbDezipper.cpp
WebServerLog.hpp
@@ -372,8 +372,10 @@ set_target_properties(hotspotManagerTest PROPERTIES EXCLUDE_FROM_ALL true)
add_executable(pipedaltest testMain.cpp
add_executable(pipedaltest
testMain.cpp
ModFileTypesTest.cpp
jsonTest.cpp
UpdaterTest.cpp
@@ -692,6 +694,7 @@ add_executable(pipedalconfig
alsaCheck.hpp
BootConfig.cpp BootConfig.hpp
ModFileTypes.cpp ModFileTypes.hpp
MimeTypes.cpp MimeTypes.hpp
PiPedalConfiguration.hpp PiPedalConfiguration.cpp
JackServerSettings.hpp JackServerSettings.cpp
SystemConfigFile.hpp SystemConfigFile.cpp
-138
View File
@@ -1,138 +0,0 @@
/*
* Copyright (c) 2023 Robin E. R. Davies
* All rights reserved.
* 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.
*/
#ifndef FILEBROWSERFILES_H
#define FILEBROWSERFILES_H
#define LV2_FILEBROWSER_URI "http://two-play.com/ns/ext/fileBrowser"
#define LV2_FILEBROWSER_PREFIX LV2_FILEBROWSER_URI "#"
#define LV2_FILEBROWSER__files LV2_FILEBROWSER_PREFIX "files" ///< http://two-play.com/ns/ext/fileBrowser#files >
/**
@defgroup filedialog LV2_FileBrowser_Files
@ingroup lv2
A feature for LV2 plugins to provide sample files (or other selectable files) within the plugin's resource bundle to host file selection dialogs.
Browser directories may contain actual files that typically have been uploaded by users; but may also contain symlinks to files
in other locations. This feature allows plugins to publish files within the plugin's resource bundle to browser directories. For
each published file consists of a symlink in the browser directory that links to a corresponding file in the plugins resource bundle.
The LV2_FileBrowser_Files.publish_resource_files() method allows plugins to publish entire directories. publish_resource_files()
implements a versioning system, which tracks whether sample files have been deleted by the user. If the symlink for sample files that were installed
by a previous version of the plugin have been deleted, the sample files from the current version are not reinstalled. See LV2_FileBrowser_Files.publish_resource_files.
not re-installed.
See: <http://rerdavies.github.io/pipedal/TBD XXX.html> for more details.
@{
*/
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
LV2_FileBrowser_Status_Success = 0,
LV2_FileBrowser_Status_Err_Filesystem = 1,
LV2_FileBrowser_Status_Err_InvalidParameter = 1,
} LV2_FileBrowser_Status;
typedef void*
LV2_FileBrowser_Files_Handle; ///< Opaque handle for pipedal:uploadPath feature
/**
Feature data for filedialog:files (@ref LV2_FILEBROWSER_file).
*/
typedef struct {
/**
Opaque host data.
*/
LV2_FileBrowser_Files_Handle handle;
LV2_FileBrowser_Status (*publish_resource_files)(LV2_FileBrowser_Files_Handle handle,uint32_t version,const char*resourcePath, const char*uploadDirectory);
/**
Return a path in which upload files will be stored for this plugin.
@param handle MUST be the `handle` member of this struct.
@param path The sub-directory name in which uploaded files will be stored.
@return The absolute path to use for the new directory.
This function can be used by plugins to get the path of files or directories
that can be browsed by file dialogs. get_filebrowser_path, unlike map_path() does
not create or modify files in any way.
The caller must free memory for the returned value with LV2_FileBrowser_Files.free_path().
Example:
[code]
TBD
[/code]
*/
char* (*get_upload_path)(LV2_FileBrowser_Files_Handle handle, const char* path);
/**
Map resource bundle filenames to upload directories.
@param handle MUST be the `handle` member of this struct.
@param path File path name
@param fileBrowserDirectory relative directory of the file browser directory in which to create a link.
@return The absolute pathname of a file in the resource directory.
The primary purpose of this method is to map file references to files in the plugin's bundle
directory to corresponding files in the a file browser directory.
If the path is relative, the full path is resolved using the `rootResourceDirectory` path.
If the full path is not a child of rootResourceDirectory or if the path is relative,
get_upload_path returns the supplied path unmodified.
`
If the filename is a child of rootResourceDirectory, creates a corresponding link to the file in the file browser
directory and returns the path of that link.
The caller must free memory for the returned value with `LV2_FileBrowser_Files.free_path()`.
Example:
*/
char* (*map_path)(LV2_FileBrowser_Files_Handle handle, const char* path, const char*rootResourceDirectory, const char*fileBrowserDirectory);
/**
Free a path returned by LV2_FileBrowser_Files.get_upload_path() or LV2_FileBrowser_Files.map_path()
@param handle MUST be the `handle` member of this struct.
@param path A path previously returned by a method of `LV2_FileBrowser_Files`.
*/
void (*free_path)(LV2_FileBrowser_Files_Handle handle, char* path);
} LV2_FileBrowser_Files;
#ifdef __cplusplus
} // extern "C"
#endif
#endif // FILEBROWSERFILES_H
+74 -6
View File
@@ -30,9 +30,11 @@
#include <stdexcept>
#include "util.hpp"
#include "ofstream_synced.hpp"
#include "ModFileTypes.hpp"
using namespace pipedal;
using namespace pipedal::implementation;
namespace fs = std::filesystem;
namespace pipedal::implementation {
@@ -128,7 +130,9 @@ void FileBrowserFilesFeature::Initialize(
lv2_log_logger_init(&this->lv2Logger,const_cast<LV2_URID_Map *>(lv2Map),const_cast<LV2_Log_Log *>(lv2Log));
this->bundleDirectory = bundleDirectory;
this->browserDirectory = browserDirectory;
this->browserRootDirectory = browserDirectory;
this->privateDirectory = browserDirectory / this->bundleDirectory.filename();
MakeDirectoryMap(browserDirectory);
featureData.handle = (void *)this;
@@ -155,9 +159,73 @@ LV2_FileBrowser_Status FileBrowserFilesFeature::FN_publish_resource_files(LV2_Fi
return ((FileBrowserFilesFeature *)handle)->PublishResourceFiles(version, resourcePath, uploadDirectory);
}
const char*FileBrowserFilesFeature::GetWellKnownDirectory(const std::string&directory)
{
if (strcmp(directory.c_str(), "~") == 0) {
return privateDirectory.c_str();
}
const auto &f = g_wellKnownDirectoryMap->find(directory);
if (f != g_wellKnownDirectoryMap->end())
{
return f->second.c_str();
}
return nullptr;
}
using WellKnownDirectoryMap = std::map<std::string,std::string>;
std::mutex FileBrowserFilesFeature::g_DirectoryMap_mutex;
std::unique_ptr<WellKnownDirectoryMap> FileBrowserFilesFeature::g_wellKnownDirectoryMap;
void FileBrowserFilesFeature::MakeDirectoryMap(const std::filesystem::path&rootBrowserDirectory)
{
std::lock_guard lock { g_DirectoryMap_mutex};
if (!g_wellKnownDirectoryMap)
{
g_wellKnownDirectoryMap = std::make_unique<WellKnownDirectoryMap>();
for (const auto&modDirectory: ModFileTypes::ModDirectories())
{
(*g_wellKnownDirectoryMap)[modDirectory.modType] =
(rootBrowserDirectory / modDirectory.pipedalPath).string();
}
}
}
char *FileBrowserFilesFeature::GetUploadPath(const char *fileBrowserPath)
{
std::filesystem::path result = this->browserDirectory / fileBrowserPath;
if (fileBrowserPath == nullptr | fileBrowserPath[0] == '\0')
{
return strdup("");
}
std::filesystem::path path {fileBrowserPath};
auto iter = path.begin();
if (iter == path.end())
{
return strdup("");
}
std::string firstSegment = iter->string();
const auto f = (g_wellKnownDirectoryMap)->find(firstSegment);
if (f == g_wellKnownDirectoryMap->end())
{
return nullptr;
}
fs::path result = f->second;
++iter;
while (iter != path.end())
{
result /= (*iter);
++iter;
}
return strdup(result.c_str());
}
void FileBrowserFilesFeature::FreePath(char *path)
@@ -224,7 +292,7 @@ char *FileBrowserFilesFeature::MapPath(const char *path,const char*resourcePathB
} else {
targetDirectory = fileBrowserPath;
}
targetDirectory = this->browserDirectory / targetDirectory;
targetDirectory = this->browserRootDirectory / targetDirectory;
std::filesystem::create_directories(targetDirectory);
std::filesystem::path targetPath = targetDirectory / relativePath;
@@ -297,7 +365,7 @@ LV2_FileBrowser_Status FileBrowserFilesFeature::PublishResourceFiles(uint32_t ve
static std::string VERSION_FILENAME_PREFIX = ".versionInfo.";
// generate a filename that will allow multiple plugins to install files into the same directory.
std::filesystem::path versionFileName = this->browserDirectory / uploadDirectory /
std::filesystem::path versionFileName = this->browserRootDirectory / uploadDirectory /
(VERSION_FILENAME_PREFIX + GetPluginTag(this->bundleDirectory));
bool loaded = versionInfo.Load(versionFileName);
@@ -325,8 +393,8 @@ LV2_FileBrowser_Status FileBrowserFilesFeature::PublishResourceFiles(uint32_t ve
LogError("uploadDirectory must be relative.");
return LV2_FileBrowser_Status::LV2_FileBrowser_Status_Err_InvalidParameter;
}
targetDirectory = this->browserDirectory / targetDirectory;
PublishRecursive(versionInfo, sourcePath,sourcePath, this->browserDirectory / uploadDirectory);
targetDirectory = this->browserRootDirectory / targetDirectory;
PublishRecursive(versionInfo, sourcePath,sourcePath, this->browserRootDirectory / uploadDirectory);
versionInfo.Save(versionFileName);
} catch (const std::exception & e)
{
+16 -2
View File
@@ -24,10 +24,12 @@
#pragma once
#include "FileBrowserFiles.h"
#include "lv2ext/pipedal.lv2/ext/fileBrowser.h"
#include <filesystem>
#include <lv2/core/lv2.h>
#include <lv2/log/logger.h>
#include <mutex>
#include <filesystem>
@@ -47,20 +49,32 @@ namespace pipedal {
const std::string&browserDirectory);
const LV2_Feature*GetFeature() { return &feature; }
private:
static void MakeDirectoryMap(const std::filesystem::path&rootBrowserDirectory);
using WellKnownDirectoryMap = std::map<std::string,std::string>;
static std::mutex g_DirectoryMap_mutex;
static std::unique_ptr<WellKnownDirectoryMap> g_wellKnownDirectoryMap;
LV2_Feature feature;
const LV2_URID_Map *lv2Map = nullptr;
LV2_Log_Logger lv2Logger = {nullptr,0,0,0,0};
std::filesystem::path bundleDirectory;
std::filesystem::path browserDirectory;
std::filesystem::path browserRootDirectory;
std::filesystem::path privateDirectory;
LV2_FileBrowser_Files featureData;
std::map<std::string,std::string> wellKnownPaths;
static char* FN_get_upload_path(LV2_FileBrowser_Files_Handle handle, const char* fileBrowserDirectory);
static char* FN_map_path(LV2_FileBrowser_Files_Handle handle, const char* path, const char *resourcePathBase,const char*fileBrowserDirectory);
static void FN_free_path(LV2_FileBrowser_Files_Handle handle, char* path);
static LV2_FileBrowser_Status FN_publish_resource_files(LV2_FileBrowser_Files_Handle handle,uint32_t version,const char*resourcePath, const char*uploadDirectory);
const char*GetWellKnownDirectory(const std::string&directory);
void LogError(const char*message);
char* GetUploadPath(const char* fileBrowserDirectory);
char* MapPath(const char* path,const char*resourcePathBase,const char* fileBrowserDirectory);
+10 -10
View File
@@ -136,7 +136,7 @@ static bool IsIpv4OnLocalSubnet(uint32_t ipv4Addres)
for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next)
{
if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
{ // TODO: Add support for AF_INET6
uint32_t netmask = htonl(((sockaddr_in *)(p->ifa_netmask))->sin_addr.s_addr);
uint32_t ifAddr = htonl(((sockaddr_in *)(p->ifa_addr))->sin_addr.s_addr);
@@ -310,7 +310,7 @@ bool pipedal::IsOnLocalSubnet(const std::string &fromAddress)
for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next)
{
if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
if (p->ifa_addr && p->ifa_addr && p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
{
struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr);
@@ -353,7 +353,7 @@ std::vector<std::string> pipedal::GetEthernetIpv4Addresses()
{
if (isEthernetAddress(p->ifa_name))
{
if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
uint32_t netmask = htonl(((sockaddr_in *)(p->ifa_netmask))->sin_addr.s_addr);
uint32_t ifAddr = htonl(((sockaddr_in *)(p->ifa_addr))->sin_addr.s_addr);
{
@@ -378,7 +378,7 @@ static std::string GetInterfaceForIp4Address(uint32_t ipv4Address)
std::string result;
for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next)
{
if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
{ // TODO: Add support for AF_INET6
uint32_t netmask = htonl(((sockaddr_in *)(p->ifa_netmask))->sin_addr.s_addr);
uint32_t ifAddr = htonl(((sockaddr_in *)(p->ifa_addr))->sin_addr.s_addr);
@@ -402,7 +402,7 @@ static std::string GetInterfaceForIp6Address(const in6_addr inetAddr6)
std::string result;
for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next)
{
if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
{ // TODO: Add support for AF_INET6
struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr);
struct sockaddr_in6 *pNetMask = (struct sockaddr_in6 *)(p->ifa_netmask);
@@ -441,7 +441,7 @@ static std::string GetNonLinkLocalAddress(const boost::asio::ip::address_v6 &ipV
// get the nameof the interface for this scopeId.
for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next)
{
if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr)
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr)
{ // TODO: Add support for AF_INET6
struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr);
if (pAddr->sin6_scope_id == scopeId)
@@ -459,7 +459,7 @@ static std::string GetNonLinkLocalAddress(const boost::asio::ip::address_v6 &ipV
// an IPV4 address would be the ideal result.
for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next)
{
if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
{
if (strcmp(p->ifa_name, targetInterfaceName) == 0)
{
@@ -503,7 +503,7 @@ static std::string GetNonLinkLocalAddressForInterface(const std::string &name)
for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next)
{
if (p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET6 && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
{ // TODO: Add support for AF_INET6
struct sockaddr_in6 *pAddr = (struct sockaddr_in6 *)(p->ifa_addr);
if (!IN6_IS_ADDR_LINKLOCAL(&(pAddr->sin6_addr)))
@@ -547,8 +547,8 @@ static std::string GetNonLinkLocalAddressForIp4Interface(const std::string &name
std::string result = "";
for (ifaddrs *p = ifap; p != nullptr; p = p->ifa_next)
{
if (p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
{ // TODO: Add support for AF_INET6
if (p->ifa_addr && p->ifa_addr->sa_family == AF_INET && p->ifa_addr != nullptr && p->ifa_netmask != nullptr)
{
struct sockaddr_in *pAddr = (struct sockaddr_in *)(p->ifa_addr);
{
if (name == p->ifa_name)
+116 -88
View File
@@ -20,99 +20,22 @@
#include "MimeTypes.hpp"
#include "ss.hpp"
#include <mutex>
#include <memory>
using namespace pipedal;
void MimeTypes::MaybeInitialize()
{
if (!initialized)
{
initialized = true;
AddMimeType("MP3", "audio/mpeg");
AddMimeType("MPGA", "audio/mpeg");
AddMimeType("M4A", "audio/mp4");
AddMimeType("WAV", "audio/x-wav");
AddMimeType("WAV", "audio/wav");
AddMimeType("AMR", "audio/amr");
AddMimeType("AWB", "audio/amr-wb");
AddMimeType("WMA", "audio/x-ms-wma");
AddMimeType("OGG", "audio/ogg");
AddMimeType("OGG", "application/ogg");
AddMimeType("OGA", "application/ogg");
AddMimeType("AAC", "audio/aac");
AddMimeType("AAC", "audio/aac-adts");
AddMimeType("MKA", "audio/x-matroska");
AddMimeType("MID", "audio/midi");
AddMimeType("MIDI", "audio/midi");
AddMimeType("XMF", "audio/midi");
AddMimeType("RTTTL", "audio/midi");
AddMimeType("SMF", "audio/sp-midi");
AddMimeType("IMY", "audio/imelody");
AddMimeType("RTX", "audio/midi");
AddMimeType("OTA", "audio/midi");
AddMimeType("MXMF", "audio/midi");
AddMimeType("MPEG", "video/mpeg");
AddMimeType("MPG", "video/mpeg");
AddMimeType("MP4", "video/mp4");
AddMimeType("M4V", "video/mp4");
AddMimeType("3GP", "video/3gpp");
AddMimeType("3GPP", "video/3gpp");
AddMimeType("3G2", "video/3gpp2");
AddMimeType("3GPP2", "video/3gpp2");
AddMimeType("MKV", "video/x-matroska");
AddMimeType("WEBM", "video/webm");
AddMimeType("TS", "video/mp2ts");
AddMimeType("AVI", "video/avi");
AddMimeType("WMV", "video/x-ms-wmv");
AddMimeType("ASF", "video/x-ms-asf");
AddMimeType("JPG", "image/jpeg");
AddMimeType("JPEG", "image/jpeg");
AddMimeType("GIF", "image/gif");
AddMimeType("PNG", "image/png");
AddMimeType("BMP", "image/x-ms-bmp");
AddMimeType("WBMP", "image/vnd.wap.wbmp");
AddMimeType("WEBP", "image/webp");
AddMimeType("M3U", "audio/x-mpegurl");
AddMimeType("M3U", "application/x-mpegurl");
AddMimeType("PLS", "audio/x-scpls");
AddMimeType("WPL", "application/vnd.ms-wpl");
AddMimeType("M3U8", "application/vnd.apple.mpegurl");
AddMimeType("M3U8", "audio/mpegurl");
AddMimeType("M3U8", "audio/x-mpegurl");
AddMimeType("FL", "application/x-android-drm-fl");
AddMimeType("TXT", "text/plain");
AddMimeType("HTM", "text/html");
AddMimeType("HTML", "text/html");
AddMimeType("PDF", "application/pdf");
AddMimeType("DOC", "application/msword");
AddMimeType("XLS", "application/vnd.ms-excel");
AddMimeType("PPT", "application/mspowerpoint");
AddMimeType("FLAC", "audio/x-flac");
AddMimeType("FLAC", "audio/flac");
AddMimeType("ZIP", "application/zip");
AddMimeType("MPG", "video/mp2p");
AddMimeType("MPEG", "video/mp2p");
}
}
static MimeTypes staticContruct;
static std::string empty;
const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension)
const std::string& MimeTypes::MimeTypeFromExtension(const std::string &extension) const
{
MaybeInitialize();
auto iter = extensionToMimeType.find(extension);
if (iter == extensionToMimeType.end()) return empty;
return iter->second;
}
const std::string& MimeTypes::ExtensionFromMimeType(const std::string &mimeType)
const std::string& MimeTypes::ExtensionFromMimeType(const std::string &mimeType) const
{
MaybeInitialize();
auto iter = mimeTypeToExtension.find(mimeType);
if (iter == mimeTypeToExtension.end()) return empty;
return iter->second;
@@ -135,27 +58,132 @@ static std::string toLower(const std::string&value)
return result;
}
void MimeTypes::AddMimeType(const std::string&extension_, const std::string&mimeType)
void MimeTypes::AddMimeType(const std::string&extension_, const std::string&mimeType)
{
std::string extension = SS("." << toLower(extension_));
mimeTypeToExtensions[mimeType].insert(extension);
mimeTypeToExtension[mimeType] = extension;
extensionToMimeType[extension] = mimeType;
if (mimeType.starts_with("audio/"))
if (mimeType.starts_with("audio/midi"))
{
midiExtensions.insert(extension);
} else if (mimeType.starts_with("audio/"))
{
mimeTypeToExtensions["audio/*"].insert(extension);
audioExtensions.insert(extension);
}
if (mimeType.starts_with("video/"))
{
mimeTypeToExtensions["video/*"].insert(extension);
videoExtensions.insert(extension);
}
}
std::map<std::string,std::string> MimeTypes::mimeTypeToExtension;
std::map<std::string,std::string> MimeTypes::extensionToMimeType;
std::set<std::string> MimeTypes::audioExtensions;
std::set<std::string> MimeTypes::videoExtensions;
bool MimeTypes::initialized;
const std::set<std::string> &MimeTypes::AudioExtensions() const
{
return audioExtensions;
}
const std::set<std::string> &MimeTypes::VideoExtensions() const
{
return videoExtensions;
}
const std::set<std::string> &MimeTypes::MidiExtensions() const
{
return midiExtensions;
}
static std::mutex m_instanceMutex;
static std::unique_ptr<MimeTypes> m_instance;
MimeTypes::MimeTypes()
{
AddMimeType("MP3", "audio/mpeg");
AddMimeType("MPGA", "audio/mpeg");
AddMimeType("M4A", "audio/mp4");
AddMimeType("WAV", "audio/x-wav");
AddMimeType("WAV", "audio/wav");
AddMimeType("AMR", "audio/amr");
AddMimeType("AWB", "audio/amr-wb");
AddMimeType("WMA", "audio/x-ms-wma");
AddMimeType("OGG", "audio/ogg");
AddMimeType("OGG", "application/ogg");
AddMimeType("OGA", "application/ogg");
AddMimeType("AAC", "audio/aac");
AddMimeType("AAC", "audio/aac-adts");
AddMimeType("MKA", "audio/x-matroska");
AddMimeType("MID", "audio/midi");
AddMimeType("MIDI", "audio/midi");
AddMimeType("XMF", "audio/midi");
AddMimeType("RTTTL", "audio/midi");
AddMimeType("SMF", "audio/sp-midi");
AddMimeType("IMY", "audio/imelody");
AddMimeType("RTX", "audio/midi");
AddMimeType("OTA", "audio/midi");
AddMimeType("MXMF", "audio/midi");
AddMimeType("MPEG", "video/mpeg");
AddMimeType("MPG", "video/mpeg");
AddMimeType("MP4", "video/mp4");
AddMimeType("M4V", "video/mp4");
AddMimeType("3GP", "video/3gpp");
AddMimeType("3GPP", "video/3gpp");
AddMimeType("3G2", "video/3gpp2");
AddMimeType("3GPP2", "video/3gpp2");
AddMimeType("MKV", "video/x-matroska");
AddMimeType("WEBM", "video/webm");
AddMimeType("TS", "video/mp2ts");
AddMimeType("AVI", "video/avi");
AddMimeType("WMV", "video/x-ms-wmv");
AddMimeType("ASF", "video/x-ms-asf");
AddMimeType("JPG", "image/jpeg");
AddMimeType("JPEG", "image/jpeg");
AddMimeType("GIF", "image/gif");
AddMimeType("PNG", "image/png");
AddMimeType("BMP", "image/x-ms-bmp");
AddMimeType("WBMP", "image/vnd.wap.wbmp");
AddMimeType("WEBP", "image/webp");
AddMimeType("M3U", "audio/x-mpegurl");
AddMimeType("M3U", "application/x-mpegurl");
AddMimeType("PLS", "audio/x-scpls");
AddMimeType("WPL", "application/vnd.ms-wpl");
AddMimeType("M3U8", "application/vnd.apple.mpegurl");
AddMimeType("M3U8", "audio/mpegurl");
AddMimeType("M3U8", "audio/x-mpegurl");
AddMimeType("FL", "application/x-android-drm-fl");
AddMimeType("TXT", "text/plain");
AddMimeType("HTM", "text/html");
AddMimeType("HTML", "text/html");
AddMimeType("PDF", "application/pdf");
AddMimeType("DOC", "application/msword");
AddMimeType("XLS", "application/vnd.ms-excel");
AddMimeType("PPT", "application/mspowerpoint");
AddMimeType("FLAC", "audio/x-flac");
AddMimeType("FLAC", "audio/flac");
AddMimeType("ZIP", "application/zip");
AddMimeType("MPG", "video/mp2p");
AddMimeType("MPEG", "video/mp2p");
}
const MimeTypes&MimeTypes::instance()
{
std::lock_guard lock { m_instanceMutex};
if (!m_instance) {
m_instance = std::unique_ptr<MimeTypes>(new MimeTypes());
}
return (*m_instance);
}
const bool MimeTypes::IsValidExtension(const std::string&mimeType, const std::string&extension) const
{
auto iter = mimeTypeToExtensions.find(mimeType);
if (iter == mimeTypeToExtensions.end()) return false;
return iter->second.contains(extension);
}
+20 -11
View File
@@ -25,17 +25,26 @@
namespace pipedal {
class MimeTypes {
public:
static const std::string& MimeTypeFromExtension(const std::string &extension);
static const std::string& ExtensionFromMimeType(const std::string &mimeType);
private:
static void AddMimeType(const std::string&extension, const std::string&mimeType);
static std::map<std::string,std::string> mimeTypeToExtension;
static std::map<std::string,std::string> extensionToMimeType;
static std::set<std::string> audioExtensions;
static std::set<std::string> videoExtensions;
static void MaybeInitialize();
static bool initialized;
MimeTypes();
public:
static const MimeTypes&instance();
const std::string& MimeTypeFromExtension(const std::string &extension) const;
// the best possible extension if we had to pick one.
const std::string& ExtensionFromMimeType(const std::string &mimeType) const;
// All possible extensions that other people might have picked.
const std::set<std::string> &AudioExtensions() const;
const std::set<std::string> &VideoExtensions() const;
const std::set<std::string> &MidiExtensions() const;
const bool IsValidExtension(const std::string&mimeType, const std::string&extension) const;
private:
void AddMimeType(const std::string&extension, const std::string&mimeType);
std::map<std::string,std::set<std::string>> mimeTypeToExtensions;
std::map<std::string,std::string> mimeTypeToExtension;
std::map<std::string,std::string> extensionToMimeType;
std::set<std::string> audioExtensions;
std::set<std::string> midiExtensions;
std::set<std::string> videoExtensions;
};
}
+72 -49
View File
@@ -22,38 +22,61 @@
#include <stdexcept>
#include <iostream>
#include "ss.hpp"
#include "MimeTypes.hpp"
using namespace pipedal;
const std::vector<ModFileTypes::ModDirectory> ModFileTypes::ModDirectories =
static std::mutex gModDirectoriesMutex;
static std::unique_ptr< std::vector<ModFileTypes::ModDirectory> > gModDirectories;
static std::vector<ModFileTypes::ModDirectory> *CreateModDirectories()
{
return new std::vector<ModFileTypes::ModDirectory>(
{
{"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
{"audiorecording","shared/audio/Audio Recordings", "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
{"h2drumkit", "shared/h2drumkit", "Hydrogen Drumkits",{".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
{"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
{"audio", "shared/audio", "Audio", {"audio/*"}}, // all audio files (Ratatouille)
{"nammodel", "NeuralAmpModels", "Neural Amp Models", {".nam"}}, // Ratatouille, Mike's NAM.
{"aidadspmodel", "shared/aidaaix", "AIDA IAX Models", {".json", ".aidaiax"}}, // Ratatouille
{"mlmodel", "ToobMlModels", "ML Models", {".json"}}, //
// pipedal-specific types. use pipedal_ui:filetypes instead (or in addition to) mod:fileTypes.
{"recording", "shared/audio/Recordings", "Recordings", {"audio/*"}}, // Recordings from recordings plugins.
//
};
// For plugins that don't secify a modDirectory (allows upload of arbitrary extension)
{"*", "shared/other", "Other", {".*"}}, //
});
}
const std::vector<ModFileTypes::ModDirectory>&ModFileTypes::ModDirectories()
{
std::lock_guard lock { gModDirectoriesMutex };
if (!gModDirectories)
{
gModDirectories = std::unique_ptr<std::vector<ModFileTypes::ModDirectory>>(CreateModDirectories());
}
return *gModDirectories;
}
const ModFileTypes::ModDirectory *ModFileTypes::GetModDirectory(const std::string &type)
{
for (const ModFileTypes::ModDirectory &modType : ModFileTypes::ModDirectories)
for (const ModFileTypes::ModDirectory &modType : ModFileTypes::ModDirectories())
{
if (modType.modType == type)
{
@@ -78,41 +101,6 @@ ModFileTypes::ModFileTypes(const std::string &fileTypes)
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(
@@ -132,7 +120,7 @@ void ModFileTypes::CreateDefaultDirectories(const std::filesystem::path &rootDir
using namespace std;
try
{
for (const auto &modType : ModDirectories)
for (const auto &modType : ModDirectories())
{
fs::path path = rootDirectory / modType.pipedalPath;
@@ -158,3 +146,38 @@ void ModFileTypes::CreateDefaultDirectories(const std::filesystem::path &rootDir
}
}
static std::set<std::string> makeFileExtensions(const std::vector<std::string>&fileTypes)
{
std::set<std::string> result;
for (const std::string&fileType: fileTypes)
{
if (fileType.starts_with('.')) {
result.insert(fileType);
} else {
if (fileType == "audio/*")
{
const auto&audioExtensions = MimeTypes::instance().AudioExtensions();
result.insert(audioExtensions.begin(),audioExtensions.end());
} else if (fileType == "video/*")
{
const auto&videoExtensions = MimeTypes::instance().VideoExtensions();
result.insert(videoExtensions.begin(),videoExtensions.end());
}
}
}
return result;
}
ModFileTypes::ModDirectory::ModDirectory(
const std::string modType,
const std::string pipedalPath,
const std::string displayName,
const std::vector<std::string> fileTypes)
:modType(modType),
pipedalPath(pipedalPath),
displayName(displayName),
fileTypesX(fileTypes),
fileExtensions(makeFileExtensions(fileTypes))
{
}
+26 -14
View File
@@ -21,33 +21,45 @@
#include <string>
#include <vector>
#include <filesystem>
#include <set>
namespace pipedal {
class ModFileTypes {
namespace pipedal
{
class ModFileTypes
{
public:
public:
ModFileTypes(const std::string&fileTypes);
const std::vector<std::string> &rootDirectories() { return rootDirectories_; }
const std::vector<std::string> &fileTypes() { return fileTypes_;}
ModFileTypes(const std::string &fileTypes);
const std::vector<std::string> &rootDirectories() const { return rootDirectories_; }
std::vector<std::string> &rootDirectories() { return rootDirectories_; }
const std::vector<std::string> &fileTypes() const { return fileTypes_; }
static constexpr const char *DEFAULT_FILE_TYPES = "audio,nammodel,mlmodel,sf2,sfz,midisong,midiclip,*"; // (everything)
private:
std::vector<std::string> rootDirectories_;
std::vector<std::string> fileTypes_;
public:
class ModDirectory {
class ModDirectory
{
public:
ModDirectory(
const std::string modType,
const std::string pipedalPath,
const std::string displayName,
const std::vector<std::string> defaultFileExtensions);
const std::string modType;
const std::string pipedalPath;
const std::filesystem::path pipedalPath;
const std::string displayName;
const std::vector<std::string> defaultFileExtensions;
const std::vector<std::string> fileTypesX; // mixture of .ext and mime types. Use fileExtensions instead.
const std::set<std::string> fileExtensions;
};
const static std::vector<ModDirectory> ModDirectories;
static const ModDirectory* GetModDirectory(const std::string&modType);
static void CreateDefaultDirectories(const std::filesystem::path&rootDirectory);
static const std::vector<ModDirectory> &ModDirectories();
static const ModDirectory *GetModDirectory(const std::string &modType);
static void CreateDefaultDirectories(const std::filesystem::path &rootDirectory);
};
}
+162
View File
@@ -0,0 +1,162 @@
// 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.
#include "pch.h"
#include "catch.hpp"
#include <string>
#include "PiPedalUI.hpp"
#include "ModFileTypes.hpp"
using namespace pipedal;
std::set<std::string> CalculateFileExtensions(const std::string &mod__fileTypes, const std::string &modType)
{
ModFileTypes modFileTypes(mod__fileTypes);
auto fileProperty =
std::make_shared<UiFileProperty>(
"Test", "urn:test", modFileTypes);
return fileProperty->GetPermittedFileExtensions(modType);
}
bool checkFile(const std::string &mod__FileTypes,const std::filesystem::path&path)
{
ModFileTypes modFileTypes(mod__FileTypes);
auto fileProperty =
std::make_shared<UiFileProperty>(
"Test", "urn:test", modFileTypes);
auto fileModDirectory = fileProperty->getParentModDirectory(path);
return fileProperty->IsValidExtension(path);
}
TEST_CASE("ModFileTypes Test", "[modFileTypes]")
{
REQUIRE(checkFile("audio","shared/audio/Test.wav"));
REQUIRE(checkFile("audio","shared/audio/xxx/Test.flac"));
REQUIRE(!checkFile("audio","shared/audio/xxx/Test.nam"));
REQUIRE(checkFile("audio,wav","shared/audio/Test.wav"));
REQUIRE(!checkFile("audio,wav","shared/audio/xxx/Test.flac"));
REQUIRE(!checkFile("audio","shared/audio/xxx/Test.nam"));
REQUIRE(checkFile("cabsim,wav","CabIR/Test.wav"));
REQUIRE(!checkFile("cabsim,wav","CabIR/xxx/Test.flac"));
REQUIRE(!checkFile("cabsim","CabIR/xxx/Test.nam"));
REQUIRE(checkFile("audio,wav","shared/audio/Test.wav"));
REQUIRE(!checkFile("audio,wav","shared/audio/xxx/Test.flac"));
REQUIRE(!checkFile("audio","shared/audio/xxx/Test.nam"));
REQUIRE(checkFile("wav","shared/audio/Test.wav"));
REQUIRE(!checkFile("wav","CabIR/Test.flac")); // only one audio directory for this case.
REQUIRE(!checkFile("wav","shared/audio/xxx/Test.nam"));
REQUIRE(!checkFile("wav","shared/midiSongs/xxx/Test.mid"));
REQUIRE(!checkFile("wav","shared/other/xxx/Test.mid"));
REQUIRE(checkFile("mid","shared/midiClips/Test.mid"));
REQUIRE(checkFile("mid","shared/midiSongs/Test.mid"));
REQUIRE(!checkFile("mid","CabIR/Test.flac")); // only one audio directory for this case.
REQUIRE(checkFile("midisong","shared/midiSongs/Test.mid"));
REQUIRE(checkFile("midisong","shared/midiSongs/xxx/Test.midi"));
REQUIRE(!checkFile("midisong","shared/midiClips/xxx/Test.nam"));
REQUIRE(checkFile("","shared/audio/Test.wav"));
REQUIRE(checkFile("","shared/audio/xxx/Test.flac"));
REQUIRE(!checkFile("","shared/audio/xxx/Test.nam"));
REQUIRE(!checkFile("","ToobMlModels/Test.nam"));
REQUIRE(checkFile("","ToobMlModels/Test.json"));
REQUIRE(checkFile("","ToobMlModels/a/b/c/Test.json"));
REQUIRE(checkFile("","shared/other/xxx/Test.xwav"));
REQUIRE(checkFile("","shared/other/xxx/Test"));
{
std::set<std::string> result =
CalculateFileExtensions("audio", "audio");
REQUIRE(result.contains(".wav"));
REQUIRE(result.contains(".flac"));
}
{
std::set<std::string> result =
CalculateFileExtensions("wav", "audio");
REQUIRE(result.size() == 1);
REQUIRE(result.contains(".wav"));
REQUIRE(!result.contains(".flac"));
}
{
std::set<std::string> result =
CalculateFileExtensions("nam", "audio");
REQUIRE(result.size() == 0);
REQUIRE(!result.contains(".flac"));
REQUIRE(!result.contains(".nam"));
}
{
// extensions specified, but no modDirectory.
ModFileTypes modFileTypes("mid,midi");
auto fileProperty =
std::make_shared<UiFileProperty>(
"Test", "urn:test", modFileTypes);
REQUIRE(fileProperty->modDirectories().size() == 2);
for (const auto&modDirectory: fileProperty->modDirectories())
{
auto fileExtensions = fileProperty->GetPermittedFileExtensions(modDirectory);
REQUIRE(fileExtensions.size() == 2);
REQUIRE(fileExtensions.contains(".mid"));
REQUIRE(fileExtensions.contains(".midi"));
}
}
{
// Custom directory, custom extensions.
// actually simulating a PiPedalUI::UiFileProperty load from ttl.
ModFileTypes modFileTypes("wavx");
modFileTypes.rootDirectories().push_back("CustomDirectory");
auto fileProperty =
std::make_shared<UiFileProperty>(
"Test", "urn:test", modFileTypes);
REQUIRE(fileProperty->modDirectories().size() == 1);
auto fileExtensions = fileProperty->GetPermittedFileExtensions("CustomDirectory");
REQUIRE(fileProperty->modDirectories().size() == 1);
REQUIRE(fileExtensions.size() == 1);
REQUIRE(fileExtensions.contains(".wavx"));
}
}
+33 -16
View File
@@ -2309,18 +2309,6 @@ void PiPedalModel::SetSystemMidiBindings(std::vector<MidiBinding> &bindings)
}
}
std::vector<std::string> PiPedalModel::GetFileList(const UiFileProperty &fileProperty)
{
try
{
return this->storage.GetFileList(fileProperty);
}
catch (const std::exception &e)
{
Lv2Log::warning("GetFileList() failed: (%s)", e.what());
return std::vector<std::string>(); // don't disclose to users what the problem is.
}
}
FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty)
{
try
@@ -2354,15 +2342,44 @@ std::string PiPedalModel::CreateNewSampleDirectory(const std::string &relativePa
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.CreateNewSampleDirectory(relativePath, uiFileProperty);
}
FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty)
FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.GetFilePropertydirectoryTree(uiFileProperty);
return storage.GetFilePropertydirectoryTree(uiFileProperty,selectedPath);
}
std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength)
UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri)
{
return storage.UploadUserFile(directory, patchProperty, filename, stream, contentLength);
auto pedalboardItems = pedalboard.GetAllPlugins();
for (const auto&pedalboardItem: pedalboardItems) {
if (pedalboardItem->instanceId() == instanceId)
{
Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pedalboardItem->uri());
if (pluginInfo && pluginInfo->piPedalUI())
{
for (const auto&fileProperty: pluginInfo->piPedalUI()->fileProperties())
if (fileProperty->patchProperty() == patchPropertyUri)
{
return fileProperty;
}
}
}
}
return nullptr;
throw std::runtime_error("Permission denied. Plugin not currently loaded.");
}
std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId,const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength)
{
UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId,patchProperty);
if (!fileProperty)
{
Lv2Log::error(SS("Upload fle: Permission denied. No currently-loaded plugin provides that patch property: " << patchProperty));
throw std::runtime_error("Permission denied.");
}
return storage.UploadUserFile(directory, fileProperty, filename, stream, contentLength);
}
uint64_t PiPedalModel::CreateNewPreset()
+3 -3
View File
@@ -259,6 +259,7 @@ namespace pipedal
PiPedalConfiguration configuration;
void CheckForResourceInitialization(Pedalboard &pedalboard);
UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri);
public:
PiPedalModel();
@@ -447,15 +448,14 @@ namespace pipedal
void SetFavorites(const std::map<std::string, bool> &favorites);
void SetUpdatePolicy(UpdatePolicyT updatePolicy);
void ForceUpdateCheck();
std::vector<std::string> GetFileList(const UiFileProperty &fileProperty);
FileRequestResult GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty);
void DeleteSampleFile(const std::filesystem::path &fileName);
std::string CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty);
std::string RenameFilePropertyFile(const std::string &oldRelativePath, const std::string &newRelativePath, const UiFileProperty &uiFileProperty);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath);
std::string UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &inputStream, size_t streamLength);
std::string UploadUserFile(const std::string &directory, int64_t instanceId, const std::string &patchProperty, const std::string &filename, std::istream &inputStream, size_t streamLength);
uint64_t CreateNewPreset();
bool LoadCurrentPedalboard();
+24 -7
View File
@@ -102,6 +102,23 @@ JSON_MAP_REFERENCE(RenameSampleFileArgs, newRelativePath)
JSON_MAP_REFERENCE(RenameSampleFileArgs, uiFileProperty)
JSON_MAP_END()
class GetFilePropertyDirectoryTreeArgs
{
public:
UiFileProperty fileProperty_;
std::string selectedPath_;
DECLARE_JSON_MAP(GetFilePropertyDirectoryTreeArgs);
};
JSON_MAP_BEGIN(GetFilePropertyDirectoryTreeArgs)
JSON_MAP_REFERENCE(GetFilePropertyDirectoryTreeArgs, fileProperty)
JSON_MAP_REFERENCE(GetFilePropertyDirectoryTreeArgs, selectedPath)
JSON_MAP_END()
class Lv2StateChangedBody
{
public:
@@ -1585,10 +1602,7 @@ public:
}
else if (message == "requestFileList")
{
UiFileProperty fileProperty;
pReader->read(&fileProperty);
std::vector<std::string> list = this->model.GetFileList(fileProperty);
this->Reply(replyTo, "requestFileList", list);
throw std::runtime_error("No longer implemented.");
}
else if (message == "requestFileList2")
{
@@ -1628,9 +1642,12 @@ public:
}
else if (message == "getFilePropertyDirectoryTree")
{
UiFileProperty uiFileProperty;
pReader->read(&uiFileProperty);
FilePropertyDirectoryTree::ptr result = model.GetFilePropertydirectoryTree(uiFileProperty);
GetFilePropertyDirectoryTreeArgs args;
pReader->read(&args);
FilePropertyDirectoryTree::ptr result =
model.GetFilePropertydirectoryTree(
args.fileProperty_,
args.selectedPath_);
this->Reply(replyTo, "GetFilePropertydirectoryTree", result);
}
+259 -16
View File
@@ -27,6 +27,10 @@
#include "ss.hpp"
#include "MimeTypes.hpp"
#include "AutoLilvNode.hpp"
#include "ModFileTypes.hpp"
#include <algorithm>
#include "util.hpp"
#include "MimeTypes.hpp"
using namespace pipedal;
@@ -122,17 +126,20 @@ UiFileType::UiFileType(PluginHost *pHost, const LilvNode *node)
}
if (fileExtension_ == "")
{
fileExtension_ = MimeTypes::ExtensionFromMimeType(mimeType_);
fileExtension_ = MimeTypes::instance().ExtensionFromMimeType(mimeType_);
}
if (mimeType_ == "")
{
mimeType_ = MimeTypes::MimeTypeFromExtension(fileExtension_);
mimeType_ = MimeTypes::instance().MimeTypeFromExtension(fileExtension_);
if (mimeType_ == "")
{
mimeType_ = "application/octet-stream";
}
}
}
UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const std::filesystem::path &resourcePath)
{
auto pWorld = pHost->getWorld();
@@ -182,6 +189,8 @@ UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const st
throw std::logic_error("PipedalUI::fileProperty: must specify at least a directory.");
}
this->modDirectories_.push_back(directory_);
AutoLilvNode patchProperty = lilv_world_get(
pWorld,
node,
@@ -207,6 +216,8 @@ UiFileProperty::UiFileProperty(PluginHost *pHost, const LilvNode *node, const st
this->resourceDirectory_ = resourceDirectory.AsString();
}
this->fileTypes_ = UiFileType::GetArray(pHost, node, pHost->lilvUris->pipedalUI__fileTypes);
PrecalculateFileExtensions();
}
std::vector<UiFileType> UiFileType::GetArray(PluginHost *pHost, const LilvNode *node, const LilvNode *uri)
@@ -271,21 +282,41 @@ bool UiFileProperty::IsDirectoryNameValid(const std::string &value)
return true;
}
bool UiFileProperty::IsValidExtension(const std::string &extension) const
{
if (fileTypes_.size() == 0)
{
return true;
}
// bool UiFileProperty::IsValidExtension(const std::filesystem::path&relativePath,const std::string &extension) const
// {
// std::string modDirectory = this->getParentModDirectory(relativePath);
// const auto&validExtensions = GetPermittedFileExtensions(modDirectory);
// return validExtensions.contains(extension);
// }
for (auto &fileType : fileTypes_)
std::string UiFileProperty::GetFileExtension(const std::filesystem::path&path)
{
if (path.has_extension())
{
if (fileType.fileExtension() == extension)
{
return true;
}
return ToLower(path.extension());
}
return false;
return "";
}
bool UiFileProperty::IsValidExtension(const std::filesystem::path&relativePath) const
{
std::string extension = GetFileExtension(relativePath);
if (this->fileTypes().size() != 0) {
for (const auto&fileType: fileTypes())
{
UiFileType x;
if (fileType.IsValidExtension(extension)) return true;
}
return false;
}
if (this->modDirectories().size() != 0)
{
std::string modDirectory = this->getParentModDirectory(relativePath);
const auto&validExtensions = GetPermittedFileExtensions(modDirectory);
if (validExtensions.contains(extension)) return true;
return validExtensions.contains(".*");
}
return true;
}
UiPortNotification::UiPortNotification(PluginHost *pHost, const LilvNode *node)
@@ -343,6 +374,52 @@ UiPortNotification::UiPortNotification(PluginHost *pHost, const LilvNode *node)
}
}
UiFileProperty::UiFileProperty(const std::string&label, const std::string&patchProperty, const ModFileTypes&modFileTypes)
: label_(label),
patchProperty_(patchProperty),
directory_("")
{
setModFileTypes(modFileTypes);
}
void UiFileProperty::setModFileTypes(const ModFileTypes&modFileTypes)
{
modDirectories_.resize(0);
fileTypes_.resize(0);
for (const std::string &type : modFileTypes.fileTypes())
{
fileTypes().push_back(UiFileType(SS(type << " file"), SS('.' << type)));
}
if (modFileTypes.rootDirectories().size() != 0)
{
for (const std::string &rootDirectory : modFileTypes.rootDirectories())
{
modDirectories().push_back(rootDirectory);
}
} else {
if (fileTypes().size() == 0)
{
// add everything.
for (const auto&modDirectory: split(ModFileTypes::DEFAULT_FILE_TYPES,','))
{
modDirectories().push_back(modDirectory);
}
} else {
// add only those mod directories that contain one or more of our fileTypes.
for (const auto&modDirectory: split(ModFileTypes::DEFAULT_FILE_TYPES,','))
{
modDirectories().push_back(modDirectory);
}
}
}
PrecalculateFileExtensions();
}
UiFileProperty::UiFileProperty(const std::string &name, const std::string &patchProperty, const std::string &directory)
: label_(name),
patchProperty_(patchProperty),
@@ -362,6 +439,22 @@ PiPedalUI::PiPedalUI(
this->fileProperties_ = std::move(fileProperties);
}
bool UiFileType::IsValidExtension(const std::string&extension) const {
if (fileExtension_.length() != 0)
{
if (fileExtension_ == ".*")
{
return true;
}
return fileExtension_ == extension;
}
if (mimeType_.length() != 0)
{
return MimeTypes::instance().IsValidExtension(mimeType_,extension);
}
return false;
}
UiFileType::UiFileType(const std::string&label, const std::string &fileType)
: label_(label)
, fileExtension_(fileType)
@@ -369,13 +462,12 @@ UiFileType::UiFileType(const std::string&label, const std::string &fileType)
if (fileType.starts_with('.'))
{
fileExtension_ = fileType;
mimeType_ = MimeTypes::MimeTypeFromExtension(fileType);
mimeType_ = MimeTypes::instance().MimeTypeFromExtension(fileType);
if (mimeType_ == "")
{
mimeType_ = "application/octet-stream";
}
} else {
fileExtension_ = MimeTypes::ExtensionFromMimeType(fileType); // (may be blank, esp. for audio/* and video/*.
mimeType_ = fileType;
}
if (mimeType_ == "*")
@@ -443,6 +535,157 @@ UiFrequencyPlot::UiFrequencyPlot(PluginHost*pHost, const LilvNode*node,
}
static std::set<std::string> emptySet;
const std::set<std::string>& UiFileProperty::GetPermittedFileExtensions(const std::string &modDirectory) const {
auto result = fileExtensionsByModDirectory.find(modDirectory);
if (result != fileExtensionsByModDirectory.end())
{
return result->second;
}
if (modDirectory == this->directory_)
{
std::set<std::string> result;
for (auto &x : this->fileTypes_)
{
}
return emptySet;
}
return emptySet;
}
void UiFileProperty::PrecalculateFileExtensions()
{
std::set<std::string> explicitlySpecifiedExtensions;
bool allAudioTypes = false;
bool allVideoTypes = false;
for (const auto &fileType: this->fileTypes())
{
if (!fileType.fileExtension().empty())
{
explicitlySpecifiedExtensions.insert(fileType.fileExtension());
} else if (fileType.mimeType().length() != 0)
{
if (fileType.mimeType() == "audio/*")
{
allAudioTypes = true;
} else if (fileType.mimeType() == "video/*")
{
allVideoTypes = true;
} else {
// The principle risk being that there is more than one commonly used extension. e.g. midi.
// If the right extension isn't picked, the client should explicitly list which extensions are wanted
// instead of using a mime type. :-/ Not a good implementation, but not a use case that's actually going to
// come up.
std::string extension = MimeTypes::instance().ExtensionFromMimeType(fileType.mimeType());
if (extension.length() == 0)
{
Lv2Log::warning(SS("Unknown MIME type: " << fileType.mimeType()));
} else {
explicitlySpecifiedExtensions.insert(extension);
}
}
}
}
if (allAudioTypes)
{
const auto&audioExtensions = MimeTypes::instance().AudioExtensions();
explicitlySpecifiedExtensions.insert(
audioExtensions.begin(),audioExtensions.end()
);
}
if (allVideoTypes)
{
const auto&videoExtensions = MimeTypes::instance().VideoExtensions();
explicitlySpecifiedExtensions.insert(
videoExtensions.begin(),videoExtensions.end()
);
}
if (this->modDirectories().size() == 0) // not specified? Add all directories, (and prune later, if we have filetypes).
{
this->modDirectories_ = split(ModFileTypes::DEFAULT_FILE_TYPES,',');
}
if (fileTypes().size() != 0)
{
// delete any modDirectories that do not contain one of the given filetyeps.
auto iter = this->modDirectories_.begin();
while(iter != this->modDirectories_.end())
{
const auto *pDirectory = ModFileTypes::GetModDirectory(*iter);
if (pDirectory == nullptr)
{
// a custom directory! How cool is that!?
this->fileExtensionsByModDirectory[*iter] = explicitlySpecifiedExtensions;
++iter;
} else {
std::set<std::string> extensions = Intersect(pDirectory->fileExtensions,explicitlySpecifiedExtensions);
if (extensions.size() == 0)
{
iter = modDirectories_.erase(iter);
} else {
this->fileExtensionsByModDirectory[*iter] = std::move(extensions);
++iter;
}
}
}
} else {
auto iter = this->modDirectories_.begin();
while(iter != this->modDirectories_.end())
{
const auto *pDirectory = ModFileTypes::GetModDirectory(*iter);
if (pDirectory)
{
this->fileExtensionsByModDirectory[*iter] = pDirectory->fileExtensions;
} else {
Lv2Log::warning(SS("Unknown Mod Directory: " << *iter));
}
++iter;
}
}
}
// std::set<std::string> modDirectoryFileTypes = rootModDirectory->fileExtensions;
// if (fileProperty.fileTypes().size() != 0)
// {
// std::set<std::string> specifiedFileTypes;
// for (const auto&fileType: fileProperty.fileTypes())
// {
// if (!fileType.fileExtension().empty())
// {
// }
// }
// }
std::string UiFileProperty::getParentModDirectory(const std::filesystem::path&path) const
{
for (const auto &modDirectory: this->modDirectories_)
{
const auto pModDirectory = ModFileTypes::GetModDirectory(modDirectory);
if (pModDirectory)
{
if (IsChildDirectory(path,pModDirectory->pipedalPath))
{
return modDirectory;
}
}
}
if (this->directory_.length() != 0)
{
if (IsChildDirectory(path,this->directory_))
{
return this->directory_;
}
}
return "";
}
JSON_MAP_BEGIN(UiPortNotification)
JSON_MAP_REFERENCE(UiPortNotification, portIndex)
+27 -6
View File
@@ -29,6 +29,8 @@
#include <lilv/lilv.h>
#include "json.hpp"
#include <filesystem>
#include <set>
#include "ModFileTypes.hpp"
#define PIPEDAL_UI "http://github.com/rerdavies/pipedal/ui"
@@ -83,7 +85,7 @@ namespace pipedal {
const std::string& label() const { return label_;}
const std::string &fileExtension() const { return fileExtension_; }
const std::string &mimeType() const { return mimeType_; }
bool IsValidExtension(const std::string&extension) const;
public:
DECLARE_JSON_MAP(UiFileType);
@@ -118,13 +120,20 @@ namespace pipedal {
std::string resourceDirectory_;
std::vector<std::string> modDirectories_;
bool useLegacyModDirectory_= false;
std::map<std::string,std::set<std::string>> fileExtensionsByModDirectory; // non-serialized.
void PrecalculateFileExtensions();
public:
using ptr = std::shared_ptr<UiFileProperty>;
UiFileProperty() { }
UiFileProperty(PluginHost*pHost, const LilvNode*node, const std::filesystem::path&resourcePath);
UiFileProperty(const std::string&name, const std::string&patchProperty,const std::string &directory);
UiFileProperty(const std::string&label, const std::string&patchProperty,const std::string &directory);
UiFileProperty(
const std::string&label,
const std::string&patchProperty,
const ModFileTypes&modFileType);
void setModFileTypes(const ModFileTypes&modFileType);
std::vector<std::string>& modDirectories() { return modDirectories_; }
const std::vector<std::string>& modDirectories() const { return modDirectories_; }
@@ -144,11 +153,19 @@ namespace pipedal {
std::vector<UiFileType> &fileTypes() { return fileTypes_; }
const std::string &patchProperty() const { return patchProperty_; }
bool IsValidExtension(const std::string&extension) const;
bool IsValidExtension(const std::filesystem::path&relativePath) const;
static bool IsDirectoryNameValid(const std::string&value);
static std::string GetFileExtension(const std::filesystem::path&path);
const std::string&resourceDirectory() const { return resourceDirectory_; }
const std::set<std::string>& GetPermittedFileExtensions(const std::string &modDirectory) const;
std::string getParentModDirectory(const std::filesystem::path&path) const;
public:
DECLARE_JSON_MAP(UiFileProperty);
};
@@ -202,6 +219,8 @@ namespace pipedal {
return frequencyPlots_;
}
const std::vector<UiPortNotification::ptr> &portNotifications() const { return portNotifications_; }
const UiFileProperty*GetFileProperty(const std::string &propertyUri) const
@@ -215,14 +234,16 @@ namespace pipedal {
}
return nullptr;
}
bool unsupportedPatchProperty() const { return unsupportedPatchProperty_; }
void unsupportedPatchProperty(bool value) { unsupportedPatchProperty_ = value; }
private:
bool unsupportedPatchProperty_ = false; // not serialized.
std::vector<UiFileProperty::ptr> fileProperties_;
std::vector<UiFrequencyPlot::ptr> frequencyPlots_;
std::vector<UiPortNotification::ptr> portNotifications_;
};
// Utiltities for validating file paths received via PiPedalFileProperty-related APIs.
// utilities for validating file paths received via PiPedalFileProperty-related APIs.
bool IsAlphaNumeric(const std::string&value);
+95 -94
View File
@@ -195,7 +195,6 @@ void PluginHost::LilvUris::Initialize(LilvWorld *pWorld)
dc__format = lilv_new_uri(pWorld, "http://purl.org/dc/terms/format");
mod__fileTypes = lilv_new_uri(pWorld, "http://moddevices.com/ns/mod#fileTypes");
pipedalui__fileTypes =lilv_new_uri(pWorld, PIPEDAL_UI__fileTypes);
}
void PluginHost::LilvUris::Free()
@@ -367,7 +366,7 @@ void PluginHost::LoadPluginClassesFromLilv()
LILV_FOREACH(plugin_classes, iPluginClass, classes)
{
const LilvPluginClass *pClass = lilv_plugin_classes_get(classes, iPluginClass);
// Get it to prepopulate the map.
// Get it to pre-populate the map.
MakePluginClass(pClass);
}
for (auto value : classesMap)
@@ -432,6 +431,9 @@ void PluginHost::Load(const char *lv2Path)
if (pluginInfo->hasCvPorts())
{
Lv2Log::debug("Plugin %s (%s) skipped. (Has CV ports).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
} else if (pluginInfo->hasUnsupportedPatchProperties())
{
Lv2Log::debug("Plugin %s (%s) skipped. (Has unsupported patch parameters).", pluginInfo->name().c_str(), pluginInfo->uri().c_str());
}
#if !SUPPORT_MIDI
else if (pluginInfo->plugin_class() == LV2_MIDI_PLUGIN)
@@ -481,10 +483,15 @@ void PluginHost::Load(const char *lv2Path)
for (auto plugin : this->plugins_)
{
pluginsByUri[plugin->uri()] = plugin;
Lv2PluginUiInfo info(this, plugin.get());
if (plugin->is_valid())
{
#if 1
// no plugins with more than 2 inputs or outputs.
// no zero-input or zero-output plugins (temporarily disables midi plugins)
// no zero output devices (permanent, I think)
if (info.audio_inputs() > 2 || info.audio_outputs() > 2) {
Lv2Log::debug(
"Plugin %s (%s) skipped. %d inputs, %d outputs.", plugin->name().c_str(), plugin->uri().c_str(),
@@ -598,7 +605,8 @@ bool Lv2PluginInfo::HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plu
return result;
}
std::shared_ptr<PiPedalUI> Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin)
Lv2PluginInfo::FindWritablePathPropertiesResult
Lv2PluginInfo::FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin)
{
// example:
@@ -615,6 +623,7 @@ std::shared_ptr<PiPedalUI> Lv2PluginInfo::FindWritablePathProperties(PluginHost
std::vector<UiFileProperty::ptr> fileProperties;
bool unsupportedPatchProperty = false;
LILV_FOREACH(nodes, iNode, patchWritables)
{
AutoLilvNode propertyUri = lilv_nodes_get(patchWritables, iNode);
@@ -631,99 +640,91 @@ std::shared_ptr<PiPedalUI> Lv2PluginInfo::FindWritablePathProperties(PluginHost
AutoLilvNode label = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->rdfs__label, nullptr);
std::string strLabel = label.AsString();
if (strLabel.length() != 0)
if (strLabel.length() == 0)
{
std::filesystem::path path = this->bundle_path();
path = path.parent_path();
std::string lv2DirectoryName = path.filename().string();
// we have a valid path property!
auto fileProperty =
std::make_shared<UiFileProperty>(
strLabel, propertyUri.AsUri(), lv2DirectoryName);
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr);
int32_t index = indexNode.AsInt(-1);
fileProperty->index(index);
// if there's a pipedalui_fileTypes node, use that instead.
AutoLilvNodes mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->pipedalui__fileTypes, nullptr);
if (!mod__fileTypes)
{
mod__fileTypes = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr);
}
LILV_FOREACH(nodes, i, mod__fileTypes)
{
// "nam,nammodel"
AutoLilvNode lilvfileType{lilv_nodes_get(mod__fileTypes, i)};
std::string fileTypes = lilvfileType.AsString();
ModFileTypes modFileTypes(fileTypes);
for (const std::string &rootDirectory : modFileTypes.rootDirectories())
{
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)
{
AutoLilvNodes dc_types = lilv_world_find_nodes(pWorld, propertyUri, lv2Host->lilvUris->dc__format, nullptr);
LILV_FOREACH(nodes, i, dc_types)
{
AutoLilvNode dc_type = lilv_nodes_get(dc_types, i);
std::string fileType = dc_type.AsString();
std::string label = "";
fileProperty->fileTypes().push_back(UiFileType(label, fileType));
}
}
fileProperties.push_back(
fileProperty);
strLabel = "File";
}
std::filesystem::path path = this->bundle_path();
path = path.parent_path();
std::string lv2DirectoryName = path.filename().string();
// we have a valid path property!
auto fileProperty =
std::make_shared<UiFileProperty>(
strLabel, propertyUri.AsUri(), lv2DirectoryName);
AutoLilvNode indexNode = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->lv2core__index, nullptr);
int32_t index = indexNode.AsInt(-1);
fileProperty->index(index);
// if there's a pipedalui_fileTypes node, use that instead.
AutoLilvNode mod__fileTypes = lilv_world_get(pWorld, propertyUri, lv2Host->lilvUris->mod__fileTypes, nullptr);
// default: everything.
std::string fileTypes = ModFileTypes::DEFAULT_FILE_TYPES;
if (mod__fileTypes)
{
// "nam,nammodel"
fileTypes = mod__fileTypes.AsString();
}
ModFileTypes modFileTypes(fileTypes);
// 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);
}
modFileTypes.rootDirectories().push_back(bundleDirectoryName.filename()); // push the private directory!
}
fileProperty->setModFileTypes(modFileTypes);
fileProperty->directory(bundleDirectoryName);
fileProperties.push_back(fileProperty);
} else {
unsupportedPatchProperty = true;
}
}
}
}
FindWritablePathPropertiesResult result;
if (fileProperties.size() != 0)
{
return std::make_shared<PiPedalUI>(std::move(fileProperties));
}
std::sort(fileProperties.begin(),fileProperties.end(),[](const UiFileProperty::ptr& left,const UiFileProperty::ptr&right) {
// properies with indexes first.
int32_t indexL = left->index();
if (indexL < 0) indexL = std::numeric_limits<int32_t>::max();
int32_t indexR = right->index();
if (indexR < 0) indexR = std::numeric_limits<int32_t>::max();
if (indexL < indexR) return true;
if (indexL > indexR) return false;
return std::shared_ptr<PiPedalUI>();
// there is no natural order. TTL indexing means that the order we see them in is random.
// We can't order them sensibly. Let's at least order them consistently.
return left->label() < right->label();
});
result.pipedalUi = std::make_shared<PiPedalUI>(std::move(fileProperties));
}
result.hasUnsupportedPatchProperties = unsupportedPatchProperty;
return result;
}
Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvPlugin *pPlugin)
@@ -837,7 +838,9 @@ Lv2PluginInfo::Lv2PluginInfo(PluginHost *lv2Host, LilvWorld *pWorld, const LilvP
else
{
// look for
this->piPedalUI_ = FindWritablePathProperties(lv2Host, pPlugin);
auto result = FindWritablePathProperties(lv2Host, pPlugin);
this->piPedalUI_ = result.pipedalUi;
this->hasUnsupportedPatchProperties_ = result.hasUnsupportedPatchProperties;
}
int nInputs = 0;
@@ -1220,14 +1223,12 @@ std::shared_ptr<Lv2PluginClass> PluginHost::GetLv2PluginClass() const
std::shared_ptr<Lv2PluginInfo> PluginHost::GetPluginInfo(const std::string &uri) const
{
for (auto i = this->plugins_.begin(); i != this->plugins_.end(); ++i)
auto ff = pluginsByUri.find(uri);
if (ff == pluginsByUri.end())
{
if ((*i)->uri() == uri)
{
return (*i);
}
return nullptr;
}
return nullptr;
return ff->second;
}
Lv2Pedalboard *PluginHost::CreateLv2Pedalboard(Pedalboard &pedalboard, Lv2PedalboardErrorList &errorMessages)
+8 -1
View File
@@ -346,7 +346,11 @@ namespace pipedal
bool isSplit() const ;
private:
std::shared_ptr<PiPedalUI> FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin);
struct FindWritablePathPropertiesResult {
std::shared_ptr<PiPedalUI> pipedalUi;
bool hasUnsupportedPatchProperties = false;
};
FindWritablePathPropertiesResult FindWritablePathProperties(PluginHost *lv2Host, const LilvPlugin *pPlugin);
bool HasFactoryPresets(PluginHost *lv2Host, const LilvPlugin *plugin);
std::string bundle_path_;
@@ -369,6 +373,7 @@ namespace pipedal
std::vector<std::shared_ptr<Lv2PortGroup>> port_groups_;
bool is_valid_ = false;
PiPedalUI::ptr piPedalUI_;
bool hasUnsupportedPatchProperties_ = false;
bool IsSupportedFeature(const std::string &feature) const;
@@ -391,6 +396,7 @@ namespace pipedal
LV2_PROPERTY_GETSET(port_groups)
LV2_PROPERTY_GETSET(has_factory_presets)
LV2_PROPERTY_GETSET(piPedalUI)
LV2_PROPERTY_GETSET(hasUnsupportedPatchProperties)
const Lv2PortInfo &getPort(const std::string &symbol)
{
@@ -779,6 +785,7 @@ namespace pipedal
void free_world();
std::vector<std::shared_ptr<Lv2PluginInfo>> plugins_;
std::map<std::string,std::shared_ptr<Lv2PluginInfo>> pluginsByUri;
std::vector<Lv2PluginUiInfo> ui_plugins_;
std::map<std::string, std::shared_ptr<Lv2PluginClass>> classesMap;
+118 -65
View File
@@ -34,6 +34,9 @@
#include "ss.hpp"
#include "ofstream_synced.hpp"
#include "ModFileTypes.hpp"
#include <set>
#include <MimeTypes.hpp>
#include "util.hpp"
using namespace pipedal;
namespace fs = std::filesystem;
@@ -1388,7 +1391,7 @@ uint64_t Storage::SavePluginPreset(
void Storage::UpdatePluginPresets(const PluginUiPresets &pluginPresets)
{
// handles deletions, renaming, and reordering only.
// If you need to add a preset, you neet to call SavePluginPreset or DulicatePluginPreset instead.
// If you need to add a preset, you need to call SavePluginPreset or DuplicatePluginPreset instead.
PluginPresets presets = this->GetPluginPresets(pluginPresets.pluginUri_);
PluginPresets newPresets;
@@ -1623,51 +1626,6 @@ static void ThrowPermissionDeniedError()
throw std::logic_error("Permission denied.");
}
std::vector<std::string> Storage::GetFileList(const UiFileProperty &fileProperty)
{
if (!UiFileProperty::IsDirectoryNameValid(fileProperty.directory()))
{
ThrowPermissionDeniedError();
}
std::vector<std::string> 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();
try
{
for (auto const &dir_entry : std::filesystem::directory_iterator(audioFileDirectory))
{
if (dir_entry.is_regular_file())
{
auto &path = dir_entry.path();
auto name = path.filename().string();
if (name.length() > 0 && name[0] != '.') // don't show hidden files.
{
if (fileProperty.IsValidExtension(path.extension().string()))
{
// a relative path!
result.push_back(path);
}
}
}
}
}
catch (const std::exception &error)
{
throw std::logic_error("GetFileList failed. Directory not found: " + audioFileDirectory.string());
}
}
// sort lexicographically
auto collator = Locale::GetInstance()->GetCollator();
std::sort(result.begin(), result.end(), [&collator](const std::string &left, const std::string &right)
{ return collator->Compare(left, right) < 0; });
return result;
}
static bool ensureNoDotDot(const std::filesystem::path &path)
{
@@ -1695,8 +1653,10 @@ static bool ensureNoDotDot(const std::filesystem::path &path)
return true;
}
static void AddFilesToResult(
FileRequestResult &result,
const ModFileTypes::ModDirectory *modDirectoryInfo, //yyx
const UiFileProperty &fileProperty,
const fs::path &rootPath)
{
@@ -1705,7 +1665,12 @@ static void AddFilesToResult(
return; // silently without error.
}
auto &resultFiles = result.files_;
try
std::set<std::string> validExtensions = fileProperty.GetPermittedFileExtensions(
modDirectoryInfo? modDirectoryInfo->modType: "");
try
{
for (auto const &dir_entry : std::filesystem::directory_iterator(rootPath))
{
@@ -1713,9 +1678,15 @@ static void AddFilesToResult(
auto name = path.filename().string();
if (dir_entry.is_regular_file())
{
std::string extension = UiFileProperty::GetFileExtension(path);
bool match = false;
if (name.length() > 0 && name[0] != '.') // don't show hidden files.
{
if (fileProperty.IsValidExtension(path.extension().string()))
bool match = validExtensions.size() == 0 || validExtensions.contains(extension)
|| validExtensions.contains(".*");
if (match)
{
resultFiles.push_back(
FileEntry(path, name, false, false));
@@ -1778,6 +1749,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
result.breadcrumbs_.push_back({"", "Home"});
fs::path fsRelativePath{relativePath};
const ModFileTypes::ModDirectory *rootModDirectory = nullptr;
for (const auto &modDirectory : fileProperty.modDirectories())
{
const ModFileTypes::ModDirectory *modDirectoryInfo = ModFileTypes::GetModDirectory(modDirectory);
@@ -1785,6 +1757,7 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
{
if (isSubdirectory(fsRelativePath, uploadsDirectory / modDirectoryInfo->pipedalPath))
{
rootModDirectory = modDirectoryInfo;
modDirectoryPath = uploadsDirectory / modDirectoryInfo->pipedalPath;
result.breadcrumbs_.push_back({modDirectoryPath.string(), modDirectoryInfo->displayName});
break;
@@ -1804,12 +1777,12 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
}
}
// add remaing path segements as breadcrumbs.
// add reaming path segments as breadcrumbs.
{
fs::path modPath{modDirectoryPath};
fs::path rp{relativePath};
auto iRp = rp.begin();
// skip past one or more segments in the modDiretoryPath.
// skip past one or more segments in the modDirectoryPath.
for (auto iModPath = modPath.begin(); iModPath != modPath.end(); ++iModPath)
{
if (iRp != rp.end())
@@ -1824,7 +1797,8 @@ FileRequestResult Storage::GetModFileList2(const std::string &relativePath, cons
}
}
AddFilesToResult(result, fileProperty, relativePath);
AddFilesToResult(result,rootModDirectory, fileProperty, relativePath);
result.currentDirectory_ = relativePath;
return result;
}
@@ -1911,7 +1885,14 @@ FileRequestResult Storage::GetFileList2(const std::string &relativePath_, const
{
throw std::runtime_error(SS("Improper location. " << absolutePath));
}
AddFilesToResult(result, fileProperty, absolutePath);
const ModFileTypes::ModDirectory*pModDirectory = nullptr;
if (fileProperty.modDirectories().size() > 0)
{
pModDirectory = ModFileTypes::GetModDirectory(fileProperty.modDirectories()[0]);
}
AddFilesToResult(result,pModDirectory, fileProperty, absolutePath);
return result;
}
@@ -2005,7 +1986,10 @@ std::filesystem::path Storage::MakeUserFilePath(const std::string &directory, co
}
return result;
}
std::string Storage::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength)
std::string Storage::UploadUserFile(const std::string &directory,
UiFileProperty::ptr uiFileProperty,
const std::string &filename,
std::istream &stream, size_t contentLength)
{
std::filesystem::path path;
if (directory.length() != 0)
@@ -2013,14 +1997,23 @@ std::string Storage::UploadUserFile(const std::string &directory, const std::str
path = this->MakeUserFilePath(directory, filename);
}
else
{
if (patchProperty.length() == 0)
{
throw std::logic_error("Malformed request.");
}
throw std::logic_error("patchProperty directory not implemented.");
throw std::logic_error("Directory argument not supplied.");
}
fs::path relativePath;
try {
relativePath = MakeRelativePath(path,this->GetPluginUploadDirectory());
} catch (const std::exception& e)
{
throw std::logic_error("Permission denied. Path is outside the upload storage directory.");
}
if (!uiFileProperty->IsValidExtension(relativePath))
{
throw std::logic_error("Permission denied. Invalid file extension for this directory.");
}
{
try
{
@@ -2138,24 +2131,84 @@ void Storage::FillSampleDirectoryTree(FilePropertyDirectoryTree *node, const std
return collator->Compare(left->directoryName_, right->directoryName_) < 0;
});
}
FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty)
static void GetAllExtensions(std::set<std::string>&result, const std::filesystem::path &path)
{
assert(fs::is_directory(path));
for (const auto&dirEnt : fs::directory_iterator(path))
{
if (dirEnt.is_directory())
{
GetAllExtensions(result,dirEnt.path());
} else {
std::string filename = dirEnt.path().filename();
if (!filename.starts_with('.'))
{
if (dirEnt.path().has_extension())
{
std::string extension = dirEnt.path().extension();
result.insert(extension);
}
}
}
}
}
static std::set<std::string> GetAllExtensions(const std::filesystem::path &path)
{
std::set<std::string> result;
if (fs::is_regular_file(path)) {
result.insert(UiFileProperty::GetFileExtension(path));
} else {
GetAllExtensions(result,path);
}
return result;
}
FilePropertyDirectoryTree::ptr Storage::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::filesystem::path&selectedPath)
{
fs::path uploadDirectory = this->GetPluginUploadDirectory();
fs::path relativePath;
try {
relativePath = MakeRelativePath(selectedPath,uploadDirectory);
} catch (const std::exception&e) {
// not an upload directory.
throw std::runtime_error(SS("Permission denied: " << selectedPath));
}
std::set<std::string> fileExtensions = GetAllExtensions(selectedPath);
if (hasSyntheticModRoot(uiFileProperty))
{
if (relativePath.empty())
{
throw std::runtime_error(SS("Permission denied: " << selectedPath));
}
FilePropertyDirectoryTree::ptr result = std::make_unique<FilePropertyDirectoryTree>("", "Home");
result->isProtected_ = true;
std::string parentModDirectory = uiFileProperty.getParentModDirectory(relativePath);
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 (IsSubset(fileExtensions,modDirectoryInfo->fileExtensions) ||
modDirectoryInfo->fileExtensions.contains(".*") ||
modDirectoryInfo->fileExtensions.empty() // Private directory that accepts files of any type.
)
{
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())
+4 -3
View File
@@ -37,6 +37,7 @@ namespace pipedal {
class UiFileProperty;
class Lv2PluginInfo;
class UiFileProperty;
class CurrentPreset {
public:
@@ -153,7 +154,6 @@ public:
void MoveBank(int from, int to);
int64_t DeleteBank(int64_t bankId);
std::vector<std::string> GetFileList(const UiFileProperty&fileProperty);
FileRequestResult GetFileList2(const std::string&relativePath,const UiFileProperty&fileProperty);
FileRequestResult GetModFileList2(const std::string &relativePath,const UiFileProperty &fileProperty);
@@ -222,13 +222,14 @@ public:
void SetSystemMidiBindings(const std::vector<MidiBinding>&bindings);
std::vector<MidiBinding> GetSystemMidiBindings();
void DeleteSampleFile(const std::filesystem::path &fileName);
std::string UploadUserFile(const std::string &directory, const std::string &patchProperty,const std::string&filename,std::istream&stream, size_t contentLength);
std::string UploadUserFile(const std::string &directory,
std::shared_ptr<UiFileProperty> uiFileProperty ,const std::string&filename,std::istream&stream, size_t contentLength);
std::string CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty);
std::string RenameFilePropertyFile(
const std::string&oldRelativePath,
const std::string&newRelativePath,
const UiFileProperty&uiFileProperty);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty);
FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath);
};
+13 -3
View File
@@ -550,11 +550,21 @@ public:
res.set(HttpField::content_type, "application/json");
res.set(HttpField::cache_control, "no-cache");
std::string instanceId = request_uri.query("id");
std::string instanceIdString = 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");
int64_t instanceId;
{
std::istringstream ss { instanceIdString};
ss >> instanceId;
if (!ss)
{
throw PiPedalException("Invalid instanceID");
}
}
if (patchProperty.length() == 0 && directory.length() == 0)
{
throw PiPedalException("Malformed request.");
@@ -588,7 +598,7 @@ public:
if (extensionChecker.IsValidExtension(extension))
{
auto si = zipFile->GetFileInputStream(inputFile);
std::string path = this->model->UploadUserFile(directory, patchProperty, inputFile, si, zipFile->GetFileSize(inputFile));
std::string path = this->model->UploadUserFile(directory, instanceId,patchProperty, inputFile, si, zipFile->GetFileSize(inputFile));
}
}
}
@@ -606,7 +616,7 @@ public:
}
else
{
outputFileName = this->model->UploadUserFile(directory, patchProperty, filename, req.get_body_input_stream(), req.content_length());
outputFileName = this->model->UploadUserFile(directory, instanceId,patchProperty, filename, req.get_body_input_stream(), req.content_length());
}
if (outputFileName.is_relative())
+257
View File
@@ -0,0 +1,257 @@
/*
* Copyright (c) 2023 Robin E. R. Davies
* All rights reserved.
* 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.
*/
#ifndef FILEBROWSERFILES_H
#define FILEBROWSERFILES_H
#define LV2_FILEBROWSER_URI "http://two-play.com/ns/ext/fileBrowser"
#define LV2_FILEBROWSER_PREFIX LV2_FILEBROWSER_URI "#"
#define LV2_FILEBROWSER__files LV2_FILEBROWSER_PREFIX "files" ///< http://two-play.com/ns/ext/fileBrowser#files >
/**
@defgroup filedialog LV2_FileBrowser_Files
@ingroup lv2
Because PiPedal is accessed remotely through a web interface, audio and model files
have to be uploaded into standardized directories using the Upload File button, found in
the File Property Dialog. Plugins that run locally can place sample and model files
more casually, since users can browse anywhere on their computer using the system
file dialog.
This LV2 Extension allows plugin developers to determine the location of well-known directory.
The extension addresses three primary uses-cases:
- Allow plugins to published factory-installed resource files that can be
selected using the Patch Property file dialog. Typically. Source files would be
stored in the bundle directory of the plugin at install time. On first use, the
plugin would create symlinks in the well-known directory that reference files in the
bundle directory.
- Allow plugins to reference factory-installed resource files that can be selected using
the Patch Property file dialog.
- Allow recording plugins to locate the directory in which new files should be
placed.
The plugin recognizes the following well-known directory locations, which are based on MOD fileType
conventions.
"audioloop"
Audio Loops, meant to be used for looper-style plugins.
"audiorecording",
Audio Recordings, triggered by plugins and stored in the unit
"audiosample",
One-shot Audio Samples, meant to be used for sampler-style plugins
"audiotrack"
Audio Tracks, meant to be used as full-performance/song or backtrack
"cabsim"
Impulse response files for convolution-based Cabinet response simulation.
"h2drumkit"
Hydrogen Drumkits, must use h2drumkit file extension. Not currently supported by PiPedal.
"ir"
Impulse response files of arbitrary size. May be either CAB IR files, or Reverbe IR files.
"midiclip"
MIDI Clips", {".mid", ".midi"}}, // MIDI Clips, to be used in sync with
host tempo, must have mid or midi file extension
"midisong"
MIDI Songs", {".mid", ".midi"}}, meant to be used as
full-performance/song or backtrack.
"sf2"
SF2 Instruments, must have sf2 or sf3 file extension
"sfz"
SFZ Instruments, must have sfz file extension
"audio"
Parent directory of all audio file directories. Not officially documented
by MOD, but has been observed in the field.
"nammodel"
.nam files: Nueral amp model's for Steven Atkin's Neural Amp Modeler library.
"aidadspmodel"
AIDA IAX model files. (file extensions of ".json")
"mlmodel"
Model files for plugins based on the ML neural net library. (file
extensions of ".json"). TooB ML, Proteus, probably others.
"~"
References a directory that is for private use by only the current plugin.
Users can upload and select files in the private directory when using
the current plugin, but no other plugins can access the files. (Not
supported by MOD).
Methods are are safe on the realtime thread, as they perform file i/o and allocate memory. This feature should be used
at initialization time, in the call to Activate, or else on a scheduler thread.
@{
*/
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
LV2_FileBrowser_Status_Success = 0,
LV2_FileBrowser_Status_Err_InvalidParameter = 1,
LV2_FileBrowser_Status_Err_Filesystem = 2,
} LV2_FileBrowser_Status;
typedef void*
LV2_FileBrowser_Files_Handle; ///< Opaque handle for pipedal:uploadPath feature
/**
Feature data for filedialog:files (@ref LV2_FILEBROWSER_file).
*/
typedef struct {
/**
Opaque host data.
*/
LV2_FileBrowser_Files_Handle handle;
/**
Publish a resource file provided by the plugin to a well-known directory location.
@param handle MUST be the `handle` member of this struct.
@param version The version number of the resource file.
@param resourcePath The source filename./
@param uploadDirectory The target filename.
@return LV2_FileBrowser_Status Zero if successful.
The version number is the version number of the resource file(s). The host keeps track of which
resource files have previously been installed, and the version thereof. Files will only be
published once. This allows users to delete published files if they want to; and allows
upgraded plugins to write upgraded resource files.
`resourcePath` is specified relative to the plugin's bundle directory. You can supply an
absolute path if you want, but it's generally better to place resource files in the plugin's
bundle, and specify the location with a relative path. If the resourcePath specifies a file,
only the file will be copied; if the the path specifies a directory, all files and subdirectories
will be published.
`uploadDirectory` most be a relative path of a directory, and the first segment of the path must be one of
the well-known directory locations.
A return code of LV2_FileBrowser_Status_Err_InvalidParameter indicates
either that the well-known directory was not found, or that an absolute
path was provided where a relative path was expected.
LV2_FileBrowser_Status_Err_Filesystem will be return if some kind of
catastrophic filesystem error occurred (out of disk space, perhaps, or
permission denied).
Example:
[code]
// becomes /usr/lib/lv2/ToobAmp.lv2/namFactoryModels/FenderBlackface.nam (for example)
const char*sourceFile = "namFactoryModels/Fender Blackface.nam"
// /becomes /var/pipedal/audio_uploads/NeuralAmpModels/TooB NAM (for example)
const char(*targetDirectory = "namModel/TooB NAM")
Lv2_FileBrowserStatus ec =
feature.publish_resource_files(feature.handle,1,sourceFile,targetDirectory);
// on success creates a symlink at
// /var/pipedal/audio_uploads/NeuralAmpModels/TooB NAM/Fender Blackface.nam
[/code]
*/
LV2_FileBrowser_Status (*publish_resource_files)(
LV2_FileBrowser_Files_Handle handle,
uint32_t version,
const char*resourcePath,
const char*uploadDirectory);
/**
ccc
@param handle MUST be the `handle` member of this struct.
@param path A relative path, starting with a well-known directory name.
@return The absolute path to use for the new directory.
This function can be used by plugins to get the path of files or directories
that can be browsed by file dialogs. get_filebrowser_path, unlike
map_path() does not create or modify files or directories in any way.
The path can have multiple segments, but the first segment of the path must be
the name of a well-known directory.
The caller must free memory for the returned value with
LV2_FileBrowser_Files.free_path().
Example:
[code]
const char *filePath = feature.get_upload_path(feature.handle,"audio/Tracks/Xmas/Frosty.wav");
// returns (for example)
// /var/pipedal/audio_uploads/shared/audio/Tracks/Xmas/Frosty.wav
[/code]
*/
char* (*get_upload_path)(LV2_FileBrowser_Files_Handle handle, const char* path);
/**
Map resource bundle filenames to upload directories.
@param handle MUST be the `handle` member of this struct.
@param path File path name
@param fileBrowserDirectory relative directory of the file browser directory in which to create a link.
@return The absolute pathname of a file in the resource directory.
The primary purpose of this method is to map file references to files in the plugin's bundle
directory to corresponding files in the a file browser directory.
If the path is relative, the full path is resolved using the `rootResourceDirectory` path.
If the full path is not a child of rootResourceDirectory or if the path is relative,
get_upload_path returns the supplied path unmodified.
`
If the filename is a child of rootResourceDirectory, creates a corresponding link to the file in the file browser
directory and returns the path of that link.
The caller must free memory for the returned value with `LV2_FileBrowser_Files.free_path()`.
Example:
*/
char* (*map_path)(LV2_FileBrowser_Files_Handle handle, const char* path, const char*rootResourceDirectory, const char*fileBrowserDirectory);
/**
Free a path returned by LV2_FileBrowser_Files.get_upload_path() or LV2_FileBrowser_Files.map_path()
@param handle MUST be the `handle` member of this struct.
@param path A path previously returned by a method of `LV2_FileBrowser_Files`.
*/
void (*free_path)(LV2_FileBrowser_Files_Handle handle, char* path);
} LV2_FileBrowser_Files;
#ifdef __cplusplus
} // extern "C"
#endif
#endif // FILEBROWSERFILES_H
+2
View File
@@ -14,6 +14,8 @@
- Feature for directory locations.
- center ellipsis on file properties.
- State of MIDI plugins:
- plugin routing is implemented. (maybe needs a not-cheating UI)
- MIDI message routing is not.