diff --git a/vite/src/pipedal/ChannelMixerSettings.tsx b/vite/src/pipedal/ChannelMixerSettings.tsx new file mode 100644 index 0000000..983131f --- /dev/null +++ b/vite/src/pipedal/ChannelMixerSettings.tsx @@ -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; + } + +} \ No newline at end of file diff --git a/vite/src/pipedal/ChannelMixerSettingsDialog.tsx b/vite/src/pipedal/ChannelMixerSettingsDialog.tsx new file mode 100644 index 0000000..2d34295 --- /dev/null +++ b/vite/src/pipedal/ChannelMixerSettingsDialog.tsx @@ -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[] = [(None)] + if (channelCount === 0) { + return items; + } + if (channelCount === 2) { + items.push((Left)); + items.push((Right)); + return items; + } + for (let i = 0; i < channelCount; ++i) { + items.push((Ch {i + 1})); + } + 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(model.channelMixerSettings.get()); + const [controlValues,setControlValues] = useState(model.channelMixerControlValues.get()); + const [showHelp, setShowHelp] = useState(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 ( { + model.previewPedalboardValue(instanceId, symbol, value); + }} + onChange={(value: number) => { + model.setPedalboardControl(instanceId, symbol, value); + }} + requestIMEEdit={() => { }} + value={value} + />); + + return ; + } + 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 ( + + ) + } + let Vu = () => { + return ( +
+ +
+ ); + } + + let LandscapeView = () => { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + {/* Main */} + + + + + + + + + + + + + + + + + + {/* Spacer */} + + + {/* Aux */} + + + + + + + + + + + + + + + + + + +
InOut
+ Main + + {ChannelSelect(RouteType.Main, 0, true)} + {Dial("mainIn")}{Vu()} + + {Dial("mainOut")}{Vu()} + {ChannelSelect(RouteType.Main, 0, false)} +
+ {(ChannelSelect(RouteType.Main, 1, true))} + + {(ChannelSelect(RouteType.Main, 1, false))} +
+ Aux + + {(ChannelSelect(RouteType.Aux, 0, true))} + {Dial("auxIn")}{Vu()} + + {Dial("auxOut")}{Vu()} + {(ChannelSelect(RouteType.Aux, 0, false))} +
+ {(ChannelSelect(RouteType.Aux, 1, true))} + + {(ChannelSelect(RouteType.Aux, 1, false))} +
+
+ ); + }; + let PortraitView = () => { + return ( +
+ + + + + + + + + {/* Main */} + + + + + + + + + + + + + + + + + + + + + {/* Spacer */} + + + {/* Aux */} + + + + + + + + + + + + + + + + + + + + +
+ Main +
+ In + + {ChannelSelect(RouteType.Main, 0, true)} + + {ChannelSelect(RouteType.Main, 1, true)} +
+ + + + + + + + + + + + +
{Dial("mainIn")}{Vu()} +
+ +
+
{Dial("mainOut")}{Vu()}
+
+ Out + + {ChannelSelect(RouteType.Main, 0, false)} + + {ChannelSelect(RouteType.Main, 1, false)} +
+ Aux +
+ In + + {(ChannelSelect(RouteType.Aux, 0, true))} + + {(ChannelSelect(RouteType.Aux, 1, true))} +
+ + + + + + + + + + + + +
{Dial("auxIn")}{Vu()} + + {Dial("auxOut")}{Vu()}
+
+ Out + + {ChannelSelect(RouteType.Aux, 0, false)} + + {ChannelSelect(RouteType.Aux, 1, false)} +
+
+ ); + }; + + return ( + + + + { onClose(); }} + > + + + + + Channel Routing + + + { setShowHelp(true); }} + > + + + + + + + + {landscape ? + LandscapeView() + : + PortraitView() + } + + {showHelp && ( + setShowHelp(false)} /> + )} + + ); +} + +export default ChannelMixerSettingsDialog; diff --git a/vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx b/vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx new file mode 100644 index 0000000..345737b --- /dev/null +++ b/vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx @@ -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 { + + refText: React.RefObject; + + constructor(props: ChannelMixerSettingsHelpDialogProps) { + super(props); + this.state = { + fullScreen: false + }; + this.refText = React.createRef(); + } + 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 ( + { }} + > + + + { onClose(); }} + > + + + + + Channel Routing Dialog Help + + + + + +
+

The Mixer Routing Dialog determines how audio signals are processed and routed between input and output audio channels. +

+

Guitar effects processing occurs on the Main route only. Plugins in the main PiPedal window are + applied before any insert plugins that have been added to the Main 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. +

+

Plugins in the main PiPedal window are not applied to the Aux route. The Aux route is intended to be + used for +

+
    +
  • Passing through a dry (unprocessed) guitar signal that can be recorded by a DAW and + re-amped later during mixing. +
  • +
  • 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. +
  • +
  • + 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. +
  • +
+

although you may find other creative uses for the Aux channel as well.

+

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. +

+
+ +
+
+ ); + } +} \ No newline at end of file diff --git a/vite/src/pipedal/ChannelMixerUiControl.tsx b/vite/src/pipedal/ChannelMixerUiControl.tsx new file mode 100644 index 0000000..731a3f3 --- /dev/null +++ b/vite/src/pipedal/ChannelMixerUiControl.tsx @@ -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, + + } + ); +} + + + diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 137f302..31ae610 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -364,6 +364,7 @@ export class Pedalboard implements Deserializable { 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"; diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index aaa0152..083603e 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -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 = new ObservableProperty(new JackServerSettings()); + channelMixerControlValues: ObservableProperty = new ObservableProperty([]); + channelMixerSettings: ObservableProperty = new ObservableProperty(new ChannelMixerSettings()); + + setChannelMixerSettings(settings: ChannelMixerSettings) { + this.channelMixerSettings.set(settings); + this.channelMixerControlValues.set(settings.controls); + } wifiConfigSettings: ObservableProperty = new ObservableProperty(new WifiConfigSettings()); wifiDirectConfigSettings: ObservableProperty = new ObservableProperty(new WifiDirectConfigSettings()); governorSettings: ObservableProperty = new ObservableProperty(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(); diff --git a/vite/src/pipedal/SelectMidiChannelsDialog.tsx b/vite/src/pipedal/SelectMidiChannelsDialog.tsx index 4ca546e..949c66b 100644 --- a/vite/src/pipedal/SelectMidiChannelsDialog.tsx +++ b/vite/src/pipedal/SelectMidiChannelsDialog.tsx @@ -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 diff --git a/vite/src/pipedal/SettingsDialog.tsx b/vite/src/pipedal/SettingsDialog.tsx index b7f2a5f..7a47b71 100644 --- a/vite/src/pipedal/SettingsDialog.tsx +++ b/vite/src/pipedal/SettingsDialog.tsx @@ -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 ( - Status: - Not configured. + Status: + Not configured. {(!this.props.onboarding) && ( Governor: )} @@ -693,23 +714,51 @@ const SettingsDialog = withStyles( this.handleJackServerSettings()} > - +
Audio device {this.state.jackServerSettings.getSummaryText()}
- this.setState({ showJackServerSettingsDialog: false })} - onApply={(jackServerSettings) => { + {this.state.showChannelMixerSettingsDialog && ( + this.setState({ showChannelMixerSettingsDialog: false })} + /> + )} + {this.state.showJackServerSettingsDialog && ( + + this.setState({ showJackServerSettingsDialog: false })} + onApply={(jackServerSettings) => { + this.setState({ + jackServerSettings: jackServerSettings + }); + this.model.setJackServerSettings(jackServerSettings); + }} + /> + )} + + { this.setState({ - jackServerSettings: jackServerSettings + showChannelMixerSettingsDialog: true }); - this.model.setJackServerSettings(jackServerSettings); }} - /> + disabled={!hasAudioConfig} + style={{ opacity: (!hasAudioConfig) ? 0.6 : 1.0 }} + > + +
+ Channel Routing + { + this.state.jackSettings == null ? "" : + this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)} +
+
+ @@ -721,7 +770,9 @@ const SettingsDialog = withStyles(
Input channels - {this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)} + { + this.state.jackSettings == null ? "" : + this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)}
this.handleOutputSelection()} @@ -731,7 +782,9 @@ const SettingsDialog = withStyles(
Output channels - {this.state.jackSettings.getAudioOutputDisplayValue(this.state.jackConfiguration)} + { + this.state.jackSettings == null ? "" : + this.state.jackSettings.getAudioOutputDisplayValue(this.state.jackConfiguration)}