Routing, in progress.

This commit is contained in:
Robin E. R. Davies
2026-01-24 19:52:44 -05:00
parent 6a292cdc44
commit 014c23f270
8 changed files with 1028 additions and 36 deletions
+55
View File
@@ -0,0 +1,55 @@
import { ControlValue } from "../pipedal/Pedalboard.tsx";
export class ChannelMixerStrip {
inputVu: number = -100.0;
inputVolume: number = 0;
outputVu: number = -100.0;
outputVolume: number = 0;
};
export default class ChannelMixerSettings {
configured: boolean = false;
guitarInputChannels: number[] = [1,1];
guitarOutputChannels: number[] = [0,1];
guitarMixerSettings: ChannelMixerStrip = new ChannelMixerStrip();
auxInputChannels: number[] = [0,0];
auxOutputChannels: number[] = [-1,-1];
auxMixerSettings: ChannelMixerStrip = new ChannelMixerStrip();
controls: ControlValue[] = [];
// Inserts...
getControlValue(symbol: string): number | null {
for (let control of this.controls) {
if (control.key === symbol) {
return control.value;
}
}
return null;
}
setControlValue(symbol: string, value: number): boolean {
for (let control of this.controls) {
if (control.key === symbol) {
if (control.value === value) {
return false;
}
control.value = value;
this.controls = this.controls.slice(); // trigger observers.
return true;
}
}
let newValue = new ControlValue(symbol,value);
this.controls.push(newValue);
this.controls = this.controls.slice(); // trigger observers.
return true;
}
}
@@ -0,0 +1,524 @@
// Copyright (c) 2026 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 React from 'react';
import { useState } from 'react';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import VuMeter from './VuMeter';
import Toolbar from '@mui/material/Toolbar';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButtonEx from './IconButtonEx';
import Button from '@mui/material/Button';
import useWindowSize from "./UseWindowSize";
import DialIcon from './svg/fx_dial.svg?react';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import ChannelMixerSettingsHelpDialog from './ChannelMixerSettingsHelpDialog';
import PluginControl from './PluginControl';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import Typography from '@mui/material/Typography';
import ChannelMixerSettings from './ChannelMixerSettings';
import { makeChanelMixerUiPlugin, CHANNEL_MIXER_INSTANCE_ID } from './ChannelMixerUiControl';
import { ControlValue } from './Pedalboard';
import DialogEx from './DialogEx';
import { PiPedalModelFactory } from './PiPedalModel';
export interface ChannelMixerSettingsDialogProps {
open: boolean;
onClose: () => void;
}
let channelMixerUiPlugin = makeChanelMixerUiPlugin();
enum RouteType {
Main,
Aux
}
let cellSectionHead: React.CSSProperties = {
border: "0px",
paddingTop: 4,
paddingBottom: 4,
paddingLeft: 0,
paddingRight: 12,
margin: "0px",
textAlign: "left",
verticalAlign: "middle"
};
let cellPortraitControlStrip: React.CSSProperties = {
border: "0px",
paddingTop: 12,
paddingBottom: 12,
paddingLeft: 0,
paddingRight: 0,
margin: "0px",
textAlign: "left",
verticalAlign: "middle"
};
let cellLeft: React.CSSProperties = {
border: "0px",
paddingTop: 4,
paddingBottom: 4,
paddingLeft: 4,
paddingRight: 4,
margin: "0px",
textAlign: "left",
verticalAlign: "middle"
};
let cellLeftIndent: React.CSSProperties = {
border: "0px",
paddingTop: 4,
paddingBottom: 4,
paddingLeft: 12,
paddingRight: 4,
margin: "0px",
textAlign: "right",
verticalAlign: "middle"
};
let cellLeftH: React.CSSProperties = {
border: "0px",
paddingTop: 4,
paddingBottom: 4,
paddingLeft: 4,
paddingRight: 4,
margin: "0px",
textAlign: "left",
verticalAlign: "middle"
};
function MakeChannelMenu(channelCount: number): React.ReactElement[] {
let items: React.ReactElement[] = [(<MenuItem key={-1} value={-1}>None</MenuItem>)]
if (channelCount === 0) {
return items;
}
if (channelCount === 2) {
items.push((<MenuItem key={0} value={0}>Left</MenuItem>));
items.push((<MenuItem key={1} value={1}>Right</MenuItem>));
return items;
}
for (let i = 0; i < channelCount; ++i) {
items.push((<MenuItem key={i} value={i}>Ch {i + 1}</MenuItem>));
}
return items;
}
function ChannelMixerSettingsDialog(props: ChannelMixerSettingsDialogProps) {
//const classes = useStyles();
const { open, onClose } = props;
let model = PiPedalModelFactory.getInstance();
let config = model.jackConfiguration.get();
const [settings, setSettings] = useState<ChannelMixerSettings>(model.channelMixerSettings.get());
const [controlValues,setControlValues] = useState<ControlValue[]>(model.channelMixerControlValues.get());
const [showHelp, setShowHelp] = useState<boolean>(false);
if (settings) {
}
if (controlValues) {
}
React.useEffect(() => {
if (open) {
let handleSettingsChanged = () => {
setSettings(model.channelMixerSettings.get());
};
model.channelMixerSettings.addOnChangedHandler(handleSettingsChanged);
let handleControlsChanged = () => {
setControlValues(model.channelMixerControlValues.get());
};
model.channelMixerControlValues.addOnChangedHandler(handleControlsChanged);
return () => {
model.channelMixerSettings.removeOnChangedHandler(handleSettingsChanged);
model.channelMixerControlValues.removeOnChangedHandler(handleControlsChanged);
}
} else {
return () => { };
}
}, [open]);
const handleClose = (): void => {
onClose();
};
let [windowSize] = useWindowSize();
let landscape = windowSize.width > windowSize.height;
let fullScreen = windowSize.width < 450 || windowSize.height < 600;
let Dial = (symbol: string) => {
let instanceId = CHANNEL_MIXER_INSTANCE_ID;
let control = channelMixerUiPlugin.getControl(symbol);
let value = settings.getControlValue(symbol);
if (value === null) {
value = control?.default_value ?? 0;
}
return (<PluginControl
instanceId={instanceId}
uiControl={control}
onPreviewChange={(value: number) => {
model.previewPedalboardValue(instanceId, symbol, value);
}}
onChange={(value: number) => {
model.setPedalboardControl(instanceId, symbol, value);
}}
requestIMEEdit={() => { }}
value={value}
/>);
return <DialIcon width={32} height={32} style={{ verticalAlign: "middle", fill: "white" }} />;
}
let ChannelSelect = (routeType: RouteType, channelIndex: number, input: boolean) => {
let channelCount = input ? config.inputAudioPorts.length : config.outputAudioPorts.length;
let value: number;
switch (routeType) {
case RouteType.Main:
value = input ? settings.guitarInputChannels[channelIndex] : settings.guitarOutputChannels[channelIndex];
break;
case RouteType.Aux:
value = input ? settings.auxInputChannels[channelIndex] : settings.auxOutputChannels[channelIndex];
break;
}
if (value >= channelCount) {
value = -1;
}
return (
<Select variant="standard" style={{ width: 80 }} value={value}
onChange={(event) => {
let newValue = event.target.value as number;
let newSettings = Object.assign(new ChannelMixerSettings(), settings);
switch (routeType) {
case RouteType.Main:
if (input) {
newSettings.guitarInputChannels[channelIndex] = newValue;
} else {
newSettings.guitarOutputChannels[channelIndex] = newValue;
}
break;
case RouteType.Aux:
if (input) {
newSettings.auxInputChannels[channelIndex] = newValue;
} else {
newSettings.auxOutputChannels[channelIndex] = newValue;
}
break;
}
model.channelMixerSettings.set(newSettings);
}}
>
{MakeChannelMenu(channelCount)}
</Select>
)
}
let Vu = () => {
return (
<div style={{ }} >
<VuMeter instanceId={CHANNEL_MIXER_INSTANCE_ID} display={"input"} />
</div>
);
}
let LandscapeView = () => {
return (
<div style={{ width: "100%", height: "100%", flexGrow: 1, fontSize: "14px" }} >
<table style={{ borderCollapse: "collapse", width: "100%" }}>
<colgroup>
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
</colgroup>
<thead>
<tr>
<th></th>
<th style={cellLeftH}><Typography variant="caption" >In</Typography></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th style={cellLeftH}><Typography variant="caption" >Out</Typography></th>
</tr>
</thead>
<tbody>
{/* Main */}
<tr>
<td rowSpan={1} style={cellLeft}>
<Typography variant="body2" color="textSecondary">Main</Typography>
</td>
<td style={cellLeft}>
{ChannelSelect(RouteType.Main, 0, true)}
</td>
<td rowSpan={2} style={cellLeft}>{Dial("mainIn")}</td>
<td rowSpan={2} style={cellLeft}>{Vu()}</td>
<td rowSpan={2} style={cellLeft}>
<Button variant="outlined" style={{ textTransform: "none", borderRadius: 24 }}>
Inserts
</Button>
</td>
<td rowSpan={2} style={cellLeft}>{Dial("mainOut")}</td>
<td rowSpan={2} style={cellLeft}>{Vu()}</td>
<td style={cellLeft}>
{ChannelSelect(RouteType.Main, 0, false)}
</td>
</tr>
<tr>
<td></td>
<td style={cellLeft}>
{(ChannelSelect(RouteType.Main, 1, true))}
</td>
<td style={cellLeft}>
{(ChannelSelect(RouteType.Main, 1, false))}
</td>
</tr>
{/* Spacer */}
<tr><td colSpan={9} style={{ height: 16 }}></td></tr>
{/* Aux */}
<tr>
<td rowSpan={1} style={cellLeft}>
<Typography variant="body2" color="textSecondary">Aux</Typography>
</td>
<td style={cellLeft}>
{(ChannelSelect(RouteType.Aux, 0, true))}
</td>
<td rowSpan={2} style={cellLeft}>{Dial("auxIn")}</td>
<td rowSpan={2} style={cellLeft}>{Vu()}</td>
<td rowSpan={2} style={cellLeft}>
<Button variant="outlined" style={{ textTransform: "none", borderRadius: 24 }}>
Inserts
</Button>
</td>
<td rowSpan={2} style={cellLeft}>{Dial("auxOut")}</td>
<td rowSpan={2} style={cellLeft}>{Vu()}</td>
<td style={cellLeft}>
{(ChannelSelect(RouteType.Aux, 0, false))}
</td>
</tr>
<tr>
<td></td>
<td style={cellLeft}>
{(ChannelSelect(RouteType.Aux, 1, true))}
</td>
<td style={cellLeft}>
{(ChannelSelect(RouteType.Aux, 1, false))}
</td>
</tr>
</tbody>
</table>
</div>
);
};
let PortraitView = () => {
return (
<div style={{ width: "100%", height: "100%", flexGrow: 1, fontSize: "14px" }} >
<table style={{ borderCollapse: "collapse", width: "100%" }}>
<colgroup>
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "auto" }} />
<col style={{ width: "100%" }} />
</colgroup>
<tbody>
{/* Main */}
<tr>
<td colSpan={4} style={cellSectionHead}>
<Typography variant="body2" color="textSecondary" >Main</Typography>
</td>
</tr>
<tr>
<td style={cellLeftIndent}>
<Typography variant="body2" color="textSecondary" >In</Typography>
</td>
<td style={cellLeft}>
{ChannelSelect(RouteType.Main, 0, true)}
</td>
<td style={cellLeft}>
{ChannelSelect(RouteType.Main, 1, true)}
</td>
<td></td>
</tr>
<tr>
<td></td>
<td colSpan={3}>
<table>
<tbody>
<tr>
<td style={cellPortraitControlStrip}>{Dial("mainIn")}</td>
<td style={cellPortraitControlStrip}>{Vu()}</td>
<td style={cellPortraitControlStrip}>
<div style={{ marginLeft: 8, marginRight: 8 }}>
<Button variant="outlined" style={{ textTransform: "none", borderRadius: 24 }}>
Inserts
</Button>
</div>
</td>
<td style={cellPortraitControlStrip}>{Dial("mainOut")}</td>
<td style={cellPortraitControlStrip}>{Vu()}</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td style={cellLeftIndent}>
<Typography variant="body2" color="textSecondary" >Out</Typography>
</td>
<td style={cellLeft}>
{ChannelSelect(RouteType.Main, 0, false)}
</td>
<td style={cellLeft}>
{ChannelSelect(RouteType.Main, 1, false)}
</td>
<td></td>
</tr>
{/* Spacer */}
<tr><td colSpan={4} style={{ height: 16 }}></td></tr>
{/* Aux */}
<tr>
<td colSpan={4} style={cellSectionHead}>
<Typography variant="body2" color="textSecondary" >Aux</Typography>
</td>
</tr>
<tr>
<td style={cellLeftIndent}>
<Typography variant="body2" color="textSecondary" >In</Typography>
</td>
<td style={cellLeft}>
{(ChannelSelect(RouteType.Aux, 0, true))}
</td>
<td style={cellLeft}>
{(ChannelSelect(RouteType.Aux, 1, true))}
</td>
<td></td>
</tr>
<tr>
<td></td>
<td colSpan={3}>
<table>
<tbody>
<tr>
<td style={cellPortraitControlStrip}>{Dial("auxIn")}</td>
<td style={cellPortraitControlStrip}>{Vu()}</td>
<td style={cellPortraitControlStrip}>
<Button variant="outlined" style={{ textTransform: "none", borderRadius: 24 }}>
Inserts
</Button>
</td>
<td style={cellPortraitControlStrip}>{Dial("auxOut")}</td>
<td style={cellPortraitControlStrip}>{Vu()}</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td style={cellLeftIndent}>
<Typography variant="body2" color="textSecondary" >Out</Typography>
</td>
<td style={cellLeft}>
{ChannelSelect(RouteType.Aux, 0, false)}
</td>
<td style={cellLeft}>
{ChannelSelect(RouteType.Aux, 1, false)}
</td>
<td></td>
</tr>
</tbody>
</table>
</div>
);
};
return (
<DialogEx tag="channelMixerSettings" onClose={handleClose} aria-labelledby="select-channel_mixer_settings"
open={open}
fullWidth maxWidth="sm"
onEnterKey={handleClose}
fullScreen={fullScreen}
style={{ maxWidth: 1200 }}
>
<DialogTitle id="select-channel_mixer_settings" style={{ paddingTop: 0, paddingBottom: 0, marginBottom: 8 }}>
<Toolbar style={{ padding: 0, margin: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
Channel Routing
</Typography>
<IconButtonEx tooltip="Help"
edge="end"
aria-label="help"
onClick={() => { setShowHelp(true); }}
>
<HelpOutlineIcon style={{ width: 24, height: 24, opacity: 0.6 }} />
</IconButtonEx>
</Toolbar>
</DialogTitle>
<DialogContent>
{landscape ?
LandscapeView()
:
PortraitView()
}
</DialogContent>
{showHelp && (
<ChannelMixerSettingsHelpDialog open={showHelp} onClose={() => setShowHelp(false)} />
)}
</DialogEx>
);
}
export default ChannelMixerSettingsDialog;
@@ -0,0 +1,146 @@
// 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 React from 'react';
import Button from '@mui/material/Button';
import DialogEx from './DialogEx';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import Typography from '@mui/material/Typography';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import Divider from '@mui/material/Divider';
import DialogTitle from '@mui/material/DialogTitle';
import Toolbar from '@mui/material/Toolbar';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import IconButtonEx from './IconButtonEx';
export interface ChannelMixerSettingsHelpDialogProps {
open: boolean,
onClose: () => void,
};
export interface ChannelMixerSettingsHelpDialogState {
fullScreen: boolean;
};
export default class ChannelMixerSettingsHelpDialog extends ResizeResponsiveComponent<ChannelMixerSettingsHelpDialogProps, ChannelMixerSettingsHelpDialogState> {
refText: React.RefObject<HTMLInputElement | null>;
constructor(props: ChannelMixerSettingsHelpDialogProps) {
super(props);
this.state = {
fullScreen: false
};
this.refText = React.createRef<HTMLInputElement>();
}
mounted: boolean = false;
onWindowSizeChanged(width: number, height: number): void {
this.setState({ fullScreen: height < 200 })
}
componentDidMount() {
super.componentDidMount();
this.mounted = true;
}
componentWillUnmount() {
super.componentWillUnmount();
this.mounted = false;
}
componentDidUpdate() {
}
render() {
let props = this.props;
let { open, onClose } = props;
const handleClose = () => {
onClose();
};
return (
<DialogEx tag="bindingHelp" open={open} fullWidth maxWidth="sm"
onClose={handleClose}
aria-labelledby="mixer-settings-dialog-title"
style={{ userSelect: "none" }}
fullwidth
onEnterKey={() => { }}
>
<DialogTitle id="select-channel_mixer_settings" style={{ paddingTop: 0, paddingBottom: 0, marginBottom: 8 }}>
<Toolbar style={{ padding: 0, margin: 0 }}>
<IconButtonEx
tooltip="Close"
edge="start"
color="inherit"
aria-label="cancel"
style={{ opacity: 0.6 }}
onClick={() => { onClose(); }}
>
<ArrowBackIcon style={{ width: 24, height: 24 }} />
</IconButtonEx>
<Typography noWrap component="div" sx={{ flexGrow: 1 }}>
Channel Routing Dialog Help
</Typography>
</Toolbar>
</DialogTitle>
<DialogContent style={{ minHeight: 96 }}>
<div style={{ fontSize: "0.9em", lineHeight: "18pt" }}>
<p> The Mixer Routing Dialog determines how audio signals are processed and routed between input and output audio channels.
</p>
<p>Guitar effects processing occurs on the <i>Main</i> route only. Plugins in the main PiPedal window are
applied before any insert plugins that have been added to the <i>Main</i> route. Main insert plugins are
applied globally, regardless of the selected preset or effects in the main PiPedal window. You might
want to add an EQ or reverb plugin here, to allow your presets to be globally adjusted to suit the room
in which you are currently performing.
</p>
<p>Plugins in the main PiPedal window are not applied to the <i>Aux</i> route. The Aux route is intended to be
used for
</p>
<ul>
<li>Passing through a dry (unprocessed) guitar signal that can be recorded by a DAW and
re-amped later during mixing.
</li>
<li>Passing through a vocal mic signal from one of the input channels, perhaps with compression, EQ or
other processing performed by plugins in Aux inserts.
</li>
<li>
Passing through a backing track or other external audio signal from one of the input channels, and then either
mixing it with the processed guitar signal, or passing it out on a dedicated output channel.
</li>
</ul>
<p>although you may find other creative uses for the Aux channel as well.</p>
<p>If the Main and Aux routes share output channels, then the results of the Main and Output signals are
summed together on those output channels. For stereo mixing, assign the same left and right output
channels to both the Main and Aux routes. If you have a 2x4 or 4x4 audio interface with two guitar input channels
you can also pass through stereo dry signals by configuring Main and Aux input and output channels appropriately.
</p>
</div>
</DialogContent>
</DialogEx>
);
}
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright (c) 2026 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 Units from './Units';
import {UiPlugin, UiControl, PluginType, ControlType, ScalePoint} from './Lv2Plugin';
export const ChannelMixerUiUri = "uri://two-play/pipedal/ppChannelMixer";
export const CHANNEL_MIXER_INSTANCE_ID = -4;
export function makeChanelMixerUiPlugin(): UiPlugin {
return new UiPlugin().deserialize({
uri: ChannelMixerUiUri,
name: "Channel Mixer",
brand: "",
label: "",
plugin_type: PluginType.MixerPlugin,
plugin_display_type: "Mixer",
author_name: "",
author_homepage: "",
audio_inputs: 1,
audio_side_chain_inputs: 0,
audio_outputs: 1,
has_midi_input: false,
has_midi_output: false,
description: "",
controls: [
new UiControl().applyProperties({
symbol: "mainIn",
name: "Main In",
index: 0,
is_input: true,
min_value: -60.0,
max_value: 20.0,
default_value: 0.0,
is_bypass: false,
units: Units.db,
controlType: ControlType.Dial,
is_program_controller: false,
custom_units: "",
connection_optional: false,
scale_points: [
new ScalePoint().deserialize({ value: -60, label: "-INF" }),
],
}),
new UiControl().applyProperties({
symbol: "mainOut",
name: "Main Out",
index: 1,
is_input: true,
min_value: -60.0,
max_value: 20.0,
default_value: 0.0,
is_bypass: false,
units: Units.db,
controlType: ControlType.Dial,
is_program_controller: false,
custom_units: "",
connection_optional: false,
scale_points: [
new ScalePoint().deserialize({ value: -60, label: "-INF" }),
],
}),
new UiControl().applyProperties({
symbol: "auxIn",
name: "Aux In",
index: 2,
is_input: true,
min_value: -60.0,
max_value: 20.0,
default_value: 0.0,
is_bypass: false,
units: Units.db,
controlType: ControlType.Dial,
is_program_controller: false,
custom_units: "",
connection_optional: false,
scale_points: [
new ScalePoint().deserialize({ value: -60, label: "-INF" }),
],
}),
new UiControl().applyProperties({
symbol: "auxOut",
name: "Aux Out",
index: 3,
is_input: true,
min_value: -60.0,
max_value: 20.0,
default_value: 0.0,
is_bypass: false,
units: Units.db,
controlType: ControlType.Dial,
is_program_controller: false,
custom_units: "",
connection_optional: false,
scale_points: [
new ScalePoint().deserialize({ value: -60, label: "-INF" }),
],
}),
new UiControl().applyProperties({
symbol: "mainInVu",
name: "In",
index: 4,
is_input: false,
min_value: -60.0,
max_value: 20.0,
default_value: 0.0,
controlType: ControlType.Vu,
is_bypass: false,
is_program_controller: false,
units: Units.db,
custom_units: "",
connection_optional: false,
}),
new UiControl().applyProperties({
symbol: "mainInVu",
name: "In",
index: 5,
is_input: false,
min_value: -60.0,
max_value: 20.0,
default_value: 0.0,
controlType: ControlType.Vu,
is_bypass: false,
is_program_controller: false,
units: Units.db,
custom_units: "",
connection_optional: false,
}),
new UiControl().applyProperties({
symbol: "mainOutVu",
name: "Out",
index: 6,
is_input: false,
min_value: -60.0,
max_value: 20.0,
default_value: 0.0,
controlType: ControlType.Vu,
is_bypass: false,
is_program_controller: false,
units: Units.db,
custom_units: "",
connection_optional: false,
}),
new UiControl().applyProperties({
symbol: "auxOutVu",
name: "Out",
index: 7,
is_input: false,
min_value: -60.0,
max_value: 20.0,
default_value: 0.0,
controlType: ControlType.Vu,
is_bypass: false,
is_program_controller: false,
units: Units.db,
custom_units: "",
connection_optional: false,
}),
],
port_groups: [],
fileProperties: [],
frequencyPlots: [],
is_vst3: false,
}
);
}
+1
View File
@@ -364,6 +364,7 @@ export class Pedalboard implements Deserializable<Pedalboard> {
static readonly START_CONTROL = -2; // synthetic PedalboardItem for input volume.
static readonly END_CONTROL = -3; // synthetic PedalboardItem for output volume.
static readonly CHANNEL_MIXER_INSTANCE_ID = -4; // Synthetic instance ID for the channel routing mixer.
static readonly START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start";
static readonly END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End";
+44 -19
View File
@@ -46,6 +46,7 @@ import AudioFileMetadata from './AudioFileMetadata';
import { pathFileName } from './FileUtils';
import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer';
import { getDefaultModGuiPreference } from './ModGuiHost';
import ChannelMixerSettings from './ChannelMixerSettings';
export enum State {
Loading,
@@ -529,6 +530,13 @@ export class PiPedalModel //implements PiPedalModel
jackServerSettings: ObservableProperty<JackServerSettings>
= new ObservableProperty<JackServerSettings>(new JackServerSettings());
channelMixerControlValues: ObservableProperty<ControlValue[]> = new ObservableProperty<ControlValue[]>([]);
channelMixerSettings: ObservableProperty<ChannelMixerSettings> = new ObservableProperty<ChannelMixerSettings>(new ChannelMixerSettings());
setChannelMixerSettings(settings: ChannelMixerSettings) {
this.channelMixerSettings.set(settings);
this.channelMixerControlValues.set(settings.controls);
}
wifiConfigSettings: ObservableProperty<WifiConfigSettings> = new ObservableProperty<WifiConfigSettings>(new WifiConfigSettings());
wifiDirectConfigSettings: ObservableProperty<WifiDirectConfigSettings> = new ObservableProperty<WifiDirectConfigSettings>(new WifiDirectConfigSettings());
governorSettings: ObservableProperty<GovernorSettings> = new ObservableProperty<GovernorSettings>(new GovernorSettings());
@@ -1643,35 +1651,52 @@ export class PiPedalModel //implements PiPedalModel
private lastControlMessageWasSentbyMe = false;
private _setChannelMixerControlValue(key: string, value: number, notifyServer: boolean) : void {
let channelMixerSettings = this.channelMixerSettings.get();
let changed = channelMixerSettings.setControlValue(key, value);
if (changed)
{
this.channelMixerControlValues.set(this.channelMixerSettings.get().controls);
if (notifyServer) {
this.lastControlMessageWasSentbyMe = true;
this._setServerControl("setControl", Pedalboard.CHANNEL_MIXER_INSTANCE_ID, key, value);
}
}
}
private _setPedalboardControlValue(instanceId: number, key: string, value: number, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready.");
let newPedalboard = pedalboard.clone();
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") {
this._setInputVolume(value, notifyServer);
if (instanceId == Pedalboard.CHANNEL_MIXER_INSTANCE_ID) {
this._setChannelMixerControlValue(key,value,notifyServer);
return;
} else if (instanceId === Pedalboard.END_CONTROL) {
this._setOutputVolume(value, notifyServer);
return;
}
let item = newPedalboard.getItem(instanceId);
let changed = item.setControlValue(key, value);
} else {
let changed: boolean;
let newPedalboard = pedalboard.clone();
if (changed) {
if (notifyServer) {
this.lastControlMessageWasSentbyMe = true;
this._setServerControl("setControl", instanceId, key, value);
if (instanceId === Pedalboard.START_CONTROL && key === "volume_db") {
this._setInputVolume(value, notifyServer);
return;
} else if (instanceId === Pedalboard.END_CONTROL) {
this._setOutputVolume(value, notifyServer);
return;
}
this.setModelPedalboard(newPedalboard);
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
let item = this._controlValueChangeItems[i];
if (instanceId === item.instanceId) {
item.onValueChanged(key, value);
let item = newPedalboard.getItem(instanceId);
changed = item.setControlValue(key, value);
if (changed) {
if (notifyServer) {
this.lastControlMessageWasSentbyMe = true;
this._setServerControl("setControl", instanceId, key, value);
}
this.setModelPedalboard(newPedalboard);
for (let i = 0; i < this._controlValueChangeItems.length; ++i) {
let item = this._controlValueChangeItems[i];
if (instanceId === item.instanceId) {
item.onValueChanged(key, value);
}
}
}
}
}
private _setVst3PedalboardControlValue(instanceId: number, key: string, value: number, state: string, notifyServer: boolean): void {
let pedalboard = this.pedalboard.get();
@@ -1,4 +1,4 @@
// Copyright (c) 2022 Robin Davies
// Copyright (c) 2026 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
+69 -16
View File
@@ -20,11 +20,13 @@
import React, { SyntheticEvent, Component } from 'react';
import Switch from "@mui/material/Switch";
import ChannelMixerSettings from './ChannelMixerSettings';
import ChannelMixerSettingsDialog from './ChannelMixerSettingsDialog';
import OkCancelDialog from './OkCancelDialog';
import RadioSelectDialog from './RadioSelectDialog';
import IconButtonEx from './IconButtonEx';
import Typography from '@mui/material/Typography';
import {isDarkMode} from './DarkMode';
import { isDarkMode } from './DarkMode';
import { PiPedalModel, PiPedalModelFactory, State } from './PiPedalModel';
import { ColorTheme } from './DarkMode';
import ButtonBase from "@mui/material/ButtonBase";
@@ -75,7 +77,8 @@ interface SettingsDialogState {
showStatusMonitor: boolean;
showStatusMonitorDialog: boolean;
jackConfiguration: JackConfiguration;
jackSettings: JackChannelSelection;
jackSettings: JackChannelSelection | null;
channelMixerSettings: ChannelMixerSettings | null;
jackServerSettings: JackServerSettings;
alsaSequencerConfiguration: AlsaSequencerConfiguration;
keepScreenOn: boolean;
@@ -98,6 +101,7 @@ interface SettingsDialogState {
showMidiSelectDialog: boolean;
showThemeSelectDialog: boolean;
showJackServerSettingsDialog: boolean;
showChannelMixerSettingsDialog: boolean;
shuttingDown: boolean;
restarting: boolean;
isAndroidHosted: boolean;
@@ -190,6 +194,7 @@ const SettingsDialog = withStyles(
showStatusMonitorDialog: false,
jackServerSettings: this.model.jackServerSettings.get(),
channelMixerSettings: this.model.channelMixerSettings.get(),
jackConfiguration: this.model.jackConfiguration.get(),
jackStatus: undefined,
jackSettings: this.model.jackSettings.get(),
@@ -210,6 +215,7 @@ const SettingsDialog = withStyles(
showMidiSelectDialog: false,
showThemeSelectDialog: false,
showJackServerSettingsDialog: false,
showChannelMixerSettingsDialog: false,
shuttingDown: false,
restarting: false,
showShutdownOkDialog: false,
@@ -222,6 +228,7 @@ const SettingsDialog = withStyles(
this.handleAlsaSequencerConfigurationChanged = this.handleAlsaSequencerConfigurationChanged.bind(this);
this.handleJackSettingsChanged = this.handleJackSettingsChanged.bind(this);
this.handleJackServerSettingsChanged = this.handleJackServerSettingsChanged.bind(this);
this.handleChannelMixerSettingsChanged = this.handleChannelMixerSettingsChanged.bind(this);
this.handleWifiConfigSettingsChanged = this.handleWifiConfigSettingsChanged.bind(this);
this.handleWifiDirectConfigSettingsChanged = this.handleWifiDirectConfigSettingsChanged.bind(this);
this.handleGovernorSettingsChanged = this.handleGovernorSettingsChanged.bind(this);
@@ -323,6 +330,12 @@ const SettingsDialog = withStyles(
continueDisabled: !this.model.jackServerSettings.get().valid
});
}
handleChannelMixerSettingsChanged(): void {
this.setState({
channelMixerSettings: this.model.channelMixerSettings.get(),
continueDisabled: !this.model.channelMixerSettings.get().configured
});
}
handleJackConfigurationChanged(): void {
this.setState({
@@ -365,6 +378,7 @@ const SettingsDialog = withStyles(
this.model.jackConfiguration.addOnChangedHandler(this.handleJackConfigurationChanged);
this.model.alsaSequencerConfiguration.addOnChangedHandler(this.handleAlsaSequencerConfigurationChanged);
this.model.jackServerSettings.addOnChangedHandler(this.handleJackServerSettingsChanged);
this.model.channelMixerSettings.addOnChangedHandler(this.handleChannelMixerSettingsChanged);
this.model.wifiConfigSettings.addOnChangedHandler(this.handleWifiConfigSettingsChanged);
this.model.wifiDirectConfigSettings.addOnChangedHandler(this.handleWifiDirectConfigSettingsChanged);
this.model.governorSettings.addOnChangedHandler(this.handleGovernorSettingsChanged);
@@ -387,6 +401,7 @@ const SettingsDialog = withStyles(
this.handleJackSettingsChanged();
this.handleShowStatusMonitorChanged();
this.handleJackServerSettingsChanged();
this.handleChannelMixerSettingsChanged();
this.handleWifiConfigSettingsChanged();
this.handleWifiDirectConfigSettingsChanged();
this.setState({ hasWifiDevice: this.model.hasWifiDevice.get() });
@@ -405,6 +420,7 @@ const SettingsDialog = withStyles(
this.model.alsaSequencerConfiguration.removeOnChangedHandler(this.handleAlsaSequencerConfigurationChanged);
this.model.jackSettings.removeOnChangedHandler(this.handleJackSettingsChanged);
this.model.jackServerSettings.removeOnChangedHandler(this.handleJackServerSettingsChanged);
this.model.channelMixerSettings.removeOnChangedHandler(this.handleChannelMixerSettingsChanged);
this.model.wifiConfigSettings.removeOnChangedHandler(this.handleWifiConfigSettingsChanged);
this.model.wifiDirectConfigSettings.removeOnChangedHandler(this.handleWifiDirectConfigSettingsChanged);
this.model.governorSettings.removeOnChangedHandler(this.handleGovernorSettingsChanged);
@@ -503,6 +519,7 @@ const SettingsDialog = withStyles(
}
handleSelectChannelsDialogResult(channels: string[] | null): void {
if (this.state.jackSettings === null) { return; }
if (channels) {
let newSelection = this.state.jackSettings.clone();
if (this.state.showInputSelectDialog) {
@@ -595,10 +612,14 @@ const SettingsDialog = withStyles(
const classes = withStyles.getClasses(this.props);
let isConfigValid = this.state.jackConfiguration.isValid;
let selectedChannels: string[] = this.state.showInputSelectDialog ? this.state.jackSettings.inputAudioPorts : this.state.jackSettings.outputAudioPorts;
let selectedChannels: string[] =
this.state.jackSettings === null ? [] :
this.state.showInputSelectDialog ? this.state.jackSettings.inputAudioPorts : this.state.jackSettings.outputAudioPorts;
let disableShutdown = this.state.shuttingDown || this.state.restarting;
let canKeepScreenOn = this.model.canKeepScreenOn;
let hasAudioConfig = isConfigValid && this.state.jackConfiguration.outputAudioPorts.length >= 1
&& this.state.jackConfiguration.inputAudioPorts.length >= 1
return (
<DialogEx tag="settings" fullScreen open={this.props.open}
@@ -663,8 +684,8 @@ const SettingsDialog = withStyles(
{(!isConfigValid) ?
(
<div className={classes.cpuStatusColor} style={{ paddingLeft: 48, position: "relative", top: -12 }}>
<Typography display="block" variant="caption" color="textSecondary">Status:
<span style={{ color: isDarkMode() ? "#F88" : "#F00" }}>Not configured.</span></Typography>
<Typography display="block" variant="caption" color="textSecondary">Status:
<span style={{ color: isDarkMode() ? "#F88" : "#F00" }}>Not configured.</span></Typography>
{(!this.props.onboarding) && (
<Typography display="block" variant="caption" color="inherit">Governor: </Typography>
)}
@@ -693,23 +714,51 @@ const SettingsDialog = withStyles(
</Typography>
<ButtonBase className={classes.setting} onClick={() => this.handleJackServerSettings()}
>
<SelectHoverBackground selected={false} showHover={true} />
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<Typography display="block" variant="body2" noWrap>Audio device</Typography>
<Typography display="block" variant="caption" noWrap color="textSecondary">{this.state.jackServerSettings.getSummaryText()}</Typography>
</div>
</ButtonBase>
<JackServerSettingsDialog
open={this.state.showJackServerSettingsDialog}
jackServerSettings={this.state.jackServerSettings}
onClose={() => this.setState({ showJackServerSettingsDialog: false })}
onApply={(jackServerSettings) => {
{this.state.showChannelMixerSettingsDialog && (
<ChannelMixerSettingsDialog
open={this.state.showChannelMixerSettingsDialog}
onClose={() => this.setState({ showChannelMixerSettingsDialog: false })}
/>
)}
{this.state.showJackServerSettingsDialog && (
<JackServerSettingsDialog
open={this.state.showJackServerSettingsDialog}
jackServerSettings={this.state.jackServerSettings}
onClose={() => this.setState({ showJackServerSettingsDialog: false })}
onApply={(jackServerSettings) => {
this.setState({
jackServerSettings: jackServerSettings
});
this.model.setJackServerSettings(jackServerSettings);
}}
/>
)}
<ButtonBase className={classes.setting}
onClick={() => {
this.setState({
jackServerSettings: jackServerSettings
showChannelMixerSettingsDialog: true
});
this.model.setJackServerSettings(jackServerSettings);
}}
/>
disabled={!hasAudioConfig}
style={{ opacity: (!hasAudioConfig) ? 0.6 : 1.0 }}
>
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<Typography display="block" variant="body2" noWrap>Channel Routing</Typography>
<Typography display="block" variant="caption" color="textSecondary" noWrap>{
this.state.jackSettings == null ? "" :
this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)}</Typography>
</div>
</ButtonBase>
@@ -721,7 +770,9 @@ const SettingsDialog = withStyles(
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<Typography display="block" variant="body2" noWrap>Input channels</Typography>
<Typography display="block" variant="caption" color="textSecondary" noWrap>{this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)}</Typography>
<Typography display="block" variant="caption" color="textSecondary" noWrap>{
this.state.jackSettings == null ? "" :
this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)}</Typography>
</div>
</ButtonBase>
<ButtonBase className={classes.setting} onClick={() => this.handleOutputSelection()}
@@ -731,7 +782,9 @@ const SettingsDialog = withStyles(
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
<Typography display="block" variant="body2" noWrap>Output channels</Typography>
<Typography display="block" variant="caption" color="textSecondary" noWrap>{this.state.jackSettings.getAudioOutputDisplayValue(this.state.jackConfiguration)}</Typography>
<Typography display="block" variant="caption" color="textSecondary" noWrap>{
this.state.jackSettings == null ? "" :
this.state.jackSettings.getAudioOutputDisplayValue(this.state.jackConfiguration)}</Typography>
</div>
</ButtonBase>
<Divider />