Checkpoint

This commit is contained in:
Robin E. R. Davies
2025-07-07 02:30:30 -04:00
parent fddfde9313
commit e7cdd38056
83 changed files with 4733 additions and 214 deletions
-3
View File
@@ -108,7 +108,6 @@ add_custom_command(
public/iso_codes.json
public/logo512.png
public/sample_lv2_plugins.json
public/vite.svg
src/pipedal/SplitUiControls.tsx
src/pipedal/JackHostStatus.tsx
src/pipedal/VirtualKeyboardHandler.tsx
@@ -291,8 +290,6 @@ add_custom_command(
src/pipedal/MidiChannelBinding.tsx
src/pipedal/ModFileTypes.tsx
src/assets/react.svg
src/App.css
src/App.tsx
src/vite-env.d.ts
src/index.css
src/main.tsx
+1 -1
View File
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="/css/roboto.css" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/css/modGui.css" />
<style>
BODY {
background: #D0D0D0;
+64
View File
@@ -0,0 +1,64 @@
.mod-pedal {
}
.mod-pedal .mod-light {
}
.mod-pedal .mod-light.off { background-image:url(../img/red-light-off.png); }
.mod-pedal .mod-light.on { background-image:url(../img/red-light-on.png); }
.mod-pedal .mod-pedal-input {
position: absolute;
top: 70px;
left: -24px;
width: 24px;
}
.mod-pedal .mod-pedal-output {
position: absolute;
top: 70px;
right: -25px;
width: 25px;
}
.mod-pedal .mod-pedal-input .mod-audio-input,
.mod-pedal .mod-pedal-input .mod-midi-input,
.mod-pedal .mod-pedal-input .mod-cv-input {
position: relative;
margin-bottom: 20px;
}
.mod-audio-input .mod-pedal-input-image {
background-image:url(../img/audio-input.png) ;
width: 24px;
height: 56px;
margin-bottom: 20px;
}
.mod-audio-output .mod-pedal-output-image {
background-image:url(../img/audio-output.png);
width: 24px;
height: 56px;
margin-bottom: 20px;
}
.ppmod-dial-control-image {
width: 48px;
height: 48px;
object-fit: fill;
}
.ppmod-dial-control-frame
{
position: relative;
width: 48px;
height: 48px;
overflow: hidden;
}
.mod-knob-title {
white-space: nowrap;
overflow: hidden;
}
.mod-slider-title {
white-space: nowrap;
overflow: hidden;
}
+4 -4
View File
@@ -2,28 +2,28 @@
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
font-display: swap;
font-display: block;
src: url(/fonts/Roboto-Light.woff2) format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
font-display: swap;
font-display: block;
src: url(/fonts/Roboto-Regular.woff2) format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
font-display: swap;
font-display: block;
src: url(/fonts/Roboto-Medium.woff2) format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
font-display: swap;
font-display: block;
src: url(/fonts/Roboto-Bold.woff2) format('woff2');
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
@font-face {
font-family: 'EnglandHand';
src: url('england-webfont.woff');
font-weight: normal;
font-display: swap;
font-style: normal;
}
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
@font-face {
font-family: 'epflul';
src: url('epf_lul-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
Binary file not shown.
+33
View File
@@ -0,0 +1,33 @@
/*
* Web Fonts from fontspring.com
*
* All OpenType features and all extended glyphs have been removed.
* Fully installable fonts can be purchased at http://www.fontspring.com
*
* The fonts included in this stylesheet are subject to the End User License you purchased
* from Fontspring. The fonts are protected under domestic and international trademark and
* copyright law. You are prohibited from modifying, reverse engineering, duplicating, or
* distributing this font software.
*
* (c) 2010-2012 Fontspring
*
*
*
*
* The fonts included are copyrighted by the vendor listed below.
*
* Vendor: Fontfabric
* License URL: http://www.fontspring.com/fflicense/fontfabric
*
*
*/
@font-face {
font-family: 'Nexa';
src: url('Nexa_Free_Bold-webfont.woff') format('woff');
font-display: block;
font-weight: normal;
font-style: normal;
}
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 25, 2013 */
@font-face {
font-family: 'Pirulen';
src: url('pirulen_rg-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
/* Generated by Font Squirrel (http://www.fontsquirrel.com) on February 27, 2013 */
@font-face {
font-family: 'Questrial';
src: url('questrial-regular-webfont.woff') format('woff');
font-display: block;
font-weight: normal;
font-style: normal;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File
-36
View File
@@ -1,36 +0,0 @@
import { useState } from 'react'
import ReactLogo from './assets/react.svg?react'
//import ViteLogo from '/vite.svg?react'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
{/* <ViteLogo/> */}
</a>
<a href="https://react.dev" target="_blank">
<ReactLogo style={{background: "blue", padding: 96}} />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
}
export default App
+1 -1
View File
@@ -152,7 +152,7 @@ const AboutDialog = class extends Component<AboutDialogProps, AboutDialogState>
<ArrowBackIcon />
</IconButtonEx>
<Typography noWrap variant="h6"
sx={{ maringLeft: 2, flex: 1 }}
sx={{ marginLeft: 2, flex: 1 }}
>
About
</Typography>
+15 -8
View File
@@ -18,13 +18,15 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import React from 'react';
import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import CssBaseline from '@mui/material/CssBaseline';
import VirtualKeyboardHandler from './VirtualKeyboardHandler';
import AppThemed from "./AppThemed";
import { isDarkMode } from './DarkMode';
import Tone3000AuthComplete from './Tone3000AuthComplete';
import FontTest from './FontTest';
declare module '@mui/material/styles' {
@@ -225,8 +227,13 @@ type AppThemeProps = {
function isTone3000Auth() {
let url = new URL(window.location.href);
let param = url.searchParams.get("api_key");
return (param !== null && param !== "")
let param = url.searchParams.get("api_key");
return (param !== null && param !== "")
}
function isFontTest() {
let url = new URL(window.location.href);
let param = url.searchParams.get("fontTest");
return (param !== null)
}
const App = (class extends React.Component {
@@ -248,11 +255,11 @@ const App = (class extends React.Component {
<StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}>
<CssBaseline />
{isTone3000Auth() ? (
<Tone3000AuthComplete />
) : (
<AppThemed />
)}
{
isTone3000Auth() && (<Tone3000AuthComplete />)
|| isFontTest() && (<FontTest />)
|| (<AppThemed />)
}
</ThemeProvider>
</StyledEngineProvider>
);
+2
View File
@@ -19,6 +19,7 @@
import './AppThemed.css';
//import {alpha} from '@mui/material/styles';
import AppBar from '@mui/material/AppBar';
@@ -74,6 +75,7 @@ import HelpOutlineIcon from './svg/ic_help_outline.svg?react';
import FxAmplifierIcon from './svg/fx_amplifier.svg?react';
import { PerformanceView } from './PerformanceView';
import DialogEx from './DialogEx';
import { IDialogStackable, popDialogStack, pushDialogStack } from './DialogStack';
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 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.
*/
+17 -3
View File
@@ -42,7 +42,11 @@ let pluginFactories: IControlViewFactory[] = [
];
export function GetControlView(pedalboardItem?: PedalboardItem | null): React.ReactNode {
export function GetControlView(
pedalboardItem: PedalboardItem | null,
showModUi: boolean,
onSetShowModGui: (instanceId: number, showModGui: boolean) => void
): React.ReactNode {
let model: PiPedalModel = PiPedalModelFactory.getInstance();
if (!pedalboardItem) {
@@ -50,7 +54,13 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
}
if (pedalboardItem.isStart() || pedalboardItem.isEnd()) {
return (
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
<PluginControlView instanceId={pedalboardItem.instanceId}
item={pedalboardItem}
showModGui={showModUi}
onSetShowModGui={(instanceId, showModGui) => {
onSetShowModGui?.(instanceId, showModGui);
}}
/>
);
}
if (pedalboardItem.isSplit()) {
@@ -73,7 +83,11 @@ export function GetControlView(pedalboardItem?: PedalboardItem | null): React.Re
</div>
} else {
return (
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} />
<PluginControlView instanceId={pedalboardItem.instanceId} item={pedalboardItem} showModGui={showModUi}
onSetShowModGui={(instanceId, showModGui) => {
onSetShowModGui?.(instanceId, showModGui);
}}
/>
)
}
}
+75
View File
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2025 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.
*/
import React from 'react';
import Divider from '@mui/material/Divider';
function TypeSample(props: { fontFamily: string, cssRef: string, fontWeights: number[] }) {
let { fontFamily, cssRef, fontWeights } = props;
React.useEffect(() => {
let link = document.createElement('link');
link.rel = 'stylesheet';
link.href = cssRef;
document.head.appendChild(link);
return () => {
document.head.removeChild(link);
};
}, []);
return (
<div>
{fontWeights.map((weight) => (
<div key={`${fontFamily}-${weight}`} style={{ marginBottom: 24 }}>
<Divider />
<div style={{ marginLeft: 24, marginRight: 24 }}>
<h4 style={{marginBottom: 0}}>{fontFamily} {weight}</h4>
<div style={{ fontFamily: fontFamily, fontSize: 20, fontWeight: weight, marginLeft: 24 }}>
<div style={{ fontFamily: fontFamily }}>The quick brown fox jumped over the lazy dog.</div>
</div>
</div>
</div>
))}
</div>
);
}
function FontTest() {
return (
<div>
<h4 style={{ marginLeft: 24, marginRight: 24 }}>Font Test</h4>
{TypeSample({ fontFamily: "EnglandHand", cssRef: "/fonts/england-hand/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "epflul", cssRef: "/fonts/epf/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Nexa", cssRef: "/fonts/nexa/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Questrial", cssRef: "/fonts/questrial/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
{TypeSample({ fontFamily: "Pirulen", cssRef: "/fonts/pirulen/stylesheet.css", fontWeights: [400] })}
</div>
);
}
export default FontTest;
+4 -2
View File
@@ -21,12 +21,13 @@
* SOFTWARE.
*/
import React from 'react';
import IconButton, {IconButtonProps} from '@mui/material/IconButton';
import ToolTipEx from './ToolTipEx';
import Typography from "@mui/material/Typography";
interface IconButtonExProps extends IconButtonProps {
tooltip: string;
tooltip: React.ReactElement | string;
style?: React.CSSProperties;
};
@@ -35,9 +36,10 @@ function IconButtonEx(props: IconButtonExProps) {
return (
<ToolTipEx title={
(typeof props.tooltip === 'string') ?
(
<Typography variant="caption">{tooltip || extra['aria-label'] }</Typography>
)
): (props.tooltip as React.ReactElement)
}
>
<IconButton {...extra} style={style} />
+17 -8
View File
@@ -164,7 +164,8 @@ interface PluginGridProps extends WithStyles<typeof pluginGridStyles> {
uri?: string;
minimumItemWidth?: number;
theme: Theme;
open: boolean
open: boolean;
modGuiOnly?: boolean;
};
type PluginGridState = {
@@ -526,6 +527,8 @@ export const LoadPluginDialog =
for (let i = 0; i < plugins.length; ++i) {
let plugin = plugins[i];
if (this.props.modGuiOnly == true && !plugin.modGui)
continue;
try {
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
let score: number = 0;
@@ -720,9 +723,15 @@ export const LoadPluginDialog =
style={{ overflowX: "hidden", overflowY: "hidden", display: "flex", flexDirection: "column", flexWrap: "nowrap" }}
aria-labelledby="select-plugin-dialog-title">
<div style={{ display: "flex", flexDirection: "column", flexWrap: "nowrap", height: "100%" }}>
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54 }}>
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap", width: "100%", alignItems: "center" }}>
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }} style={{ flex: "0 0 auto" }} >
<DialogTitle id="select-plugin-dialog-title" style={{ flex: "0 0 auto", padding: "0px", height: 54,
marginRight: 16
}}>
<div style={{ display: "flex", flexDirection: "row", paddingTop: 3, paddingBottom: 3, flexWrap: "nowrap",
height: 64,
width: "100%", alignItems: "center"
}}>
<IconButtonEx tooltip="Back" onClick={() => { this.cancel(); }}
style={{ flex: "0 0 auto", marginRight: 16 }} >
<ArrowBackIcon />
</IconButtonEx>
@@ -731,9 +740,9 @@ export const LoadPluginDialog =
visibility: (this.state.search_collapsed ? "visible" : "collapse"),
width: (this.state.search_collapsed ? undefined : "0px")
}}>
{this.state.client_width > 520 ? "Select Plugin" : ""}
{this.state.client_width > 645 ? "Select Plugin" : ""}
</Typography>
<div style={{ flex: "1 1 auto" }} >
<div style={{ flex: "2 2 auto" }} >
<SearchControl collapsed={this.state.search_collapsed} searchText={this.state.search_string}
inputRef={this.searchInputRef}
showSearchIcon={showSearchIcon}
@@ -784,13 +793,13 @@ export const LoadPluginDialog =
)}
</IconButtonEx>
</div>
<div style={{ flex: "0 0 160px", marginRight: 24 }} >
<div style={{ flex: "0 1 160px", position: "relative", marginRight: 24 }} >
<TextField select variant="standard"
defaultValue={this.state.filterType}
key={this.state.filterType}
onChange={(e) => { this.onFilterChange(e); }}
sx={{ minWidth: 160 }}
sx={{ width: "100%",minWidth: "100%" }}
>
{this.createFilterOptions()}
</TextField>
+145 -26
View File
@@ -26,10 +26,10 @@ interface Deserializable<T> {
}
const noteNames: string[] = [
"C","C#","D","Eb","E","F","F#","G","Ab","A","Bb","B","C"
"C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B", "C"
];
function semitone12TETValue(value: number) : string {
function semitone12TETValue(value: number): string {
let iValue = Math.round(value) % 12;
return noteNames[iValue];
@@ -313,10 +313,85 @@ export class UiFileProperty {
useLegacyModDirectory: boolean = false;
};
export class ModGuiPort {
deserialize(input: any): ModGuiPort {
this.index = input.index;
this.symbol = input.symbol;
this.name = input.name;
return this;
}
static deserialize_array(input: any): ModGuiPort[] {
let result: ModGuiPort[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new ModGuiPort().deserialize(input[i]);
}
return result;
}
index: number = -1;
symbol: string = "";
name: string = "";
};
export class ModGui {
deserialize(input: any): ModGui {
this.pluginUri = input.pluginUri;
this.resourceDirectory = input.resourceDirectory;
this.iconTemplate = input.iconTemplate ?? "";
this.settingsTemplate = input.settingsTemplate;
this.javascript = input.javascript;
this.stylesheet = input.stylesheet ?? "";
this.screenshot = input.screenshot;
this.thumbnail = input.thumbnail;
this.discussionUrl = input.discussionUrl;
this.documentationUrl = input.documentationUrl;
this.brand = input.brand;
this.label = input.label;
this.model = input.model;
this.panel = input.panel;
this.color = input.color;
this.knob = input.knob;
if (input.ports) {
this.ports = ModGuiPort.deserialize_array(input.ports);
} else {
this.ports = [];
}
return this;
}
static serialize_array(input: any): ModGui[] {
let result: ModGui[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new ModGui().deserialize(input[i]);
}
return result;
}
pluginUri = "";
resourceDirectory = "";
iconTemplate = "";
settingsTemplate = "";
javascript = "";
stylesheet = "";
screenshot = "";
thumbnail = "";
discussionUrl = "";
documentationUrl = "";
brand = "";
label = "";
model = "";
panel = "";
color = "";
knob = "";
ports: ModGuiPort[] = [];
};
export class Lv2Plugin implements Deserializable<Lv2Plugin> {
deserialize(input: any): Lv2Plugin {
this.uri = input.uri;
this.name = input.name;
this.minorVersion = input.minorVersion ? input.minorVersion : 0;
this.microVersion = input.microVersion ? input.microVersion : 0;
this.brand = input.name ? input.name : "";
this.label = input.label ? input.label : this.name;
this.plugin_class = input.plugin_class;
@@ -344,12 +419,18 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
} else {
this.uiPortNotifications = [];
}
this.modGui = null;
if (input.modGui) {
this.modGui = new ModGui().deserialize(input.modGui);
}
return this;
}
static EmptyFeatures: string[] = [];
uri: string = "";
name: string = "";
minorVersion: number = 0;
microVersion: number = 0;
brand: string = "";
label: string = "";
plugin_class: string = "";
@@ -364,6 +445,7 @@ export class Lv2Plugin implements Deserializable<Lv2Plugin> {
fileProperties: UiFileProperty[] = [];
frequencyPlots: UiFrequencyPlot[] = [];
uiPortNotifications: UiPropertyNotification[] = [];
modGui: ModGui | null = null;
}
@@ -459,10 +541,12 @@ export enum ControlType {
Vu,
Progress,
DbVu,
OutputText
OutputText,
BypassLight
}
const textUnits : Set<Units> = new Set<Units>([
const textUnits: Set<Units> = new Set<Units>([
Units.bar,
Units.beat,
Units.bpm,
@@ -478,13 +562,12 @@ const textUnits : Set<Units> = new Set<Units>([
Units.semitone12TET
]);
function displayUnitAsText(unit: Units): boolean
{
function displayUnitAsText(unit: Units): boolean {
return textUnits.has(unit);
}
export class UiControl implements Deserializable<UiControl> {
deserialize(input: any): UiControl {
deserialize(input: any): UiControl {
this.symbol = input.symbol;
this.name = input.name;
this.index = input.index;
@@ -531,12 +614,11 @@ deserialize(input: any): UiControl {
this.controlType = ControlType.Progress;
} else if (this.enumeration_property) {
this.controlType = ControlType.OutputText;
} else if (displayUnitAsText(this.units))
{
} else if (displayUnitAsText(this.units)) {
this.controlType = ControlType.OutputText
} else {
this.controlType = ControlType.Vu;
}
}
}
if (this.isValidEnumeration()) {
this.controlType = ControlType.Select;
@@ -548,10 +630,8 @@ deserialize(input: any): UiControl {
this.controlType = ControlType.OnOffSwitch;
}
}
if (this.is_input)
{
if (this.pipedal_graphicEq)
{
if (this.is_input) {
if (this.pipedal_graphicEq) {
this.controlType = ControlType.GraphicEq;
}
else if (this.mod_momentaryOnByDefault) {
@@ -560,7 +640,7 @@ deserialize(input: any): UiControl {
this.controlType = ControlType.Momentary;
} else if (this.trigger_property) {
this.controlType = ControlType.Trigger;
}
}
}
return this;
}
@@ -654,7 +734,7 @@ deserialize(input: any): UiControl {
isAbToggle(): boolean {
return this.controlType === ControlType.ABSwitch;
}
isGraphicEq() : boolean {
isGraphicEq(): boolean {
return this.controlType === ControlType.GraphicEq;
}
isSelect(): boolean {
@@ -739,15 +819,12 @@ deserialize(input: any): UiControl {
}
}
if (this.units === Units.s)
{
if (this.units === Units.s) {
let iValue = Math.round(value);
if (iValue >= 60)
{
let minutes = Math.floor(iValue/60);
if (iValue >= 60) {
let minutes = Math.floor(iValue / 60);
let seconds = iValue % 60;
if (seconds < 10)
{
if (seconds < 10) {
return minutes + ":0" + seconds;
}
@@ -756,11 +833,10 @@ deserialize(input: any): UiControl {
return this.formatShortValue(value);
}
}
if (this.units === Units.semitone12TET)
{
if (this.units === Units.semitone12TET) {
return semitone12TETValue(value);
}
let text = this.formatShortValue(value);
switch (this.units) {
@@ -831,11 +907,45 @@ deserialize(input: any): UiControl {
}
export class Lv2PatchPropertyInfo {
deserialize(input: any): Lv2PatchPropertyInfo {
this.uri = input.uri;
this.writable = input.writable;
this.readable = input.readable;
this.label = input.label;
this.index = input.index;
this.type = input.type;
this.comment = input.comment;
this.shortName = input.shortName;
this.fileTypes = input.fileTypes;
this.supportedExtensions = input.supportedExtensions;
return this;
}
static deserialize_array(input: any): Lv2PatchPropertyInfo[] {
let result: Lv2PatchPropertyInfo[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new Lv2PatchPropertyInfo().deserialize(input[i]);
}
return result;
}
uri: string = "";
writable : boolean = false;
readable: boolean = false;
label: string = "";
index: number = -1;
type: string = "";
comment: string = "";
shortName: string = "";
fileTypes: string[] = [];
supportedExtensions: string[] = [];
};
export class UiPlugin implements Deserializable<UiPlugin> {
deserialize(input: any): UiPlugin {
this.uri = input.uri;
this.name = input.name;
this.minorVersion = input.minorVersion ? input.minorVersion : 0;
this.microVersion = input.microVersion ? input.microVersion : 0;
this.brand = input.brand ? input.brand : "";
this.label = input.label ? input.label : this.name;
this.plugin_type = input.plugin_type as PluginType;
@@ -861,6 +971,11 @@ export class UiPlugin implements Deserializable<UiPlugin> {
}
this.is_vst3 = input.is_vst3;
this.modGui = null;
if (input.modGui) {
this.modGui = new ModGui().deserialize(input.modGui);
}
this.patchProperties = Lv2PatchPropertyInfo.deserialize_array(input.patchProperties ?? []);
return this;
}
@@ -908,6 +1023,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
uri: string = "";
name: string = "";
minorVersion: number = 0;
microVersion: number = 0;
brand: string = "";
label: string = "";
plugin_type: PluginType = PluginType.InvalidPlugin;
@@ -924,6 +1041,8 @@ export class UiPlugin implements Deserializable<UiPlugin> {
fileProperties: UiFileProperty[] = [];
frequencyPlots: UiFrequencyPlot[] = [];
is_vst3: boolean = false;
modGui: ModGui | null = null; // null if no mod gui.
patchProperties: Lv2PatchPropertyInfo[] = [];
}
+54 -9
View File
@@ -58,6 +58,8 @@ import Snapshot3Icon from "./svg/snapshot_3.svg?react";
import Snapshot4Icon from "./svg/snapshot_4.svg?react";
import Snapshot5Icon from "./svg/snapshot_5.svg?react";
import Snapshot6Icon from "./svg/snapshot_6.svg?react";
import ModUiIcon from './svg/mod_ui.svg?react';
import PipedalUiIcon from './svg/pp_ui.svg?react';
import SnapshotDialog from './SnapshotDialog';
import { css } from '@emotion/react';
@@ -128,6 +130,8 @@ interface MainState {
showMidiBindingsDialog: boolean;
screenHeight: number;
displayNameDialogOpen: boolean;
showModUi: boolean;
}
@@ -167,7 +171,8 @@ export const MainPage =
displayAuthor: this.getDisplayAuthor(),
horizontalScrollLayout: this.windowSize.height < HORIZONTAL_CONTROL_SCROLL_HEIGHT_BREAK,
showMidiBindingsDialog: false,
screenHeight: this.windowSize.height
screenHeight: this.windowSize.height,
showModUi: false
};
@@ -375,7 +380,7 @@ export const MainPage =
if (pedalboardItem === null) return "";
return pedalboardItem.uri;
}
titleBar(pedalboardItem: PedalboardItem | null): React.ReactNode {
titleBar(pedalboardItem: PedalboardItem | null,canShowModUi: boolean): React.ReactNode {
let title = "";
let author = "";
let infoPluginUri = "";
@@ -441,7 +446,7 @@ export const MainPage =
{canEditTitle ? (
<ButtonBase
style={{ borderRadius: "6px", width: "100%", paddingLeft: 8, paddingRight: 8, height: 48, textTransform: "none" }}
onClick={()=> {
onClick={() => {
this.handleEditPluginDisplayName();
}}
>
@@ -453,7 +458,7 @@ export const MainPage =
</span>
</ButtonBase>)
: (
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8,minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
<div style={{ flex: "0 1 auto", paddingLeft: 8, paddingRight: 8, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis" }}>
<span className={classes.title}>{title}</span>
{this.state.displayAuthor && (
<span className={classes.author}>{author}</span>
@@ -469,6 +474,32 @@ export const MainPage =
<PluginPresetSelector pluginUri={presetsUri} instanceId={pedalboardItem?.instanceId ?? 0}
/>
</div>
{canShowModUi && (
<div style={{ flex: "0 0 auot" }}>
<IconButtonEx tooltip={(
<div>
<Typography variant="body2" >
{this.state.showModUi ? "PiPedal UI" : "MOD UI"}</Typography>
<Divider />
<Typography variant="caption">Use MOD UI or PiPedal UI for plugin</Typography>
</div>
)}
onClick={() => {
this.setState({ showModUi: !this.state.showModUi });
}}
size="large">
{!this.state.showModUi ?
(
<ModUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
) : (
<PipedalUiIcon style={{ height: 24, width: 24, fill: this.props.theme.palette.text.primary, opacity: 0.6 }} />
)
}
</IconButtonEx>
</div>
)}
</div>
);
}
@@ -494,6 +525,15 @@ export const MainPage =
}
}
canShowModUi(pedalboardItem: PedalboardItem): boolean {
let pluginInfo = this.model.getUiPlugin(pedalboardItem.uri);
if (pluginInfo === null) {
return false;
}
return pluginInfo.modGui !== null;
}
render() {
const classes = withStyles.getClasses(this.props);
let pedalboard = this.model.pedalboard.get();
@@ -508,8 +548,9 @@ export const MainPage =
let instanceId = -1;
let missing = false;
let pluginUri = "#error";
let canShowModUi = false;
if (pedalboardItem) {
canShowModUi = this.canShowModUi(pedalboardItem);
canDelete = pedalboard.canDeleteItem(pedalboardItem.instanceId);
instanceId = pedalboardItem.instanceId;
if (pedalboardItem.isEmpty()) {
@@ -570,7 +611,7 @@ export const MainPage =
</div>
{
(!this.state.splitControlBar || !this.props.enableStructureEditing)
&& this.titleBar(pedalboardItem)
&& this.titleBar(pedalboardItem, canShowModUi)
}
<div style={{ flex: "1 1 1px" }}>
@@ -642,7 +683,7 @@ export const MainPage =
this.state.splitControlBar && this.props.enableStructureEditing && (
<div className={classes.splitControlBar}>
{
this.titleBar(pedalboardItem)
this.titleBar(pedalboardItem, canShowModUi)
}
</div>
)
@@ -657,7 +698,11 @@ export const MainPage =
) :
(
GetControlView(pedalboardItem)
GetControlView(pedalboardItem,this.state.showModUi && canShowModUi,
(instanceId: number, showModGui: boolean) => {
this.setState({ showModUi: showModGui });
}
)
)
}
</div>
@@ -689,7 +734,7 @@ export const MainPage =
this.setState({ displayNameDialogOpen: false });
}}
/>
)}
)}
</div>
);
}
+104
View File
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2025 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.
*/
import { Component, ErrorInfo, ReactNode } from "react";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import { UiPlugin } from "./Lv2Plugin";
interface Props {
plugin: UiPlugin | null;
children?: ReactNode;
onClose: () => void;
}
interface State {
hasError: boolean;
message: string;
}
class ModGuiErrorBoundary extends Component<Props, State> {
public state: State = {
hasError: false,
message: "",
};
public static getDerivedStateFromError(error: Error): State {
// Update state so the next render will show the fallback UI.
return { hasError: true, message: error.message };
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error("ModGUI rendering error: ", error);
console.error("Stack trace: " , errorInfo.componentStack);
}
public render() {
if (this.state.hasError) {
return (
<div style={{
position: "absolute",left: 0, right: 0, top: 0, bottom: 0,
display: "flex",
flexDirection: "column",
justifyContent: "stretch",
}}>
<div style={{ flex: "1 1 auto" }} />
<div style={{display: "flex", flexDirection: "row", alignItems: "stretch", justifyContent: "stretch"}}>
<div style={{ flex: "1 1 auto" }} />
<div style={{maxWidth: 600, margin: 24}}>
<Typography variant="h6" style={{ marginBottom: 8 }} >
{ this.props.plugin ?
`Error Loading '${this.props.plugin.name}'` :
`Error Loading UI`
}
</Typography>
<Typography variant="body2" style={{marginBottom:16}} >
{this.state.message}
</Typography>
<div style={{alignSelf: "flex-end", marginBottom: 16, marginRight: 24, display: "flex", flexFlow: "row nowrap", justifyContent: "flex-end" }}>
<Button
style={{alignSelf: "flex-end"}}
variant="contained"
onClick={() => {
this.props.onClose();
}}
>Close
</Button>
</div>
</div>
<div style={{ flex: "1 1 auto" }} />
</div>
<div style={{ flex: "2 2 auto" }} />
</div>
);
}
return this.props.children;
}
}
export default ModGuiErrorBoundary;
File diff suppressed because it is too large Load Diff
+346
View File
@@ -0,0 +1,346 @@
/*
* Copyright (c) 2025 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,
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from 'react';
import Divider from '@mui/material/Divider';
import { PiPedalModel, PiPedalModelFactory, MonitorPortHandle, State, ListenHandle, PatchPropertyListener } from './PiPedalModel';
import { useTheme } from '@mui/material/styles';
import CircularProgress from '@mui/material/CircularProgress';
import LoadPluginDialog from './LoadPluginDialog';
import ButtonEx from './ButtonEx';
import Typography from '@mui/material/Typography/Typography';
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
import { UiPlugin } from './Lv2Plugin';
import ModGuiErrorBoundary from './ModGuiErrorBoundary';
class MyPortHandle implements MonitorPortHandle {
private static nextHandle: number = 1;
constructor() {
this.handle = MyPortHandle.nextHandle++;
}
handle: number;
};
type MonitorCallback = (value: number) => void;
class ValueEntry {
constructor(defaultValue: number) {
this.value = defaultValue;
}
private listeners: { [handle: number]: MonitorCallback } = {};
monitor(interval: number, callback: MonitorCallback): MyPortHandle {
let handle = new MyPortHandle();
this.listeners[handle.handle] = callback;
return handle;
}
unmonitor(handle: MonitorPortHandle) {
let h = (handle as MyPortHandle).handle;
// remove entry h from listeners
if (!(h in this.listeners)) {
throw new Error("Invalid handle: " + h);
}
// remove the callback from the listeners
delete this.listeners[h];
}
setValue(value: number) {
if (value !== this.value) {
this.value = value;
// notify all listeners
for (let handle in this.listeners) {
this.listeners[handle](value);
}
}
}
value: number;
};
class PatchListenerEntry {
constructor(instanceId: number, propertyUri: string) {
this.instanceId = instanceId;
this.propertyUri = propertyUri;
}
instanceId: number;
propertyUri: string;
value: any = "";
listeners: { handle: number, listener: PatchPropertyListener}[] = [];;
};
class ValueHandler {
private valueDictionary: { [symbol: string]: ValueEntry } = {};
private handleToValueEnry: { [handle: number]: ValueEntry } = {};
constructor(instanceId: number, plugin: UiPlugin) {
for (let control of plugin.controls) {
this.valueDictionary[control.symbol] = new ValueEntry(control.default_value);
}
this.valueDictionary["_bypass"] = new ValueEntry(0);
}
monitorPort(instanceId: number, symbol: string, interval: number, callback: MonitorCallback): MonitorPortHandle {
let valueEntry = this.valueDictionary[symbol];
let portHandle = valueEntry.monitor(interval, callback);
this.handleToValueEnry[(portHandle as MyPortHandle).handle] = valueEntry;
callback(valueEntry.value);
return portHandle;
}
unmonitorPort(handle: MonitorPortHandle): void {
let h = (handle as MyPortHandle).handle;
if (!(h in this.handleToValueEnry)) {
throw new Error("Invalid handle: " + h);
}
let valueEntry = this.handleToValueEnry[h];
valueEntry.unmonitor(handle);
delete this.handleToValueEnry[h];
}
setValue(instanceId: number, symbol: string, value: number) {
// add a delay just for funsies.
window.setTimeout(() => {
if (!(symbol in this.valueDictionary)) {
throw new Error("Invalid symbol: " + symbol);
}
let valueEntry = this.valueDictionary[symbol];
valueEntry.setValue(value);
}, 100);
}
getValue(instanceId: number, symbol: string): number {
if (!(symbol in this.valueDictionary)) {
throw new Error("Invalid symbol: " + symbol);
}
let valueEntry = this.valueDictionary[symbol];
return valueEntry.value;
}
private nextListenHandle: number = 1;
private patchListeners: PatchListenerEntry[] = [];
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
let h = this.nextListenHandle++;
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === propertyUri) {
// already listening to this property
entry.listeners.push({ handle: h, listener: onReceived });
onReceived(instanceId,propertyUri,entry.value);
return { _handle: h };
}
}
let entry = new PatchListenerEntry(instanceId, propertyUri);
this.patchListeners.push(entry);
entry.listeners.push({ handle: h, listener: onReceived });
onReceived(instanceId, propertyUri, entry.value);
return {_handle: h};
}
cancelMonitorPatchProperty(listenHandle: ListenHandle): void {
for (let i = 0; i < this.patchListeners.length; ++i) {
let entry = this.patchListeners[i];
for (let j = 0; j < entry.listeners.length; ++j) {
if (entry.listeners[j].handle === listenHandle._handle) {
// found the listener, remove it
entry.listeners.splice(j, 1);
if (entry.listeners.length === 0) {
// no more listeners, remove the entry
this.patchListeners.splice(i, 1);
}
return;
}
}
}
}
getPatchProperty(instanceId: number, uri: string): Promise<any> {
let promise = new Promise<any>((resolve, reject) => {
window.setTimeout(() => {
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
window.setTimeout(() => {
// simulate a delay for getting the property
entry.listeners.forEach(l => l.listener(instanceId, uri, entry.value));
}, 100);
return resolve(entry.value);
}
}
reject(new Error(`Patch property not found: ${uri} for instance ${instanceId}`));
},50);
});
return promise;
}
setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean> {
// Implementation here
for (let entry of this.patchListeners) {
if (entry.instanceId === instanceId && entry.propertyUri === uri) {
entry.value = value;
// notify all listeners
entry.listeners.forEach(l => l.listener(instanceId, uri, value));
return Promise.resolve(true);
}
}
// if we reach here, the property was not found
return Promise.reject("Not found.");
}
};
function ModGuiTest() {
const [pluginUrl, setPluginUrl] = React.useState("");
const [uiPlugin, setUiPlugin] = React.useState<UiPlugin | null>(null);
const [pluginDialogOpen, setPluginDialogOpen] = React.useState(false);
const [loading, setLoading] = React.useState(true);
const [valueHandler, setValueHandler] = React.useState<ValueHandler | null>(null);
const [modGuiHostSite] = React.useState<IModGuiHostSite>({
monitorPort: (instanceId: number, symbol: string, interval: number, callback: (value: number) => void) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.monitorPort(instanceId, symbol, interval, callback);
},
unmonitorPort: (handle) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.unmonitorPort(handle);
},
setPedalboardControl: (instanceId, symbol, value) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.setValue(instanceId, symbol, value);
},
monitorPatchProperty(instanceId: number, propertyUri: string, onReceived: PatchPropertyListener): ListenHandle {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.monitorPatchProperty(instanceId, propertyUri, onReceived);
},
cancelMonitorPatchProperty: (listenHandle) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
valueHandler.cancelMonitorPatchProperty(listenHandle);
},
getPatchProperty: async (instanceId, uri) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.getPatchProperty(instanceId, uri) as Promise<any>;
},
setPatchProperty: async (instanceId, uri, value) => {
if (!valueHandler) {
throw new Error("ValueHandler is not set");
}
return valueHandler.setPatchProperty(instanceId, uri, value) as Promise<boolean>;
}
});
let model: PiPedalModel = PiPedalModelFactory.getInstance();
React.useEffect(() => {
const onStateChange = (state: State) => {
if (state == State.Ready) {
setLoading(false);
}
};
model.state.addOnChangedHandler(onStateChange);
return () => {
model.state.removeOnChangedHandler(onStateChange);
};
}, [])
const theme = useTheme();
return (
<div style={{
display: "flex", flexFlow: "column nowrap", alignItems: "stretch", justifyContent: "stretch",
position: "absolute", top: 0, left: 0, width: "100%", height: "100%", backgroundColor: (theme.palette as any).mainBackground
}}>
<div style={{
position: "absolute", top: 0, left: 0, right: 0,
bottom: 0, zIndex: 1000,
display: loading ? "flex" : "none",
alignItems: "center", justifyContent: "center",
background: "rgba(0.5,0.5,0.5, 0.9)"
}}>
<CircularProgress style={{ color: theme.palette.text.secondary }} />
</div>
<div style={{
flex: "0 0 auto", display: "flex", flexFlow: "row nowrap",
alignItems: "center", gap: 16, paddingLeft: 32, paddingRight: 32, paddingTop: 16, paddingBottom: 16,
}}>
<Typography noWrap variant="h6" style={{ flex: "1 0 auto" }}>ModGUI Test</Typography>
<Typography noWrap variant="body2" style={{ flex: "0 1 auto", }}>
{pluginUrl ? pluginUrl : "No plugin loaded"}
</Typography>
<ButtonEx style={{ flex: "0 0 auto" }} variant="contained" tooltip="Select plugin" onClick={() => setPluginDialogOpen(true)} >
Load
</ButtonEx>
</div>
<Divider />
{uiPlugin && (
<div style={{ flex: "1 1 auto", position: "relative" }}>
<ModGuiErrorBoundary plugin={uiPlugin} onClose={() => {
setPluginUrl("");
setUiPlugin(null);
setValueHandler(null);
}} >
<ModGuiHost instanceId={-1} plugin={uiPlugin}
onClose={() => {
setPluginUrl("");
setUiPlugin(null);
}}
hostSite={modGuiHostSite}
/>
</ModGuiErrorBoundary>
</div>
)}
{pluginDialogOpen &&
<LoadPluginDialog
open={pluginDialogOpen}
onOk={(url) => {
setPluginUrl(url as string);
let uiPlugin = model.getUiPlugin(url);
setUiPlugin(uiPlugin);
if (uiPlugin) {
setValueHandler(new ValueHandler(-1, uiPlugin));
}
setPluginDialogOpen(false);
}}
onCancel={() => setPluginDialogOpen(false)}
uri={pluginUrl}
modGuiOnly={true}
/>
}
</div>
);
}
export default ModGuiTest;
+105
View File
@@ -0,0 +1,105 @@
/*
* MIT License
*
* Copyright (c) 2022 Robin E. R. Davies
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import React from 'react';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButtonEx from './IconButtonEx';
import Button from '@mui/material/Button';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import DialogEx from './DialogEx';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import ReportGmailerrorredIcon from '@mui/icons-material/ReportGmailerrorred';
export interface OkDialogProps {
open: boolean,
title?: string,
text: string,
okButtonText?: string,
onClose: () => void
};
export interface OkDialogState {
};
export default class OkDialog extends React.Component<OkDialogProps, OkDialogState> {
constructor(props: OkDialogProps) {
super(props);
this.state = {
};
}
render() {
let props = this.props;
let { open, okButtonText, text, onClose } = props;
const handleClose = () => {
onClose();
};
const handleOk = () => {
onClose();
}
return (
<DialogEx tag="alert" open={open} onClose={handleClose}
onEnterKey={handleOk}
maxWidth="sm"
style={{ userSelect: "none" }}
>
{props.title && (
<DialogTitle id="alert-dialog-title" style={{ userSelect: "none", padding: 0, margin: 0 }}>
<Toolbar style={{paddingLeft: 24, paddingRight: 24,margin: 0}} >
<IconButtonEx
tooltip="Back" edge="start" color="inherit" onClick={()=>{this.props.onClose();}} aria-label="back"
>
<ArrowBackIcon />
</IconButtonEx>
<Typography noWrap variant="body1" style={{ flex: "1 1 auto" }}>{props.title}</Typography>
</Toolbar>
</DialogTitle>
)}
<DialogContent >
<div style={{ display: "flex", flexFlow: "row nowrap", gap: 16, marginLeft: 32, marginRight: 32, alignItems: "start" }}>
<ReportGmailerrorredIcon style={{ color: "var(--color-error)", fontSize: 48, flexShrink: 0, opacity: 0.8 }} />
<Typography variant="body2" style={{ marginTop:16}}
>{text}</Typography>
</div>
</DialogContent>
<DialogActions>
<Button variant="dialogPrimary" onClick={handleOk} style={{marginRight: 24}} >
{okButtonText ? okButtonText : "OK"}
</Button>
</DialogActions>
</DialogEx>
);
}
}
+8 -2
View File
@@ -416,6 +416,7 @@ export class PiPedalModel //implements PiPedalModel
socketServerUrl: string = "";
varServerUrl: string = "";
modResourcesUrl: string = "";
lv2Path: string = "";
webSocket?: PiPedalSocket;
@@ -972,13 +973,17 @@ export class PiPedalModel //implements PiPedalModel
// anything could have changed while we were disconnected.
await this.loadServerState();
}
makeSocketServerUrl(hostName: string, port: number): string {
private makeSocketServerUrl(hostName: string, port: number): string {
return "ws://" + hostName + ":" + port + "/pipedal";
}
makeVarServerUrl(protocol: string, hostName: string, port: number): string {
private makeVarServerUrl(protocol: string, hostName: string, port: number): string {
return protocol + "://" + hostName + ":" + port + "/var/";
}
private makeModResourceUrl(protocol: string, hostName: string, port: number): string {
return protocol + "://" + hostName + ":" + port + "/resources/";
}
async getNextAudioFile(filePath: string): Promise<string> {
@@ -1053,6 +1058,7 @@ export class PiPedalModel //implements PiPedalModel
if (!socket_server_port) socket_server_port = 8080;
let socket_server = this.makeSocketServerUrl(socket_server_address, socket_server_port);
let var_server_url = this.makeVarServerUrl("http", socket_server_address, socket_server_port);
this.modResourcesUrl = this.makeModResourceUrl("http", socket_server_address, socket_server_port);
this.socketServerUrl = socket_server;
this.varServerUrl = var_server_url;
+94 -29
View File
@@ -23,6 +23,8 @@ import WithStyles, { withTheme } from './WithStyles';
import { createStyles } from './WithStyles';
import { css } from '@emotion/react';
import ModGuiHost, { IModGuiHostSite } from './ModGuiHost';
import { withStyles } from "tss-react/mui";
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from './Lv2Plugin';
@@ -340,6 +342,8 @@ export interface PluginControlViewProps extends WithStyles<typeof styles> {
item: PedalboardItem;
customization?: ControlViewCustomization;
customizationId?: number;
showModGui: boolean;
onSetShowModGui: (instanceId: number, showModGui: boolean) => void;
}
type PluginControlViewState = {
landscapeGrid: boolean;
@@ -360,7 +364,6 @@ const PluginControlView =
constructor(props: PluginControlViewProps) {
super(props);
this.model = PiPedalModelFactory.getInstance();
this.state = {
landscapeGrid: false,
imeUiControl: undefined,
@@ -376,6 +379,19 @@ const PluginControlView =
this.onControlValueChanged = this.onControlValueChanged.bind(this);
this.onPreviewChange = this.onPreviewChange.bind(this);
}
// unmonitorPort: (handle: MonitorPortHandle) => void;
// onValueChanged: (instanceId: number, symbol: string, value: number) => void;
// monitorPatchProperty(
// instanceId: number,
// propertyUri: string,
// onReceived: PatchPropertyListener
// ): ListenHandle;
// cancelMonitorPatchProperty(listenHandle: ListenHandle): void;
// getPatchProperty(instanceId: number, uri: string): Promise<any>;
// setPatchProperty(instanceId: number, uri: string, value: any): Promise<boolean>
onPreviewChange(key: string, value: number): void {
@@ -758,19 +774,45 @@ const PluginControlView =
}
return false;
}
makeHostSite(): IModGuiHostSite {
// Yuck. :-( This worked out badly.
let model = this.model;
return {
monitorPort: this.model.monitorPort.bind(model),
unmonitorPort: this.model.unmonitorPort.bind(model),
setPedalboardControl: this.model.setPedalboardControl.bind(model),
monitorPatchProperty: this.model.monitorPatchProperty.bind(model),
cancelMonitorPatchProperty: this.model.cancelMonitorPatchProperty.bind(model),
getPatchProperty: this.model.getPatchProperty.bind(model),
setPatchProperty: this.model.setPatchProperty.bind(model),
};
}
render(): ReactNode {
this.controlKeyIndex = 0;
const classes = withStyles.getClasses(this.props);
let pedalboardItem: PedalboardItem;
let pedalboard = this.model.pedalboard.get();
if (this.props.instanceId === Pedalboard.START_CONTROL) {
pedalboardItem = pedalboard.makeStartItem();
} else if (this.props.instanceId === Pedalboard.END_CONTROL) {
pedalboardItem = pedalboard.makeEndItem();
} else {
pedalboardItem = pedalboard.getItem(this.props.instanceId);
renderModGui(): ReactNode {
let uiPlugin = this.model.getUiPlugin(this.props.item.uri);
if (!uiPlugin) {
return (<div />);
}
return (
<ModGuiHost
instanceId={this.props.instanceId}
plugin={uiPlugin}
onClose={() => {
if (this.props.onSetShowModGui) {
this.props.onSetShowModGui(this.props.instanceId, false);
}
}}
hostSite={this.makeHostSite()}
/>
)
}
renderPiPedalControl(pedalboardItem?: PedalboardItem): ReactNode {
this.controlKeyIndex = 0;
const classes = withStyles.getClasses(this.props);
let pedalboard = this.model.pedalboard.get();
if (!pedalboardItem)
return (<div className={classes.frame} ></div>);
@@ -792,19 +834,13 @@ const PluginControlView =
controlValues = this.filterNotOnGui(controlValues, plugin);
let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid;
let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait;
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
let controlNodes: ControlNodes;
let frameClass = classes.frame;
if (this.fullScreen()) {
gridClass = classes.noScrollGrid;
scrollClass = classes.frameScrollNone;
frameClass = classes.noScrollFrame;
}
let controlNodes: ControlNodes;
controlNodes = this.getStandardControlNodes(plugin, controlValues);
@@ -822,16 +858,7 @@ const PluginControlView =
if (pedalboardItem.midiChannelBinding) {
nodes.push(this.midiBindingControl(pedalboardItem));
}
return (
<div className={frameClass}>
<div className={classes.vuMeterL}>
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
</div>
<div className={vuMeterRClass}>
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
</div>
<div className={scrollClass}>
<div className={gridClass} >
{
@@ -848,6 +875,44 @@ const PluginControlView =
}
</div>
</div>
);
}
render(): ReactNode {
const classes = withStyles.getClasses(this.props);
let pedalboard = this.model.pedalboard.get();
let pedalboardItem: PedalboardItem;
if (this.props.instanceId === Pedalboard.START_CONTROL) {
pedalboardItem = pedalboard.makeStartItem();
} else if (this.props.instanceId === Pedalboard.END_CONTROL) {
pedalboardItem = pedalboard.makeEndItem();
} else {
pedalboardItem = pedalboard.getItem(this.props.instanceId);
}
let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR;
let frameClass = classes.frame;
if (this.fullScreen()) {
frameClass = classes.noScrollFrame;
}
return (
<div className={frameClass}>
<div className={classes.vuMeterL}>
<VuMeter displayText={true} display="input" instanceId={pedalboardItem.instanceId} />
</div>
<div className={vuMeterRClass}>
<VuMeter displayText={true} display="output" instanceId={pedalboardItem.instanceId} />
</div>
{
this.props.showModGui && this.renderModGui()
|| this.renderPiPedalControl(pedalboardItem)
}
{this.state.showFileDialog && (
<FilePropertyDialog open={this.state.showFileDialog}
@@ -889,7 +954,7 @@ const PluginControlView =
}
/>
)}
{/* xxx: I don't think we need this anyore. */}
<FullScreenIME uiControl={this.state.imeUiControl} value={this.state.imeValue}
onChange={(key, value) => this.onImeValueChange(key, value)}
+91
View File
@@ -0,0 +1,91 @@
// 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.
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles';
import { withStyles } from "tss-react/mui";
import {createStyles} from './WithStyles';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
const styles = (theme: Theme) => createStyles({
pgraph: {
paddingBottom: 16
}
});
export interface ScratchClassProps extends WithStyles<typeof styles> {
}
export interface ScratchClassState {
screenWidth: number,
screenHeight: number
}
const ScratchClass = withStyles(
class extends ResizeResponsiveComponent<ScratchClassProps, ScratchClassState> {
private model: PiPedalModel;
constructor(props: ScratchClassProps) {
super(props);
this.model = PiPedalModelFactory.getInstance();
void this.model; // suppress unused variable warning
this.state = {
screenWidth: this.windowSize.width,
screenHeight: this.windowSize.height
};
}
mounted: boolean = false;
onWindowSizeChanged(width: number, height: number): void {
this.setState({ screenWidth: width, screenHeight: height });
}
componentDidMount() {
super.componentDidMount();
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
super.componentWillUnmount();
}
componentDidUpdate(prevProps: ScratchClassProps) {
}
render() {
const classes = withStyles.getClasses(this.props);
void classes; // suppress unused variable warning
return (<div/>);
}
},
styles);
export default ScratchClass;
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 6.3499999 6.35"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
id="layer1">
<path
style="fill-rule:evenodd;-inkscape-stroke:none"
d="m 2.2089844,0.26367188 c -0.564979,0 -1.0234375,0.46978477 -1.0234375,1.03710932 v 3.7578125 c 0,0.5673246 0.4584585,1.0371094 1.0234375,1.0371094 H 4.140625 c 0.564979,0 1.0234375,-0.4697848 1.0234375,-1.0371094 V 1.3007812 c 0,-0.56732455 -0.4584585,-1.03710932 -1.0234375,-1.03710932 z m 0,0.52929687 H 4.140625 c 0.276179,0 0.4941406,0.21977775 0.4941406,0.50781245 v 3.7578125 c 0,0.2880348 -0.2179616,0.5078125 -0.4941406,0.5078125 H 2.2089844 c -0.276179,0 -0.4941406,-0.2197777 -0.4941407,-0.5078125 V 1.3007812 c 0,-0.2880347 0.2179617,-0.50781245 0.4941407,-0.50781245 z"
id="rect234" />
<path
style="-inkscape-stroke:none"
d="m 3.1757813,3.0429688 c -0.4352418,0 -0.7949219,0.357727 -0.7949219,0.7929687 0,0.4352417 0.3596801,0.7949219 0.7949219,0.7949219 0.4352417,0 0.7929687,-0.3596802 0.7929687,-0.7949219 0,-0.4352417 -0.357727,-0.7929687 -0.7929687,-0.7929687 z m 0,0.5292968 c 0.1492592,0 0.2636718,0.1144129 0.2636718,0.2636719 0,0.149259 -0.1144126,0.265625 -0.2636718,0.265625 -0.1492593,0 -0.2656251,-0.116366 -0.265625,-0.265625 0,-0.149259 0.1163657,-0.2636719 0.265625,-0.2636719 z"
id="path2914" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 6.3499999 6.35"
version="1.1"
id="svg5"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<g
id="layer1">
<path
id="path3692"
d="M 1.7218342,0.53148711 C 1.03731,0.54023357 0.48004958,1.102787 0.48004958,1.7892914 c 0,0.69191 0.56589442,1.2578044 1.25780432,1.2578044 0.69191,0 1.2598715,-0.5658944 1.2598715,-1.2578044 0,-0.5024751 -0.2996529,-0.9383787 -0.7286377,-1.13946526 L 2.1460978,1.1831269 c 0.1950392,0.1300488 0.3224609,0.3518001 0.3224609,0.6061645 0,0.4059278 -0.324777,0.7286377 -0.7307048,0.7286377 -0.4059278,0 -0.7286376,-0.3227099 -0.7286376,-0.7286377 0,-0.3691252 0.2670204,-0.669466 0.6206339,-0.7208862 L 1.4810221,1.7143606 1.7388875,1.7737885 1.8990844,1.0782237 2.0174234,0.56300973 c -0.0053,-0.001214 -0.010694,-0.002471 -0.01602,-0.003617 -0.00604,-0.001301 -0.012021,-0.002404 -0.018087,-0.003617 -0.00465,-9.2974e-4 -0.00928,-0.001706 -0.013953,-0.002584 -0.0066,-0.001242 -0.013523,-0.002479 -0.020154,-0.003617 -0.00411,-7.0267e-4 -0.00828,-0.001405 -0.012402,-0.002067 -0.00736,-0.001187 -0.01482,-0.00256 -0.022221,-0.003617 -8.711e-4,-1.2398e-4 -0.00171,-3.9459e-4 -0.00258,-5.1676e-4 -0.010646,-0.001498 -0.021313,-0.002391 -0.032039,-0.003617 -0.00292,-3.3196e-4 -0.00586,-7.218e-4 -0.00879,-0.001034 -0.00879,-9.4149e-4 -0.017517,-0.001827 -0.026355,-0.002584 -0.00221,-1.8873e-4 -0.0045,-3.3967e-4 -0.00672,-5.1677e-4 -0.00954,-7.6485e-4 -0.019336,-0.00152 -0.028939,-0.002067 -0.00717,-4.0808e-4 -0.014505,-7.4831e-4 -0.021704,-0.001034 -0.00474,-1.8774e-4 -0.0092,-3.827e-4 -0.013953,-5.1677e-4 -0.00657,-1.8642e-4 -0.013046,-4.3271e-4 -0.019637,-5.1676e-4 -0.00537,-6.855e-5 -0.010629,0 -0.01602,0 -0.00541,0 -0.01063,-6.887e-5 -0.01602,0 z" />
<path
id="path3861"
d="m 4.23951,2.741472 c -0.6845242,0.00875 -1.2417846,0.5712999 -1.2417846,1.2578043 0,0.69191 0.5658944,1.2578044 1.2578043,1.2578044 0.69191,0 1.2598715,-0.5658944 1.2598715,-1.2578044 0,-0.5024751 -0.2996529,-0.9383787 -0.7286377,-1.1394653 L 4.6637736,3.3931118 c 0.1950392,0.1300488 0.3224609,0.3518001 0.3224609,0.6061645 0,0.4059278 -0.324777,0.7286377 -0.7307048,0.7286377 -0.4059278,0 -0.7286377,-0.3227099 -0.7286377,-0.7286377 0,-0.3691252 0.2670204,-0.669466 0.620634,-0.7208862 l -0.1488281,0.6459554 0.2578654,0.059428 0.1601969,-0.6955648 0.118339,-0.515214 c -0.0053,-0.00121 -0.010694,-0.00247 -0.01602,-0.00362 -0.00604,-0.0013 -0.012021,-0.0024 -0.018087,-0.00362 -0.00465,-9.297e-4 -0.00928,-0.00171 -0.013953,-0.00258 -0.0066,-0.00124 -0.013523,-0.00248 -0.020154,-0.00362 -0.00411,-7.027e-4 -0.00828,-0.0014 -0.012402,-0.00207 -0.00736,-0.00119 -0.01482,-0.00256 -0.022221,-0.00362 -8.711e-4,-1.24e-4 -0.00171,-3.946e-4 -0.00258,-5.168e-4 -0.010646,-0.0015 -0.021313,-0.00239 -0.032039,-0.00362 -0.00292,-3.319e-4 -0.00586,-7.218e-4 -0.00878,-0.00103 -0.00879,-9.415e-4 -0.017517,-0.00183 -0.026355,-0.00258 -0.00221,-1.887e-4 -0.0045,-3.397e-4 -0.00672,-5.168e-4 -0.00954,-7.648e-4 -0.019336,-0.00152 -0.028939,-0.00207 -0.00717,-4.081e-4 -0.014505,-7.483e-4 -0.021704,-0.00103 -0.00474,-1.877e-4 -0.0092,-3.827e-4 -0.013953,-5.167e-4 -0.00657,-1.864e-4 -0.013046,-4.327e-4 -0.019637,-5.168e-4 -0.00537,-6.85e-5 -0.010629,0 -0.01602,0 -0.00541,0 -0.01063,-6.89e-5 -0.01602,0 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+8
View File
@@ -8,4 +8,12 @@ export default defineConfig({
chunkSizeWarningLimit: 2000
},
plugins: [react(),svgr()],
server: {
proxy: {
'/resources': {
target: 'http://localhost:8080',
changeOrigin: false,
},
}
}
})