From 6a292cdc44351fa3c58a383b7d26b7f2d6b7416b Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sat, 24 Jan 2026 16:34:04 -0500 Subject: [PATCH 1/6] Midi bindings dialog broken for splits. --- vite/src/pipedal/Lv2Plugin.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/vite/src/pipedal/Lv2Plugin.tsx b/vite/src/pipedal/Lv2Plugin.tsx index ebe4165..be37817 100644 --- a/vite/src/pipedal/Lv2Plugin.tsx +++ b/vite/src/pipedal/Lv2Plugin.tsx @@ -1120,6 +1120,8 @@ export class UiPlugin implements Deserializable { + + export function makeSplitUiPlugin(): UiPlugin { return new UiPlugin().deserialize({ @@ -1138,7 +1140,7 @@ export function makeSplitUiPlugin(): UiPlugin { has_midi_output: 0, description: "", controls: [ - new UiControl().deserialize({ + new UiControl().applyProperties({ symbol: "splitType", name: "Type", index: 0, @@ -1159,7 +1161,7 @@ export function makeSplitUiPlugin(): UiPlugin { connection_optional: false, }), - new UiControl().deserialize({ + new UiControl().applyProperties({ symbol: "select", name: "Select", index: 1, @@ -1182,7 +1184,7 @@ export function makeSplitUiPlugin(): UiPlugin { }), - new UiControl().deserialize({ + new UiControl().applyProperties({ symbol: "mix", name: "Mix", index: 2, @@ -1195,9 +1197,10 @@ export function makeSplitUiPlugin(): UiPlugin { is_program_controller: false, custom_units: "", connection_optional: false, + scale_points: [], }), - new UiControl().deserialize({ + new UiControl().applyProperties({ symbol: "panL", name: "Pan Top", index: 3, @@ -1210,10 +1213,12 @@ export function makeSplitUiPlugin(): UiPlugin { is_program_controller: false, custom_units: "", connection_optional: false, + scale_points: [], }), - new UiControl().deserialize({ + new UiControl().applyProperties({ symbol: "volL", + name: "Vol Top", index: 4, is_input: true, min_value: -60.0, @@ -1224,9 +1229,10 @@ export function makeSplitUiPlugin(): UiPlugin { is_program_controller: false, custom_units: "", connection_optional: false, + scale_points: [], }), - new UiControl().deserialize({ + new UiControl().applyProperties({ symbol: "panR", name: "Pan Bottom", index: 5, @@ -1239,9 +1245,10 @@ export function makeSplitUiPlugin(): UiPlugin { is_program_controller: false, custom_units: "", connection_optional: false, + scale_points: [], }), - new UiControl().deserialize({ + new UiControl().applyProperties({ symbol: "volR", name: "Vol Bottom", index: 6, @@ -1254,6 +1261,8 @@ export function makeSplitUiPlugin(): UiPlugin { is_program_controller: false, custom_units: "", connection_optional: false, + scale_points: [], + }) ], port_groups: [], From 014c23f2701452ed8e1e672290957d8dd03cfd73 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sat, 24 Jan 2026 19:52:44 -0500 Subject: [PATCH 2/6] Routing, in progress. --- vite/src/pipedal/ChannelMixerSettings.tsx | 55 ++ .../pipedal/ChannelMixerSettingsDialog.tsx | 524 ++++++++++++++++++ .../ChannelMixerSettingsHelpDialog.tsx | 146 +++++ vite/src/pipedal/ChannelMixerUiControl.tsx | 188 +++++++ vite/src/pipedal/Pedalboard.tsx | 1 + vite/src/pipedal/PiPedalModel.tsx | 63 ++- vite/src/pipedal/SelectMidiChannelsDialog.tsx | 2 +- vite/src/pipedal/SettingsDialog.tsx | 85 ++- 8 files changed, 1028 insertions(+), 36 deletions(-) create mode 100644 vite/src/pipedal/ChannelMixerSettings.tsx create mode 100644 vite/src/pipedal/ChannelMixerSettingsDialog.tsx create mode 100644 vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx create mode 100644 vite/src/pipedal/ChannelMixerUiControl.tsx 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)}
From 0078e2661c4b0a66901826c5b8e806620e8aaaee Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sat, 24 Jan 2026 19:57:19 -0500 Subject: [PATCH 3/6] v1.5.99 --- CMakeLists.txt | 4 ++-- README.md | 4 ++-- docs/Installing.md | 10 +++++----- docs/ReleaseNotes.md | 5 +++++ docs/download.md | 8 ++++---- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 77fb1ac..8ed3d0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,13 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.5.98 + VERSION 1.5.99 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) execute_process( COMMAND dpkg --print-architecture COMMAND tr -d '\n' OUTPUT_VARIABLE DEBIAN_ARCHITECTURE ) -set (DISPLAY_VERSION "PiPedal v1.5.98-Beta") +set (DISPLAY_VERSION "PiPedal v1.5.99-Beta") set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (CMAKE_INSTALL_PREFIX "/usr/") diff --git a/README.md b/README.md index 7de051a..0e0fd95 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ -Download: v1.5.98 +Download: v1.5.99 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Documentation: [https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html). -#### NEW version 1.5.98 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE! +#### NEW version 1.5.99 Release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details. TooB NAM calibration, manipulation of i/r files in Toob Convolution Reverb, new Parametric EQ and 3 Band EQ plugins, and MORE!   diff --git a/docs/Installing.md b/docs/Installing.md index 0c20889..711fd98 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,18 +13,18 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v1.5.98](https://github.com/rerdavies/pipedal/releases/download/v1.5.98/pipedal_1.5.98_arm64.deb) -- [Ubuntu 24.04 through 25.04 (aarch64) v1.5.98](https://github.com/rerdavies/pipedal/releases/download/v1.5.98/pipedal_1.5.98_arm64.deb) -- [Ubuntu 24.04 through 25.04 (amd64) v1.5.98](https://github.com/rerdavies/pipedal/releases/download/v1.5.98/pipedal_1.5.98_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) +- [Ubuntu 24.04 through 25.04 (aarch64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) +- [Ubuntu 24.04 through 25.04 (amd64) v1.5.99](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_amd64.deb) -Version 1.5.98 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: +Version 1.5.99 has been tested on Raspberry Pi OS bookworm, Ubuntu 24.04 (amd64), Ubuntu 24.10 (aarch64), and Ubuntu 25.04 (aarch64). Download the appropriate package for your platform, and install using the following procedure: ``` sudo apt update sudo apt upgrade cd ~/Downloads - sudo apt-get install ./pipedal_1.5.98_arm64.deb + sudo apt-get install ./pipedal_1.5.99_arm64.deb ``` You MUST use `apt-get`. `apt` will not install downloaded packages; and `dpkg -i` will not install dependencies. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index 3f4991f..cb59e69 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,10 @@ # Release Notes +## PiPedal 1.5.99 Beta + +### Bug Fixes +- Midi Bindings dialog shows blank screen for pedalboards with splits. + ## PiPedal 1.5.98 Beta ### Features diff --git a/docs/download.md b/docs/download.md index 578ef1d..a6b9293 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,9 +4,9 @@ Download the most recent Debian (.deb) package for your platform: -- [Raspberry Pi OS bookworm (aarch64) v1.5.98 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.5.98/pipedal_1.5.98_arm64.deb) -- [Ubuntu 24.x, 25.04 (aarch64) v1.5.98 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.98/pipedal_1.5.98_arm64.deb) -- [Ubuntu 24.x, 25.04 (amd64) v1.5.98 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.98/pipedal_1.5.98_amd64.deb) +- [Raspberry Pi OS bookworm (aarch64) v1.5.99 Beta](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) +- [Ubuntu 24.x, 25.04 (aarch64) v1.5.99 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_arm64.deb) +- [Ubuntu 24.x, 25.04 (amd64) v1.5.99 Alpha](https://github.com/rerdavies/pipedal/releases/download/v1.5.99/pipedal_1.5.99_amd64.deb) Install the package by running @@ -14,7 +14,7 @@ Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.5.98_arm64.deb + sudo apt-get install ./pipedal_1.5.99_arm64.deb ``` You MUST use `apt-get` to install the package. `apt install` will NOT install the package correctly. The message about missing permissions given by `apt-get` is expected, and can be safely ignored. From 59263ee715f7d4f838051f79faa98918c9bc26c4 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Tue, 27 Jan 2026 10:10:36 -0500 Subject: [PATCH 4/6] Sync --- PiPedalCommon/src/include/json.hpp | 11 + PiPedalCommon/src/json.cpp | 6 +- src/AlsaDriver.cpp | 337 +++++-- src/AlsaDriverTest.cpp | 8 +- src/AudioDriver.hpp | 24 +- src/AudioHost.cpp | 116 ++- src/AudioHost.hpp | 3 +- src/Banks.hpp | 2 +- src/CMakeLists.txt | 1 + src/ChannelRouterSettings.cpp | 151 ++++ src/ChannelRouterSettings.hpp | 107 +++ src/DummyAudioDriver.cpp | 202 ++--- src/IHost.hpp | 4 +- src/JackConfiguration.hpp | 1 - src/Lv2Pedalboard.cpp | 4 +- src/Pedalboard.cpp | 18 +- src/Pedalboard.hpp | 47 +- src/PiLatencyMain.cpp | 8 +- src/PiPedalCommon.hpp | 32 + src/PiPedalModel.cpp | 52 +- src/PiPedalModel.hpp | 7 +- src/PiPedalSocket.cpp | 16 +- src/PluginHost.cpp | 7 +- src/PluginHost.hpp | 22 +- src/RingBufferReader.hpp | 20 +- src/Storage.cpp | 229 ++++- src/Storage.hpp | 20 +- todo.txt | 1 + vite/src/pipedal/ChannelMixerSettings.tsx | 55 -- .../pipedal/ChannelMixerSettingsDialog.tsx | 524 ----------- vite/src/pipedal/ChannelRouterSettings.tsx | 64 ++ .../pipedal/ChannelRouterSettingsDialog.tsx | 853 ++++++++++++++++++ ...sx => ChannelRouterSettingsHelpDialog.tsx} | 21 +- ...Control.tsx => ChannelRouterUiControl.tsx} | 5 +- vite/src/pipedal/Pedalboard.tsx | 45 +- vite/src/pipedal/PiPedalModel.tsx | 45 +- vite/src/pipedal/SettingsDialog.tsx | 36 +- vite/src/pipedal/ToobParametricEqView.tsx | 2 +- 38 files changed, 2127 insertions(+), 979 deletions(-) create mode 100644 src/ChannelRouterSettings.cpp create mode 100644 src/ChannelRouterSettings.hpp create mode 100644 src/PiPedalCommon.hpp delete mode 100644 vite/src/pipedal/ChannelMixerSettings.tsx delete mode 100644 vite/src/pipedal/ChannelMixerSettingsDialog.tsx create mode 100644 vite/src/pipedal/ChannelRouterSettings.tsx create mode 100644 vite/src/pipedal/ChannelRouterSettingsDialog.tsx rename vite/src/pipedal/{ChannelMixerSettingsHelpDialog.tsx => ChannelRouterSettingsHelpDialog.tsx} (80%) rename vite/src/pipedal/{ChannelMixerUiControl.tsx => ChannelRouterUiControl.tsx} (97%) diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index 7792535..48cb13c 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -57,6 +57,17 @@ } \ ; + +#define JSON_GETTER_SETTER_REF(name) \ + const decltype(name##_) &name() const { return name##_; } \ + decltype(name##_) &name() { return name##_; } \ + void name(const decltype(name##_) &value) { name##_ = value; } + +#define JSON_GETTER_SETTER(name) \ + decltype(name##_) name() const { return name##_; } \ + void name(decltype(name##_) value) { name##_ = value; } + + namespace pipedal { class JsonException: public std::runtime_error { diff --git a/PiPedalCommon/src/json.cpp b/PiPedalCommon/src/json.cpp index d486bf8..ca8e86e 100644 --- a/PiPedalCommon/src/json.cpp +++ b/PiPedalCommon/src/json.cpp @@ -562,8 +562,8 @@ void json_reader::skip_array() void json_reader::skip_object() { - int c; consume('{'); + int c; while (true) { c = peek(); @@ -571,11 +571,11 @@ void json_reader::skip_object() if (c == '}') { c = get(); - break; + return; } skip_string(); // name. consume(':'); - skip_object(); + skip_property(); if (peek() == ',') { consume(','); diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index c86c554..92472d6 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -23,6 +23,7 @@ */ #include "pch.h" +#include "PiPedalCommon.hpp" #include "util.hpp" #include #include "Finally.hpp" @@ -39,6 +40,7 @@ #include "CrashGuard.hpp" #include #include +#include "ChannelRouterSettings.hpp" #include "CpuUse.hpp" @@ -328,11 +330,34 @@ namespace pipedal bool inputSwapped = false; bool outputSwapped = false; - std::vector activeCaptureBuffers; - std::vector activePlaybackBuffers; + std::vector> allocatedBuffers; - std::vector captureBuffers; - std::vector playbackBuffers; + std::set usedOutputChannels; + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; + float *zeroInputBuffer = nullptr; + float *discardOutputBuffer = nullptr; + std::vector mainCaptureBuffers; + std::vector mainPlaybackBuffers; + + std::vector auxCaptureBuffers; + std::vector auxPlaybackBuffers; + + std::vector sendCaptureBuffers; + std::vector sendPlaybackBuffers; + + struct OutputBufferMix { + size_t outputChannel = 0; + std::vector inputBuffers; + float *outputBuffer = nullptr; + }; + + std::vector outputBufferMixes; + + + + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; std::vector rawCaptureBuffer; std::vector rawPlaybackBuffer; @@ -742,7 +767,7 @@ namespace pipedal { int32_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -760,7 +785,7 @@ namespace pipedal { float *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -776,7 +801,7 @@ namespace pipedal { int16_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr double scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -792,7 +817,7 @@ namespace pipedal { int16_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -809,7 +834,7 @@ namespace pipedal { int32_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -825,7 +850,7 @@ namespace pipedal { uint8_t *p = getCaptureBuffer(rawCaptureBuffer); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1LL); for (size_t frame = 0; frame < frames; ++frame) @@ -842,7 +867,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1LL); for (size_t frame = 0; frame < frames; ++frame) @@ -859,7 +884,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (0x00FFFFFFL + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -875,7 +900,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (0x00FFFFFFL + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -891,7 +916,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawCaptureBuffer.data(); - std::vector &buffers = this->captureBuffers; + std::vector &buffers = this->deviceCaptureBuffers; int channels = this->captureChannels; constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); for (size_t frame = 0; frame < frames; ++frame) @@ -907,7 +932,7 @@ namespace pipedal { int16_t *p = (int16_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr float scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -927,7 +952,7 @@ namespace pipedal { int16_t *p = (int16_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr float scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -947,7 +972,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -969,7 +994,7 @@ namespace pipedal int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = 0x00FFFFFF; for (size_t frame = 0; frame < frames; ++frame) @@ -991,7 +1016,7 @@ namespace pipedal int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = 0x00FFFFFF; for (size_t frame = 0; frame < frames; ++frame) @@ -1011,7 +1036,7 @@ namespace pipedal { int32_t *p = (int32_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1031,7 +1056,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1056,7 +1081,7 @@ namespace pipedal { uint8_t *p = (uint8_t *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; constexpr double scale = std::numeric_limits::max(); for (size_t frame = 0; frame < frames; ++frame) @@ -1082,7 +1107,7 @@ namespace pipedal { float *p = (float *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -1097,7 +1122,7 @@ namespace pipedal { float *p = (float *)rawPlaybackBuffer.data(); - std::vector &buffers = this->playbackBuffers; + std::vector &buffers = this->devicePlaybackBuffers; int channels = this->playbackChannels; for (size_t frame = 0; frame < frames; ++frame) { @@ -1127,9 +1152,9 @@ namespace pipedal } } - JackChannelSelection channelSelection; + ChannelSelection channelSelection; bool open = false; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { terminateAudio_ = false; if (open) @@ -1249,7 +1274,7 @@ namespace pipedal rawCaptureBuffer.resize(captureFrameSize * bufferSize * 2); memset(rawCaptureBuffer.data(), 0, rawCaptureBuffer.size()); - AllocateBuffers(captureBuffers, captureChannels); + AllocateBuffers(deviceCaptureBuffers, captureChannels); } virtual std::string GetConfigurationDescription() @@ -1260,8 +1285,15 @@ namespace pipedal << ", " << GetAlsaFormatDescription(this->captureFormat) << ", " << this->sampleRate << ", " << this->bufferSize << "x" << this->numberOfBuffers - << ", in: " << this->InputBufferCount() << "/" << this->captureChannels - << ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels); + << ", " << "device in: " << this->DeviceInputBufferCount() + << ", " << "device out: " << this->DeviceOutputBufferCount() + << ", main in: " << this->MainInputBufferCount() + << ", main out: " << this->MainOutputBufferCount() + << ", aux in: " << this->AuxInputBufferCount() + << ", aux out: " << this->AuxOutputBufferCount() + << ", send in: " << this->SendInputBufferCount() + << ", send out: " << this->SendOutputBufferCount() + ); return result; } void PreparePlaybackFunctions(snd_pcm_format_t playbackFormat) @@ -1321,10 +1353,10 @@ namespace pipedal rawPlaybackBuffer.resize(playbackFrameSize * bufferSize); memset(rawPlaybackBuffer.data(), 0, playbackFrameSize * bufferSize); - AllocateBuffers(playbackBuffers, playbackChannels); + AllocateBuffers(devicePlaybackBuffers, playbackChannels); } - void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + void OpenAudio(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { std::lock_guard lock{restartMutex}; @@ -1717,7 +1749,7 @@ namespace pipedal } return 0; } - void AudioThread() + PIPEDAL_NON_INLINE void AudioThread() { SetThreadName("alsaDriver"); @@ -1797,6 +1829,19 @@ namespace pipedal cpuUse.AddSample(ProfileCategory::Execute); + // Perform any neccessary mixing of outputs. + for (auto &bufferMix : outputBufferMixes) + { + float *outputBuffer = bufferMix.outputBuffer; + for (auto &mixBuffer : bufferMix.inputBuffers) + { + for (size_t i = 0; i < framesRead; ++i) + { + outputBuffer[i] += mixBuffer[i]; + } + } + } + (this->*copyOutputFn)(framesRead); cpuUse.AddSample(ProfileCategory::Driver); // process. @@ -1824,9 +1869,9 @@ namespace pipedal { this->driverHost->OnAlsaDriverStopped(); // zero out input buffers. - for (size_t i = 0; i < this->captureBuffers.size(); ++i) + for (size_t i = 0; i < this->deviceCaptureBuffers.size(); ++i) { - float *pBuffer = captureBuffers[i]; + float *pBuffer = deviceCaptureBuffers[i]; for (size_t j = 0; j < this->bufferSize; ++j) { pBuffer[j] = 0; @@ -1836,7 +1881,7 @@ namespace pipedal { while (!terminateAudio()) { - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); this->driverHost->OnProcess(this->bufferSize); } } @@ -1849,63 +1894,129 @@ namespace pipedal bool alsaActive = false; - static int IndexFromPortName(const std::string &s) - { - auto pos = s.find_last_of('_'); - if (pos == std::string::npos) - { - throw std::invalid_argument("Bad port name."); - } - const char *p = s.c_str() + (pos + 1); - int v = atoi(p); - if (v < 0) + PIPEDAL_NON_INLINE void AllocateInputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers + ) { + size_t nChannels = channelSelection.size(); + channelBuffers.resize(nChannels); + + for (size_t i = 0; i < nChannels; ++i) { - throw std::invalid_argument("Bad port name."); + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1 || deviceChannel >= captureChannels) + { + channelBuffers[i] = zeroInputBuffer; + } + else + { + channelBuffers[i] = deviceCaptureBuffers[deviceChannel]; + } + } + } + PIPEDAL_NON_INLINE void AddMixBuffer(int64_t outputChannel, float *mixBuffer) + { + for (auto&bufferMix: outputBufferMixes) + { + if (bufferMix.outputChannel == outputChannel) + { + bufferMix.inputBuffers.push_back(mixBuffer); + return; + } + } + OutputBufferMix newMix; + newMix.outputChannel = outputChannel; + newMix.outputBuffer = devicePlaybackBuffers[outputChannel]; + newMix.inputBuffers.push_back(mixBuffer); + outputBufferMixes.push_back(std::move(newMix)); + } + PIPEDAL_NON_INLINE void AllocateOutputChannels( + const std::vector &channelSelection, + std::vector &channelBuffers + ) { + size_t nChannels = channelSelection.size(); + + channelBuffers.resize(nChannels); + for (size_t i = 0; i < nChannels; ++i) + { + int64_t deviceChannel = channelSelection[i]; + if (deviceChannel == -1 || deviceChannel >= playbackChannels) + { + if (discardOutputBuffer == nullptr) + { + discardOutputBuffer = AllocateAudioBuffer(); + } + channelBuffers[i] = discardOutputBuffer; + } + else + { + if (!usedOutputChannels.contains(deviceChannel)) + { + usedOutputChannels.insert(deviceChannel); + channelBuffers[i] = deviceCaptureBuffers[deviceChannel]; + + } else { + // Used once already. We need to mix down. + float *mixBuffer = AllocateAudioBuffer(); + channelBuffers[i] = mixBuffer; + AddMixBuffer(deviceChannel, mixBuffer); + } + } } - return v; } + + bool activated = false; - virtual void Activate() + PIPEDAL_NON_INLINE virtual void Activate() { if (activated) { throw PiPedalStateException("Already activated."); } + activated = true; - this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size()); + // Reset previously allocated buffers. + usedOutputChannels.clear(); + allocatedBuffers.resize(0); - int ix = 0; - for (auto &x : channelSelection.GetInputAudioPorts()) + + // Allocate device capture buffers. + zeroInputBuffer = AllocateAudioBuffer(); + deviceCaptureBuffers.resize(captureChannels); + for (size_t i = 0; i < captureChannels; ++i) { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= captureBuffers.size()) - { - Lv2Log::error(SS("Invalid audio input port: " << x)); - } - else - { - this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; - } + deviceCaptureBuffers[i] = AllocateAudioBuffer(); + } + devicePlaybackBuffers.resize(playbackChannels); + for (size_t i = 0; i < playbackChannels; ++i) + { + devicePlaybackBuffers[i] = AllocateAudioBuffer(); } - this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size()); - - ix = 0; - for (auto &x : channelSelection.GetOutputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= playbackBuffers.size()) - { - Lv2Log::error(SS("Invalid audio output port: " << x)); - } - else - { - this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; - } - } + AllocateInputChannels( + channelSelection.mainInputChannels(), + this->mainCaptureBuffers); + AllocateOutputChannels( + channelSelection.mainOutputChannels(), + this->mainPlaybackBuffers + ); + AllocateInputChannels( + channelSelection.auxInputChannels(), + this->auxCaptureBuffers); + AllocateOutputChannels( + channelSelection.auxOutputChannels(), + this->auxPlaybackBuffers + ); + AllocateInputChannels( + channelSelection.sendInputChannels(), + this->sendCaptureBuffers); + AllocateOutputChannels( + channelSelection.sendOutputChannels(), + this->sendPlaybackBuffers + ); audioThread = std::make_unique([this]() { AudioThread(); }); @@ -1941,10 +2052,43 @@ namespace pipedal this->alsaSequencer = alsaSequencer; } - virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } - virtual float *GetInputBuffer(size_t channel) override + virtual size_t DeviceInputBufferCount() const override { + return deviceCaptureBuffers.size(); + } + virtual size_t DeviceOutputBufferCount() const override { + return devicePlaybackBuffers.size(); + } + + + virtual size_t MainInputBufferCount() const { return mainCaptureBuffers.size(); } + virtual float *GetMainInputBuffer(size_t channel) override { - return activeCaptureBuffers[channel]; + if (channel >= (int64_t)mainCaptureBuffers.size()) + { + throw std::runtime_error("Argument out of range."); + } + return mainCaptureBuffers[channel]; + } + virtual size_t AuxInputBufferCount() const { return auxCaptureBuffers.size(); } + virtual float *GetAuxInputBuffer(size_t channel) override + { + return auxCaptureBuffers[channel]; + } + virtual size_t AuxOutputBufferCount() const { + return auxPlaybackBuffers.size(); } + virtual float *GetAuxOutputBuffer(size_t channel) override + { + return auxPlaybackBuffers[channel]; + } + virtual size_t SendInputBufferCount() const { return sendCaptureBuffers.size(); } + virtual float *GetSendInputBuffer(size_t channel) override + { + return sendCaptureBuffers[channel]; + } + virtual size_t SendOutputBufferCount() const { return sendPlaybackBuffers.size(); } + virtual float *GetSendOutputBuffer(size_t channel) override + { + return sendPlaybackBuffers[channel]; } virtual size_t GetMidiInputEventCount() override @@ -1956,27 +2100,30 @@ namespace pipedal return this->midiEvents.data(); } - virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); } - virtual float *GetOutputBuffer(size_t channel) override + virtual size_t MainOutputBufferCount() const { return mainPlaybackBuffers.size(); } + virtual float *GetMainOutputBuffer(size_t channel) override { - return activePlaybackBuffers[channel]; + return mainPlaybackBuffers[channel]; } - void FreeBuffers(std::vector &buffer) - { - for (size_t i = 0; i < buffer.size(); ++i) - { - delete[] buffer[i]; - buffer[i] = 0; - } - buffer.clear(); + float* AllocateAudioBuffer() { + std::vector buffer; + buffer.resize(this->bufferSize); + float*pBuffer = buffer.data(); + allocatedBuffers.push_back(std::move(buffer)); + return pBuffer; } void DeleteBuffers() { - activeCaptureBuffers.clear(); - activePlaybackBuffers.clear(); - FreeBuffers(this->playbackBuffers); - FreeBuffers(this->captureBuffers); + mainCaptureBuffers.clear(); + mainPlaybackBuffers.clear(); + auxCaptureBuffers.clear(); + auxPlaybackBuffers.clear(); + sendCaptureBuffers.clear(); + sendPlaybackBuffers.clear(); + zeroInputBuffer = nullptr; + discardOutputBuffer = nullptr; + allocatedBuffers.clear(); } virtual void Close() { @@ -2241,7 +2388,7 @@ namespace pipedal { value += 1.0f * (c) / (128.0 * 256.0 * 256.0); } - this->playbackBuffers[c][i] = value; + this->devicePlaybackBuffers[c][i] = value; } } @@ -2257,7 +2404,7 @@ namespace pipedal for (size_t c = 0; c < captureChannels; ++c) { float error = - this->captureBuffers[c][i] - this->playbackBuffers[c][i]; + this->deviceCaptureBuffers[c][i] - this->devicePlaybackBuffers[c][i]; assert(std::abs(error) < 4e-5); } diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp index 2508570..129fa37 100644 --- a/src/AlsaDriverTest.cpp +++ b/src/AlsaDriverTest.cpp @@ -218,16 +218,16 @@ public: if (testType == TestType::NullTest) return; - size_t inputs = audioDriver->InputBufferCount(); - size_t outputs = audioDriver->OutputBufferCount(); + size_t inputs = audioDriver->MainInputBufferCount(); + size_t outputs = audioDriver->MainOutputBufferCount(); for (size_t i = 0; i < inputs; ++i) { - inputBuffers[i] = audioDriver->GetInputBuffer(i); + inputBuffers[i] = audioDriver->GetMainInputBuffer(i); } for (size_t i = 0; i < outputs; ++i) { - outputBuffers[i] = audioDriver->GetOutputBuffer(i); + outputBuffers[i] = audioDriver->GetMainOutputBuffer(i); } if (this->testType == TestType::Oscillator) { diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index 3899b05..ea88c28 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -35,6 +35,8 @@ namespace pipedal { using ProcessCallback = std::function; + class ChannelSelection; + class AudioDriverHost { public: @@ -58,14 +60,26 @@ namespace pipedal { virtual size_t GetMidiInputEventCount() = 0; virtual MidiEvent*GetMidiEvents() = 0; - virtual size_t InputBufferCount() const = 0; - virtual float*GetInputBuffer(size_t channel) = 0; + virtual size_t DeviceInputBufferCount() const = 0; + virtual size_t DeviceOutputBufferCount() const = 0; + virtual size_t MainInputBufferCount() const = 0; + virtual float*GetMainInputBuffer(size_t channel) = 0; + virtual size_t MainOutputBufferCount() const = 0; + virtual float*GetMainOutputBuffer(size_t channe) = 0; - virtual size_t OutputBufferCount() const = 0; - virtual float*GetOutputBuffer(size_t channe) = 0; + virtual size_t AuxInputBufferCount() const = 0; + virtual float*GetAuxInputBuffer(size_t channel) = 0; + virtual size_t AuxOutputBufferCount() const = 0; + virtual float*GetAuxOutputBuffer(size_t channel) = 0; - virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0; + virtual size_t SendInputBufferCount() const = 0; + virtual float*GetSendInputBuffer(size_t channel) = 0; + virtual size_t SendOutputBufferCount() const = 0; + virtual float*GetSendOutputBuffer(size_t channel) = 0; + + + virtual void Open(const JackServerSettings & jackServerSettings,const ChannelSelection &channelSelection) = 0; virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0; virtual void Activate() = 0; virtual void Deactivate() = 0; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index a744f84..c1eb2ca 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -18,6 +18,7 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +#include "PiPedalCommon.hpp" #include "AudioHost.hpp" #include "util.hpp" #include @@ -519,12 +520,17 @@ private: SystemMidiBinding rebootMidiBinding; SystemMidiBinding shutdownMidiBinding; - JackChannelSelection channelSelection; + ChannelSelection channelSelection; std::atomic active = false; std::atomic audioStopped = false; std::atomic isDummyAudioDriver = false; std::shared_ptr currentPedalboard; + std::shared_ptr currentMainInsertPedalboard; + std::shared_ptr currentAuxInsertPedalboard; + + + std::vector> activePedalboards; // pedalboards that have been sent to the audio queue. Lv2Pedalboard *realtimeActivePedalboard = nullptr; @@ -620,9 +626,9 @@ private: void ZeroOutputBuffers(size_t nframes) { - for (size_t i = 0; i < audioDriver->OutputBufferCount(); ++i) + for (size_t i = 0; i < audioDriver->MainOutputBufferCount(); ++i) { - float * out = (float *)audioDriver->GetOutputBuffer(i); + float * out = (float *)audioDriver->GetMainOutputBuffer(i); if (out) { ZeroBuffer(out, nframes); @@ -1173,9 +1179,9 @@ private: float *inputBuffers[4]; float *outputBuffers[4]; bool buffersValid = true; - for (int i = 0; i < audioDriver->InputBufferCount(); ++i) + for (int i = 0; i < audioDriver->MainInputBufferCount(); ++i) { - float *input = (float *)audioDriver->GetInputBuffer(i); + float *input = (float *)audioDriver->GetMainInputBuffer(i); if (input == nullptr) { buffersValid = false; @@ -1183,11 +1189,11 @@ private: } inputBuffers[i] = input; } - inputBuffers[audioDriver->InputBufferCount()] = nullptr; + inputBuffers[audioDriver->MainInputBufferCount()] = nullptr; - for (int i = 0; i < audioDriver->OutputBufferCount(); ++i) + for (int i = 0; i < audioDriver->MainOutputBufferCount(); ++i) { - float *output = audioDriver->GetOutputBuffer(i); + float *output = audioDriver->GetMainOutputBuffer(i); if (output == nullptr) { buffersValid = false; @@ -1195,7 +1201,7 @@ private: } outputBuffers[i] = output; } - outputBuffers[audioDriver->OutputBufferCount()] = nullptr; + outputBuffers[audioDriver->MainOutputBufferCount()] = nullptr; if (buffersValid) { @@ -1670,7 +1676,7 @@ public: return result; } - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection_) { std::lock_guard guard(mutex); @@ -1692,11 +1698,6 @@ public: this->audioDriver = std::unique_ptr(CreateAlsaDriver(this)); } - if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0) - { - return; - } - this->currentSample = 0; this->underruns = 0; @@ -1707,7 +1708,7 @@ public: this->realtimeReader.Reset(); this->realtimeWriter.Reset(); - this->channelSelection = channelSelection; + this->channelSelection = channelSelection_; StartReaderThread(); @@ -1795,6 +1796,7 @@ public: } } + virtual void SetPluginPreset(uint64_t instanceId, const std::vector &values) { std::lock_guard guard(mutex); @@ -1896,6 +1898,49 @@ public: pendingSnapshots.clear(); } + PIPEDAL_NON_INLINE RealtimePedalboardItemIndex GetRealtimeItemIndex(int64_t instanceId) + { + Pedalboard::InstanceType instanceType = Pedalboard::GetInstanceTypeFromInstanceId(instanceId); + int64_t index = -1; + switch (instanceType) + { + case Pedalboard::InstanceType::MainPedalboard: + if (this->currentPedalboard) { + if (instanceId == Pedalboard::START_CONTROL_ID) { + index = Pedalboard::START_CONTROL_ID; + } else if (instanceId == Pedalboard::END_CONTROL_ID) { + index = Pedalboard::END_CONTROL_ID; + } else { + index = this->currentPedalboard->GetIndexOfInstanceId(instanceId); + } + } + break; + case Pedalboard::InstanceType::MainInsert: + if (this->currentMainInsertPedalboard) { + if (instanceId == Pedalboard::MAIN_INSERT_START_CONTROL_ID) { + index = Pedalboard::MAIN_INSERT_START_CONTROL_ID; + } else if (instanceId == Pedalboard::MAIN_INSERT_END_CONTROL_ID) { + index = Pedalboard::END_CONTROL_ID; + } else { + index = this->currentMainInsertPedalboard->GetIndexOfInstanceId(instanceId); + } + } + break; + case Pedalboard::InstanceType::AuxInsert: + if (this->currentAuxInsertPedalboard) { + if (instanceId == Pedalboard::AUX_INSERT_START_CONTROL_ID) { + index = Pedalboard::START_CONTROL_ID; + } else if (instanceId == Pedalboard::AUX_INSERT_END_CONTROL_ID) { + index = Pedalboard::END_CONTROL_ID; + } else { + index = this->currentAuxInsertPedalboard->GetIndexOfInstanceId(instanceId); + } + } + break; + } + RealtimePedalboardItemIndex result { instanceType, index}; + return result; + } virtual void SetVuSubscriptions(const std::vector &instanceIds) { std::lock_guard guard(mutex); @@ -1917,7 +1962,7 @@ public: auto effect = this->currentPedalboard->GetEffect(instanceId); if (effect) { - int index = this->currentPedalboard->GetIndexOfInstanceId(instanceIds[i]); + RealtimePedalboardItemIndex index = this->GetRealtimeItemIndex(instanceIds[i]); vuConfig->enabledIndexes.push_back(index); VuUpdate v; v.instanceId_ = instanceId; @@ -1929,20 +1974,41 @@ public: vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); } - else if (instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) + else if ( + instanceId == Pedalboard::START_CONTROL_ID || instanceId == Pedalboard::END_CONTROL_ID + || instanceId == Pedalboard::MAIN_INSERT_START_CONTROL_ID || instanceId == Pedalboard::MAIN_INSERT_END_CONTROL_ID + || instanceId == Pedalboard::AUX_INSERT_START_CONTROL_ID || instanceId == Pedalboard::AUX_INSERT_END_CONTROL_ID + ) { - int index = (int)instanceId; + auto index = GetRealtimeItemIndex(instanceId); VuUpdate v; vuConfig->enabledIndexes.push_back(index); + v.instanceId_ = instanceId; - if (instanceId == Pedalboard::INPUT_VOLUME_ID) + size_t nChannels = 0; + + switch (instanceId) { - v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfInputAudioChannels() > 1; - } - else - { - v.isStereoInput_ = v.isStereoOutput_ = this->pHost->GetNumberOfOutputAudioChannels() > 1; + case Pedalboard::START_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainInputChannels().size(); + break; + case Pedalboard::END_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainOutputChannels().size(); + break; + case Pedalboard::MAIN_INSERT_START_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainOutputChannels().size(); + break; + case Pedalboard::MAIN_INSERT_END_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().mainOutputChannels().size(); + break; + case Pedalboard::AUX_INSERT_START_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().auxInputChannels().size(); + break; + case Pedalboard::AUX_INSERT_END_CONTROL_ID: + nChannels = this->pHost->GetChannelSelection().auxOutputChannels().size(); + break; } + v.isStereoInput_ = v.isStereoOutput_ = nChannels > 1; vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); } diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index f582547..d5af186 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -32,6 +32,7 @@ #include "Promise.hpp" #include "json_variant.hpp" #include "RealtimeMidiEventType.hpp" +#include "ChannelRouterSettings.hpp" namespace pipedal { @@ -227,7 +228,7 @@ namespace pipedal virtual std::string AtomToJson(const LV2_Atom *atom) = 0; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0; + virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) = 0; virtual void Close() = 0; virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; diff --git a/src/Banks.hpp b/src/Banks.hpp index 4522426..18951f3 100644 --- a/src/Banks.hpp +++ b/src/Banks.hpp @@ -240,7 +240,7 @@ namespace pipedal { // zero length? We can never have a zero-length bank. // Add a default preset and make it the selected preset. - Pedalboard pedalboard = Pedalboard::MakeDefault(); + Pedalboard pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); this->addPreset(pedalboard); newSelection = presets_[0]->instanceId(); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 97a2ffb..e8f0bae 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -204,6 +204,7 @@ else() endif() set (PIPEDAL_SOURCES + ChannelRouterSettings.cpp ChannelRouterSettings.hpp CrashGuard.cpp CrashGuard.hpp ModTemplateGenerator.cpp ModTemplateGenerator.hpp WebServerMod.cpp WebServerMod.hpp diff --git a/src/ChannelRouterSettings.cpp b/src/ChannelRouterSettings.cpp new file mode 100644 index 0000000..0b9e9be --- /dev/null +++ b/src/ChannelRouterSettings.cpp @@ -0,0 +1,151 @@ +// 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. + + +#include "ChannelRouterSettings.hpp" +#include + +using namespace pipedal; + +static uint64_t countChannels(const std::vector &channels) +{ + if (channels.size() != 2) + { + throw std::runtime_error("Invalid Channel Router channel configuration."); + } + if (channels[0] == -1) + { + if (channels[1] == -1) + { + return 0; + } + else + { + return 1; + } + } + else + { + if (channels[1] == -1) + { + return 1; + } + if (channels[0] == channels[1]) + { + return 1; + } + return 2; + } +} + +uint64_t ChannelRouterSettings::numberOfAudioInputChannels() const +{ + if (!configured_) + { + return 2; + } + return countChannels(mainInputChannels_); +} +uint64_t ChannelRouterSettings::numberOfAudioOutputChannels() const +{ + if (!configured_) + { + return 2; + } + return countChannels(mainOutputChannels_); +} + +ChannelSelection::ChannelSelection(ChannelRouterSettings&settings) +: mainInputChannels_(settings.mainInputChannels()) +, mainOutputChannels_(settings.mainOutputChannels()) +, auxInputChannels_(settings.auxInputChannels()) +, auxOutputChannels_(settings.auxOutputChannels()) +, sendInputChannels_(settings.sendInputChannels()) +, sendOutputChannels_(settings.sendOutputChannels()) +{ + normalizeChannelSelection(); +} + +static void normalizeInputChannels(std::vector&channels) { + if (channels.size() == 2) { + if (channels[0] == -1 && channels[1] == -1) + { + channels.resize(0); + } else if (channels[0] == channels[1]) { + channels.resize(1); + } else if (channels[1] == -1) { + channels.resize(1);} { + } + } +} + +static void normalizeOutputChannels(std::vector&channels) { + normalizeInputChannels(channels); +} + +void ChannelSelection::normalizeChannelSelection() { + normalizeInputChannels(mainInputChannels_); + normalizeOutputChannels(mainOutputChannels_); + normalizeInputChannels(auxInputChannels_); + normalizeOutputChannels(auxOutputChannels_); + normalizeInputChannels(sendInputChannels_); + normalizeOutputChannels(sendOutputChannels_); + + // If either aux inputs or outputs are zero, don't do ANY aux processing. + if (auxInputChannels_.size() == 0) + { + auxOutputChannels_.resize(0); + } else if (auxOutputChannels_.size() == 0) { + auxInputChannels_.resize(0); + } + // If either main inputs or outputs are empty, add send/receive dummy buffers. (Lv2 plugins shouldn't have to deal with this) + if (mainInputChannels_.size() == 0) + { + mainInputChannels_.resize(1); + mainInputChannels_[0] = -1; + } + if (mainOutputChannels_.size() == 0) { + mainInputChannels_.resize(1); + mainOutputChannels_[0] = -1; + } + // Send buffers are what they are. Let the send plugin deal with it. +} + + +ChannelRouterSettings::ChannelRouterSettings() +: mainInserts_(Pedalboard::InstanceType::MainInsert) +, auxInserts_(Pedalboard::InstanceType::AuxInsert) +{ + +} + + +JSON_MAP_BEGIN(ChannelRouterSettings) +JSON_MAP_REFERENCE(ChannelRouterSettings, configured) +JSON_MAP_REFERENCE(ChannelRouterSettings, channelRouterPresetId) +JSON_MAP_REFERENCE(ChannelRouterSettings, mainInputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, mainOutputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, mainInserts) +JSON_MAP_REFERENCE(ChannelRouterSettings, auxInputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, auxOutputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, auxInserts) +JSON_MAP_REFERENCE(ChannelRouterSettings, sendInputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, sendOutputChannels) +JSON_MAP_REFERENCE(ChannelRouterSettings, controlValues) +JSON_MAP_END() diff --git a/src/ChannelRouterSettings.hpp b/src/ChannelRouterSettings.hpp new file mode 100644 index 0000000..885b6e9 --- /dev/null +++ b/src/ChannelRouterSettings.hpp @@ -0,0 +1,107 @@ +// 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. + +#pragma once + +#include +#include "json.hpp" +#include "Pedalboard.hpp" +#include + +namespace pipedal +{ + + class ChannelRouterSettings + { + protected: + static constexpr int64_t CHANNEL_ROUTER_MAIN_INSERT_ID = -4; // Reserved Instance ID for Router Main Inserts. + static constexpr int64_t CHANNEL_ROUTER_AUX_INSERT_ID = -5; // Reserved Instance ID for Router Aux inserts. + + bool configured_ = false; + int64_t channelRouterPresetId_ = -1; + + std::vector mainInputChannels_ = {1, 1}; + std::vector mainOutputChannels_ = {0, 1}; + Pedalboard mainInserts_; + + std::vector auxInputChannels_ = {-1, -1}; + std::vector auxOutputChannels_ = {-1, -1}; + Pedalboard auxInserts_; + + std::vector sendInputChannels_ = {-1, -1}; + std::vector sendOutputChannels_ = {-1, -1}; + + std::vector controlValues_; + + public: + using self = ChannelRouterSettings; + using ptr = std::shared_ptr; + + ChannelRouterSettings(); + + uint64_t numberOfAudioInputChannels() const; + uint64_t numberOfAudioOutputChannels() const; + + JSON_GETTER_SETTER(configured) + JSON_GETTER_SETTER(channelRouterPresetId) + JSON_GETTER_SETTER_REF(mainInputChannels) + JSON_GETTER_SETTER_REF(mainOutputChannels) + JSON_GETTER_SETTER_REF(mainInserts) + JSON_GETTER_SETTER_REF(auxInputChannels) + JSON_GETTER_SETTER_REF(auxOutputChannels) + JSON_GETTER_SETTER_REF(auxInserts) + JSON_GETTER_SETTER_REF(sendInputChannels) + JSON_GETTER_SETTER_REF(sendOutputChannels) + JSON_GETTER_SETTER_REF(controlValues) + + + DECLARE_JSON_MAP(ChannelRouterSettings); + + }; + + // just the channel selecttions. + class ChannelSelection { + public: + ChannelSelection() = default; + ChannelSelection(ChannelRouterSettings&settings); + ChannelSelection(const ChannelSelection&other) = default; + ChannelSelection(ChannelSelection&&other) = default; + ChannelSelection& operator=(const ChannelSelection&other) = default; + ChannelSelection& operator=(ChannelSelection&&other) = default; + ~ChannelSelection() = default; + + const std::vector&mainInputChannels() const { return mainInputChannels_; } + const std::vector&mainOutputChannels() const { return mainOutputChannels_; } + const std::vector&auxInputChannels() const { return auxInputChannels_; } + const std::vector&auxOutputChannels() const { return auxOutputChannels_; } + const std::vector&sendInputChannels() const { return sendInputChannels_; } + const std::vector&sendOutputChannels() const { return sendOutputChannels_; } + + private: + void normalizeChannelSelection(); + + std::vector mainInputChannels_; + std::vector mainOutputChannels_; + std::vector auxInputChannels_; + std::vector auxOutputChannels_; + std::vector sendInputChannels_; + std::vector sendOutputChannels_; + }; + +} \ No newline at end of file diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index dd2301b..0d298d7 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2024 Robin E. R. Davies + * Copyright (c) 2026 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 @@ -23,6 +23,7 @@ */ #include "pch.h" +#include "PiPedalCommon.hpp" #include "util.hpp" #include #include @@ -39,6 +40,7 @@ #include "ss.hpp" #include "SchedulerPriority.hpp" #include "CrashGuard.hpp" +#include "ChannelRouterSettings.hpp" #include "CpuUse.hpp" @@ -85,14 +87,19 @@ namespace pipedal uint32_t captureFrameSize = 0; - std::vector activeCaptureBuffers; - std::vector activePlaybackBuffers; + std::vector> allocatedBuffers; + std::vector mainCaptureBuffers; + std::vector mainPlaybackBuffers; - std::vector captureBuffers; - std::vector playbackBuffers; + std::vector auxCaptureBuffers; + std::vector auxPlaybackBuffers; + + std::vector sendCaptureBuffers; + std::vector sendPlaybackBuffers; + + std::vector deviceCaptureBuffers; + std::vector devicePlaybackBuffers; - uint8_t *rawCaptureBuffer = nullptr; - uint8_t *rawPlaybackBuffer = nullptr; AudioDriverHost *driverHost = nullptr; uint32_t channels = 2; @@ -145,7 +152,7 @@ namespace pipedal std::atomic terminateAudio_ = false; - void terminateAudio(bool terminate) + PIPEDAL_NON_INLINE void terminateAudio(bool terminate) { this->terminateAudio_ = terminate; } @@ -161,32 +168,28 @@ namespace pipedal } private: - void AllocateBuffers(std::vector &buffers, size_t n) + PIPEDAL_NON_INLINE void AllocateBuffers(std::vector &buffers, size_t n) { buffers.resize(n); for (size_t i = 0; i < n; ++i) { - buffers[i] = new float[this->bufferSize]; - for (size_t j = 0; j < this->bufferSize; ++j) - { - buffers[i][j] = 0; - } + buffers[i] = AllocateAudioBuffer(); } } - virtual size_t GetMidiInputEventCount() override + PIPEDAL_NON_INLINE virtual size_t GetMidiInputEventCount() override { return midiEventCount; } - virtual MidiEvent *GetMidiEvents() override + PIPEDAL_NON_INLINE virtual MidiEvent *GetMidiEvents() override { return this->midiEvents.data(); } - JackChannelSelection channelSelection; + ChannelSelection channelSelection; bool open = false; - virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + PIPEDAL_NON_INLINE virtual void Open(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { terminateAudio_ = false; if (open) @@ -208,7 +211,7 @@ namespace pipedal throw; } } - virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override + PIPEDAL_NON_INLINE virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override { this->alsaSequencer = alsaSequencer; } @@ -220,23 +223,28 @@ namespace pipedal << ", " << "Native float" << ", " << this->sampleRate << ", " << this->bufferSize << "x" << this->numberOfBuffers - << ", in: " << this->InputBufferCount() << "/" << this->captureChannels - << ", out: " << this->OutputBufferCount() << "/" << this->playbackChannels); + << ", " << "device in: " << this->DeviceInputBufferCount() + << ", " << "device out: " << this->DeviceOutputBufferCount() + << ", main in: " << this->MainInputBufferCount() + << ", main out: " << this->MainOutputBufferCount() + << ", aux in: " << this->AuxInputBufferCount() + << ", aux out: " << this->AuxOutputBufferCount() + << ", send in: " << this->SendInputBufferCount() + << ", send out: " << this->SendOutputBufferCount() + ); return result; } - void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + void OpenAudio(const JackServerSettings &jackServerSettings, const ChannelSelection &channelSelection) { int err; this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers(); this->bufferSize = jackServerSettings.GetBufferSize(); - AllocateBuffers(captureBuffers, channels); - AllocateBuffers(playbackBuffers, channels); } - std::jthread *audioThread; + std::unique_ptr audioThread; bool audioRunning; bool block = false; @@ -303,7 +311,7 @@ namespace pipedal this->driverHost->OnProcess(framesRead); /// no attempt at realtime. Just as long as we run occasionally. - std::this_thread::sleep_for(std::chrono::milliseconds(20)); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); } @@ -318,22 +326,6 @@ namespace pipedal bool alsaActive = false; - static int IndexFromPortName(const std::string &s) - { - auto pos = s.find_last_of('_'); - if (pos == std::string::npos) - { - throw std::invalid_argument("Bad port name."); - } - const char *p = s.c_str() + (pos + 1); - - int v = atoi(p); - if (v < 0) - { - throw std::invalid_argument("Bad port name."); - } - return v; - } bool activated = false; virtual void Activate() @@ -344,41 +336,17 @@ namespace pipedal } activated = true; - this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size()); + AllocateBuffers(deviceCaptureBuffers, channels); + AllocateBuffers(devicePlaybackBuffers, channels); - playbackBuffers.resize(channels); + AllocateBuffers(mainCaptureBuffers, channelSelection.mainInputChannels().size()); + AllocateBuffers(mainPlaybackBuffers, channelSelection.mainOutputChannels().size()); + AllocateBuffers(auxCaptureBuffers, channelSelection.auxInputChannels().size()); + AllocateBuffers(auxPlaybackBuffers, channelSelection.auxOutputChannels().size()); + AllocateBuffers(sendCaptureBuffers, channelSelection.sendInputChannels().size()); + AllocateBuffers(sendPlaybackBuffers, channelSelection.sendOutputChannels().size()); - int ix = 0; - for (auto &x : channelSelection.GetInputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= captureBuffers.size()) - { - Lv2Log::error(SS("Invalid audio input port: " << x)); - } - else - { - this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; - } - } - - this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size()); - - ix = 0; - for (auto &x : channelSelection.GetOutputAudioPorts()) - { - int sourceIndex = IndexFromPortName(x); - if (sourceIndex >= playbackBuffers.size()) - { - Lv2Log::error(SS("Invalid audio output port: " << x)); - } - else - { - this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; - } - } - - audioThread = new std::jthread([this]() + audioThread = std::make_unique([this]() { AudioThread(); }); } @@ -393,7 +361,7 @@ namespace pipedal if (audioThread) { this->audioThread->join(); - this->audioThread = 0; + this->audioThread = nullptr; } Lv2Log::debug("Audio thread joined."); } @@ -402,46 +370,64 @@ namespace pipedal public: + virtual size_t DeviceInputBufferCount() const override { return deviceCaptureBuffers.size(); } + virtual size_t DeviceOutputBufferCount() const override { return devicePlaybackBuffers.size(); } - virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } - virtual float *GetInputBuffer(size_t channel) + virtual size_t MainInputBufferCount() const override { return mainCaptureBuffers.size(); } + virtual float *GetMainInputBuffer(size_t channel) override { - return activeCaptureBuffers[channel]; + return mainCaptureBuffers[channel]; } - virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); } - virtual float *GetOutputBuffer(size_t channel) + virtual size_t MainOutputBufferCount() const { return mainPlaybackBuffers.size(); } + virtual float *GetMainOutputBuffer(size_t channel) override { - return activePlaybackBuffers[channel]; + return mainPlaybackBuffers[channel]; } - void FreeBuffers(std::vector &buffer) + virtual size_t AuxInputBufferCount() const override { return auxCaptureBuffers.size(); } + virtual float *GetAuxInputBuffer(size_t channel) override { - for (size_t i = 0; i < buffer.size(); ++i) - { - // delete[] buffer[i]; - buffer[i] = 0; - } - buffer.clear(); + return auxCaptureBuffers[channel]; } - void DeleteBuffers() + + virtual size_t AuxOutputBufferCount() const override { return auxPlaybackBuffers.size(); } + virtual float *GetAuxOutputBuffer(size_t channel) override { - activeCaptureBuffers.clear(); - activePlaybackBuffers.clear(); - FreeBuffers(this->playbackBuffers); - FreeBuffers(this->captureBuffers); - if (rawCaptureBuffer) - { - delete[] rawCaptureBuffer; - rawCaptureBuffer = nullptr; - } - if (rawPlaybackBuffer) - { - delete[] rawPlaybackBuffer; - rawPlaybackBuffer = nullptr; - } + return auxPlaybackBuffers[channel]; } - virtual void Close() + + virtual size_t SendInputBufferCount() const override { return sendCaptureBuffers.size(); } + virtual float *GetSendInputBuffer(size_t channel) override + { + return sendCaptureBuffers[channel]; + } + + virtual size_t SendOutputBufferCount() const override { return sendPlaybackBuffers.size(); } + virtual float *GetSendOutputBuffer(size_t channel) override + { + return sendPlaybackBuffers[channel]; + } + + + + + + PIPEDAL_NON_INLINE float*AllocateAudioBuffer() { + allocatedBuffers.push_back(std::vector(bufferSize)); + return allocatedBuffers.back().data(); + } + PIPEDAL_NON_INLINE void DeleteBuffers() + { + mainCaptureBuffers.clear(); + mainPlaybackBuffers.clear(); + auxCaptureBuffers.clear(); + auxPlaybackBuffers.clear(); + sendCaptureBuffers.clear(); + sendPlaybackBuffers.clear(); + allocatedBuffers.clear(); + } + PIPEDAL_NON_INLINE virtual void Close() { if (!open) { @@ -453,18 +439,18 @@ namespace pipedal DeleteBuffers(); } - virtual float CpuUse() + PIPEDAL_NON_INLINE virtual float CpuUse() { return 0; } - virtual float CpuOverhead() + PIPEDAL_NON_INLINE virtual float CpuOverhead() { return 0.1; } }; - AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost,const std::string&deviceName) + PIPEDAL_NON_INLINE AudioDriver *CreateDummyAudioDriver(AudioDriverHost *driverHost,const std::string&deviceName) { return new DummyDriverImpl(driverHost,deviceName); } diff --git a/src/IHost.hpp b/src/IHost.hpp index ab02649..48c7c52 100644 --- a/src/IHost.hpp +++ b/src/IHost.hpp @@ -26,6 +26,7 @@ namespace pipedal { class Lv2PluginInfo; class IEffect; class HostWorkerThread; + class ChannelSelection; class IHost { @@ -43,13 +44,12 @@ namespace pipedal { virtual size_t GetAtomBufferSize() const = 0; virtual bool HasMidiInputChannel() const = 0; - virtual int GetNumberOfInputAudioChannels() const = 0; - virtual int GetNumberOfOutputAudioChannels() const = 0; virtual std::shared_ptr GetPluginInfo(const std::string &uri) const = 0; virtual IEffect *CreateEffect(PedalboardItem &pedalboard) = 0; virtual std::string GetPluginStoragePath() const = 0; + virtual const ChannelSelection&GetChannelSelection() const = 0; }; } \ No newline at end of file diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index b76abb0..a6a9bc4 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -124,5 +124,4 @@ namespace pipedal DECLARE_JSON_MAP(JackChannelSelection); }; - } // namespace. \ No newline at end of file diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 4c750c8..34d0055 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -574,7 +574,7 @@ void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samp { int index = vuConfiguration->enabledIndexes[i]; VuUpdate *pUpdate = &vuConfiguration->vuUpdateWorkingData[i]; - if (index == Pedalboard::INPUT_VOLUME_ID) + if (index == Pedalboard::START_CONTROL_ID) { if (this->pedalboardInputBuffers.size() > 1) { @@ -588,7 +588,7 @@ void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samp pUpdate->AccumulateOutputs(&(this->pedalboardInputBuffers[0][0]), samples); // after input volume applied. } } - else if (index == Pedalboard::OUTPUT_VOLUME_ID) + else if (index == Pedalboard::END_CONTROL_ID) { if (this->pedalboardOutputBuffers.size() > 1) { diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 97d7f0f..5cdc8fb 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -210,7 +210,7 @@ PedalboardItem Pedalboard::MakeSplit() -Pedalboard Pedalboard::MakeDefault() +Pedalboard Pedalboard::MakeDefault(InstanceType instanceType) { // copy insanity. but it happens so rarely. Pedalboard result; @@ -510,6 +510,22 @@ Snapshot Pedalboard::MakeSnapshotFromCurrentSettings(const Pedalboard &previousP } +Pedalboard::Pedalboard(InstanceType instanceType = InstanceType::MainPedalboard) +{ + switch (instanceType) + { + case InstanceType::MainPedalboard: + nextInstanceId_ = 0; + break; + case InstanceType::MainInsert: + nextInstanceId_ = MAIN_INSERT_INSTANCE_BASE;; + break; + case InstanceType::AuxInsert: + nextInstanceId_ = AUX_INSERT_INSTANCE_BASE;; + break; + } +} + JSON_MAP_BEGIN(ControlValue) JSON_MAP_REFERENCE(ControlValue,key) diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 173b497..9532efb 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -230,11 +230,52 @@ class Pedalboard { int64_t selectedPlugin_ = -1; + static constexpr int64_t TWO_POWER_52 = 4503599627370496; // Maximum Javascript integer value. + static constexpr int64_t TWO_POWER_48 = 281474976710656; // Number of possible instance IDs for Main Pedalboards. + static constexpr int64_t MAX_INSTANCE_ID = TWO_POWER_48; + public: + + enum class InstanceType { + MainPedalboard, + MainInsert, + AuxInsert + }; + static constexpr int64_t MAIN_INSERT_INSTANCE_BASE = TWO_POWER_52-2*MAX_INSTANCE_ID; + static constexpr int64_t AUX_INSERT_INSTANCE_BASE = TWO_POWER_52-1*MAX_INSTANCE_ID; + + static constexpr int64_t CHANNEL_ROUTER_CONTROLS_INSTANCE_ID = -4; // Reserved Instance ID for Router Controls. + + static constexpr int64_t START_CONTROL_ID = -2; // synthetic PedalboardItem for input volume. + static constexpr int64_t END_CONTROL_ID = -3; // synthetic PedalboardItem for output volume. + static constexpr int64_t MAIN_INSERT_START_CONTROL_ID = MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; + static constexpr int64_t MAIN_INSERT_END_CONTROL_ID = MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + static constexpr int64_t AUX_INSERT_START_CONTROL_ID = AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; + static constexpr int64_t AUX_INSERT_END_CONTROL_ID = AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + + static InstanceType GetInstanceTypeFromInstanceId(int64_t instanceId) { + if (instanceId == START_CONTROL_ID) return InstanceType::MainPedalboard; + if (instanceId == END_CONTROL_ID) return InstanceType::MainPedalboard; + + if (instanceId >= MAIN_INSERT_INSTANCE_BASE) { + return InstanceType::MainInsert; + } else if (instanceId >= AUX_INSERT_INSTANCE_BASE) { + return InstanceType::AuxInsert; + } else { + return InstanceType::MainPedalboard; + } + } + InstanceType GetInstanceType() const { + return GetInstanceTypeFromInstanceId(this->nextInstanceId_); // instanceId is irrelevant here. + } + + + + Pedalboard(InstanceType instanceType = InstanceType::MainPedalboard); + // deep copy, breaking shared pointers. Pedalboard DeepCopy(); - static constexpr int64_t INPUT_VOLUME_ID = -2; // synthetic PedalboardItem for input volume. - static constexpr int64_t OUTPUT_VOLUME_ID = -3; // synthetic PedalboardItem for output volume. + bool SetControlValue(int64_t pedalItemId, const std::string &symbol, float value); bool SetItemTitle(int64_t pedalItemId, const std::string &title, const std::string&iconColor); bool SetItemEnabled(int64_t pedalItemId, bool enabled); @@ -266,7 +307,7 @@ public: PedalboardItem MakeSplit(); - static Pedalboard MakeDefault(); + static Pedalboard MakeDefault(Pedalboard::InstanceType instanceType); }; #undef GETTER_SETTER_REF diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index 01887e5..edc6d4f 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -364,16 +364,16 @@ public: virtual void OnProcess(size_t nFrames) { - size_t inputs = audioDriver->InputBufferCount(); - size_t outputs = audioDriver->OutputBufferCount(); + size_t inputs = audioDriver->MainInputBufferCount(); + size_t outputs = audioDriver->MainOutputBufferCount(); for (size_t i = 0; i < inputs; ++i) { - inputBuffers[i] = audioDriver->GetInputBuffer(i); + inputBuffers[i] = audioDriver->GetMainInputBuffer(i); } for (size_t i = 0; i < outputs; ++i) { - outputBuffers[i] = audioDriver->GetOutputBuffer(i); + outputBuffers[i] = audioDriver->GetMainOutputBuffer(i); } for (size_t i = 0; i < nFrames; ++i) diff --git a/src/PiPedalCommon.hpp b/src/PiPedalCommon.hpp new file mode 100644 index 0000000..a464e39 --- /dev/null +++ b/src/PiPedalCommon.hpp @@ -0,0 +1,32 @@ +// 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. + + + +#pragma once + +#ifndef PIPEDAL_NON_INLINE +#if defined(__GNUC__) || defined(__clang__) +#define PIPEDAL_NON_INLINE __attribute__((noinline)) +#elif defined(_MSC_VER) +#define PIPEDAL_NON_INLINE __declspec(noinline) +#else +#define PIPEDAL_NON_INLINE +#endif +#endif diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 7e9f4ff..f07b645 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -81,7 +81,7 @@ PiPedalModel::PiPedalModel() this->updater = Updater::Create(); this->updater->Start(); this->currentUpdateStatus = updater->GetCurrentStatus(); - this->pedalboard = Pedalboard::MakeDefault(); + this->pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); #if JACK_HOST this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag. this->jackServerSettings.ReadJackDaemonConfiguration(); @@ -216,6 +216,7 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) storage.Initialize(); pluginHost.SetPluginStoragePath(storage.GetPluginUploadDirectory()); + this->channelRouterSettings = storage.GetChannelRouterSettings(); this->systemMidiBindings = storage.GetSystemMidiBindings(); #if JACK_HOST @@ -298,7 +299,7 @@ void PiPedalModel::Load() if (CrashGuard::HasCrashed()) { // ignore the current preset, and load a blank pedalboard in order to avoid a potential plugin crash. - this->pedalboard = Pedalboard::MakeDefault(); + this->pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); } else { @@ -1422,12 +1423,8 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver) throw std::runtime_error("Audio configuration not valid."); } - auto channelSelection = this->storage.GetJackChannelSelection(jackConfiguration); + const auto & channelSelection = this->storage.GetChannelSelection(); - if (!channelSelection.isValid()) - { - throw std::runtime_error("Audio configuration not valid."); - } this->audioHost->Open(jackServerSettings, channelSelection); this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); @@ -1528,21 +1525,6 @@ std::vector PiPedalModel::GetAlsaSequencerPorts() return result; } -void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection) -{ - { - std::lock_guard lock(mutex); // copy atomically. - this->storage.SetJackChannelSelection(channelSelection); - - this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); - - CancelAudioRetry(); - } - - RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. - - this->FireChannelSelectionChanged(clientId); -} void PiPedalModel::FireChannelSelectionChanged(int64_t clientId) { @@ -1671,7 +1653,7 @@ void PiPedalModel::UpdateRealtimeVuSubscriptions() for (int i = 0; i < activeVuSubscriptions.size(); ++i) { auto instanceId = activeVuSubscriptions[i].instanceid; - if (pedalboard.HasItem(instanceId) || instanceId == Pedalboard::INPUT_VOLUME_ID || instanceId == Pedalboard::OUTPUT_VOLUME_ID) + if (pedalboard.HasItem(instanceId) || instanceId == Pedalboard::START_CONTROL_ID || instanceId == Pedalboard::END_CONTROL_ID) { addedInstances.insert(activeVuSubscriptions[i].instanceid); } @@ -3330,3 +3312,27 @@ int64_t PiPedalModel::CopyPresetsToBank(int64_t bankInstanceId, const std::vecto return lastAdded; } + + +ChannelRouterSettings::ptr PiPedalModel::GetChannelRouterSettings() +{ + std::lock_guard lock(mutex); + return this->channelRouterSettings; +} + +void PiPedalModel::SetChannelRouterSettings(int64_t clientId,ChannelRouterSettings::ptr &settings) +{ + { + std::lock_guard lock(mutex); + this->channelRouterSettings = settings; + this->storage.SetChannelRouterSettings(settings); + this->pluginHost.OnConfigurationChanged(jackConfiguration, *settings); + CancelAudioRetry(); + } + RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. + + this->FireChannelSelectionChanged(clientId); + +} + + diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 897f524..17f1cf9 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -40,6 +40,7 @@ #include "Promise.hpp" #include "AtomConverter.hpp" #include "FileEntry.hpp" +#include "ChannelRouterSettings.hpp" #include namespace pipedal @@ -176,6 +177,8 @@ namespace pipedal AtomConverter atomConverter; // must be AFTER pluginHost! Pedalboard pedalboard; + ChannelRouterSettings::ptr channelRouterSettings; + bool previousPedalboardLoaded = false; Pedalboard previousPedalboard; Storage storage; @@ -397,7 +400,6 @@ namespace pipedal JackConfiguration GetJackConfiguration(); - void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection); JackChannelSelection GetJackChannelSelection(); void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); @@ -497,6 +499,9 @@ namespace pipedal std::vector RequestBankPresets(int64_t bankInstanceId); int64_t ImportPresetsFromBank(int64_t bankInstanceId, const std::vector &presets); int64_t CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets); + + void SetChannelRouterSettings(int64_t clientId, ChannelRouterSettings::ptr &settings); + ChannelRouterSettings::ptr GetChannelRouterSettings(); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index a3bf46b..85d0624 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2023 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 @@ -1422,12 +1422,6 @@ public: this->model.AddNotificationSubscription(shared_from_this()); Reply(replyTo, "ehlo", clientId); } - else if (message == "setJackSettings") - { - JackChannelSelection jackSettings; - pReader->read(&jackSettings); - this->model.SetJackChannelSelection(this->clientId, jackSettings); - } else if (message == "setShowStatusMonitor") { bool showStatusMonitor; @@ -1847,6 +1841,14 @@ public: pReader->read(&args); auto result = this->model.CopyPresetsToBank(args.bankInstanceId_, args.presets_); this->Reply(replyTo,"copyPresetsToBank",result); + } else if (message == "getChannelRouterSettings") + { + ChannelRouterSettings::ptr result = this->model.GetChannelRouterSettings(); + this->Reply(replyTo, "getChannelRouterSettings", result); + } else if (message == "setChannelRouterSettings") { + ChannelRouterSettings::ptr args; + pReader->read(&args); + this->model.SetChannelRouterSettings(this->clientId, args); } else { diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 6973a5b..ca1c3c7 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -34,6 +34,7 @@ #include "lv2/urid/urid.h" #include "lv2/ui/ui.h" #include "lv2/core/lv2.h" +#include "ChannelRouterSettings.hpp" // #include "lv2.h" #include "lv2/atom/atom.h" @@ -315,15 +316,15 @@ PluginHost::PluginHost() this->urids = new Urids(mapFeature); } -void PluginHost::OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings) + +void PluginHost::OnConfigurationChanged(const JackConfiguration &configuration, const ChannelSelection &channelSelection) { this->sampleRate = configuration.sampleRate(); if (configuration.isValid()) { - this->numberOfAudioInputChannels = settings.GetInputAudioPorts().size(); - this->numberOfAudioOutputChannels = settings.GetOutputAudioPorts().size(); this->maxBufferSize = configuration.blockLength(); } + this->channelSelection = channelSelection; } PluginHost::~PluginHost() diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index 9e0abec..5b31745 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -43,6 +43,7 @@ #include "PiPedalUI.hpp" #include "MapPathFeature.hpp" #include "ModGui.hpp" +#include "ChannelRouterSettings.hpp" namespace pipedal { @@ -54,6 +55,7 @@ namespace pipedal class PluginHost; class JackConfiguration; class JackChannelSelection; + class ChannelRouterSettings; #ifndef LV2_PROPERTY_GETSET #define LV2_PROPERTY_GETSET(name) \ @@ -893,8 +895,8 @@ namespace pipedal size_t maxBufferSize = 1024; size_t maxAtomBufferSize = 16 * 1024; bool hasMidiInputChannel; - int numberOfAudioInputChannels = 1; - int numberOfAudioOutputChannels = 1; + ChannelSelection channelSelection; + double sampleRate = 48000; std::string vst3CachePath; @@ -955,13 +957,12 @@ namespace pipedal private: // IHost implementation. - virtual void SetMaxAudioBufferSize(size_t size) { maxBufferSize = size; } - virtual size_t GetMaxAudioBufferSize() const { return maxBufferSize; } - virtual size_t GetAtomBufferSize() const { return maxAtomBufferSize; } - virtual bool HasMidiInputChannel() const { return hasMidiInputChannel; } - virtual int GetNumberOfInputAudioChannels() const { return numberOfAudioInputChannels; } - virtual int GetNumberOfOutputAudioChannels() const { return numberOfAudioOutputChannels; } - virtual LV2_Feature *const *GetLv2Features() const { return (LV2_Feature *const *)&(this->lv2Features[0]); } + virtual void SetMaxAudioBufferSize(size_t size) override { maxBufferSize = size; } + virtual size_t GetMaxAudioBufferSize() const override { return maxBufferSize; } + virtual size_t GetAtomBufferSize() const override { return maxAtomBufferSize; } + virtual bool HasMidiInputChannel() const override { return hasMidiInputChannel; } + virtual LV2_Feature *const *GetLv2Features() const override { return (LV2_Feature *const *)&(this->lv2Features[0]); } + virtual const ChannelSelection &GetChannelSelection() const override { return this->channelSelection; } public: virtual MapFeature &GetMapFeature() override { return this->mapFeature; } @@ -1027,7 +1028,8 @@ namespace pipedal Urids *urids = nullptr; ModGuiUris *mod_gui_uris = nullptr; - void OnConfigurationChanged(const JackConfiguration &configuration, const JackChannelSelection &settings); + void OnConfigurationChanged(const JackConfiguration &configuration, const ChannelSelection &channelSelection); + std::shared_ptr GetPluginClass(const std::string &uri) const; bool is_a(const std::string &class_, const std::string &target_class); diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index c3260fb..46f8c46 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -96,6 +96,24 @@ namespace pipedal }; + struct RealtimePedalboardItemIndex { + RealtimePedalboardItemIndex() + : instanceType(Pedalboard::InstanceType::MainPedalboard), index(-1) + { + } + RealtimePedalboardItemIndex( + Pedalboard::InstanceType instanceType, + int index) + : instanceType(instanceType), index(index) + { + } + RealtimePedalboardItemIndex(const RealtimePedalboardItemIndex& other) = default; + RealtimePedalboardItemIndex& operator=(const RealtimePedalboardItemIndex& other) = default; + + Pedalboard::InstanceType instanceType = Pedalboard::InstanceType::MainPedalboard; + int index = -1; + }; + struct RealtimeMidiEventRequest { RealtimeMidiEventType eventType; @@ -159,7 +177,7 @@ namespace pipedal return &vuUpdateResponseData; } - std::vector enabledIndexes; + std::vector enabledIndexes; std::vector vuUpdateWorkingData; std::vector vuUpdateResponseData; diff --git a/src/Storage.cpp b/src/Storage.cpp index 81e283b..83a9524 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -196,11 +196,11 @@ void Storage::SetDataRoot(const std::filesystem::path &path) this->dataRoot__audio_uploads = dataRoot / "audio_uploads"; } -const std::filesystem::path &Storage::GetConfigRoot() +const std::filesystem::path &Storage::GetConfigRoot() const { return this->configRoot; } -const std::filesystem::path &Storage::GetDataRoot() +const std::filesystem::path &Storage::GetDataRoot() const { return this->dataRoot; } @@ -249,7 +249,6 @@ static void removeFileNoThrow(const std::filesystem::path &path) } } - void Storage::UpgradeFactoryPresets() { auto presetsDirectory = this->GetPresetsDirectory(); @@ -387,8 +386,10 @@ void Storage::Initialize() LoadWifiConfigSettings(); LoadWifiDirectConfigSettings(); + this->channelRouterSettings = LoadChannelRouterSettings(); LoadUserSettings(); UpgradeFactoryPresets(); + UpgradeChannelRouterSettings(); } void Storage::LoadBank(int64_t instanceId) @@ -687,40 +688,46 @@ int64_t Storage::SaveCurrentPresetAs(const Pedalboard &pedalboard, int64_t bankI } } -static std::string stripNumericSuffix(const std::string &name) { +static std::string stripNumericSuffix(const std::string &name) +{ // remove (digit*) from the end of the string. // Find the last '(' character size_t pos = name.find_last_of('('); - if (pos == std::string::npos) { + if (pos == std::string::npos) + { return name; } // Check if everything between '(' and ')' is digits - size_t len = name.length()-1; - if (name[len] != ')') + size_t len = name.length() - 1; + if (name[len] != ')') { return name; } bool allDigits = true; - for (size_t i = pos + 1; i < len - 1; ++i) { - if (name[i] < '0' || name[i] > '9') { + for (size_t i = pos + 1; i < len - 1; ++i) + { + if (name[i] < '0' || name[i] > '9') + { allDigits = false; break; } } - if (!allDigits) { + if (!allDigits) + { return name; } // Remove trailing spaces before the '(' - while (pos > 0 && name[pos - 1] == ' ') { + while (pos > 0 && name[pos - 1] == ' ') + { --pos; } return name.substr(0, pos); } -static std::string makeUniqueName(const std::string &name, const std::set&existingNames) +static std::string makeUniqueName(const std::string &name, const std::set &existingNames) { if (!existingNames.contains(name)) { @@ -729,9 +736,11 @@ static std::string makeUniqueName(const std::string &name, const std::set &presets) { - if (bankIndex.selectedBank() == bankInstanceId) { + if (bankIndex.selectedBank() == bankInstanceId) + { throw std::runtime_error("Can't import to self."); } - std::set presetsSet { presets.begin(), presets.end()}; + std::set presetsSet{presets.begin(), presets.end()}; auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); std::set existingNames; - for (auto&preset: this->currentBank.presets()) { + for (auto &preset : this->currentBank.presets()) + { existingNames.insert(preset->preset().name()); } BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); + LoadBankFile(indexEntry.name(), &bankFile); int64_t lastPresetId = -1; - for (auto &presetEntry: bankFile.presets()) { - if (presetsSet.contains(presetEntry->instanceId())) { - std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + for (auto &presetEntry : bankFile.presets()) + { + if (presetsSet.contains(presetEntry->instanceId())) + { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(), existingNames); existingNames.insert(uniqueName); Pedalboard t = presetEntry->preset(); t.name(uniqueName); @@ -767,31 +780,35 @@ int64_t Storage::ImportPresetsFromBank(int64_t bankInstanceId, const std::vector } int64_t Storage::CopyPresetsToBank(int64_t bankInstanceId, const std::vector &presets) { - if (bankIndex.selectedBank() == bankInstanceId) { + if (bankIndex.selectedBank() == bankInstanceId) + { throw std::runtime_error("Can't copy to self."); } auto indexEntry = this->bankIndex.getBankIndexEntry(bankInstanceId); BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); + LoadBankFile(indexEntry.name(), &bankFile); - std::set presetsSet { presets.begin(), presets.end()}; + std::set presetsSet{presets.begin(), presets.end()}; std::set existingNames; - for (auto&preset: bankFile.presets()) { + for (auto &preset : bankFile.presets()) + { existingNames.insert(preset->preset().name()); } - for (auto &presetEntry: this->currentBank.presets()) { - if (presetsSet.contains(presetEntry->instanceId())) { - std::string uniqueName = makeUniqueName(presetEntry->preset().name(),existingNames); + for (auto &presetEntry : this->currentBank.presets()) + { + if (presetsSet.contains(presetEntry->instanceId())) + { + std::string uniqueName = makeUniqueName(presetEntry->preset().name(), existingNames); existingNames.insert(uniqueName); Pedalboard t = presetEntry->preset(); t.name(uniqueName); bankFile.addPreset(t); } } - SaveBankFile(indexEntry.name(),bankFile); + SaveBankFile(indexEntry.name(), bankFile); return -1; } @@ -801,17 +818,15 @@ std::vector Storage::RequestBankPresets(int64_t bankInstanceId std::vector result; BankFile bankFile; - LoadBankFile(indexEntry.name(),&bankFile); - for (auto &preset: bankFile.presets()) { + LoadBankFile(indexEntry.name(), &bankFile); + for (auto &preset : bankFile.presets()) + { result.push_back( - PresetIndexEntry(preset->instanceId(),preset->preset().name()) - ); + PresetIndexEntry(preset->instanceId(), preset->preset().name())); } return result; } - - void Storage::SetPresetIndex(const PresetIndex &presets) { // painful because we must move unique_ptrs. @@ -885,7 +900,8 @@ Pedalboard Storage::GetPreset(int64_t instanceId) const int64_t Storage::DeletePresets(const std::vector &presetInstanceIds) { int64_t newSelection = currentBank.selectedPreset(); - for (auto presetId: presetInstanceIds) { + for (auto presetId : presetInstanceIds) + { newSelection = currentBank.deletePreset(presetId); } SaveCurrentBank(); @@ -977,7 +993,7 @@ int64_t Storage::CreateNewPreset() } } newPedalboard.name(name); - auto t = this->currentBank.addPreset(newPedalboard, -1); + auto t = this->currentBank.addPreset(newPedalboard, -1); SaveCurrentBank(); return t; } @@ -997,7 +1013,7 @@ int64_t Storage::CopyPreset(int64_t fromId, int64_t toId) { auto &toItem = this->currentBank.getItem(toId); toItem.preset(fromItem.preset()); - result = toId; + result = toId; } SaveCurrentBank(); return result; @@ -2075,21 +2091,21 @@ static bool ensureNoDotDot(const std::filesystem::path &path) return true; } -static bool isInfoFile(const std::string&fileName) +static bool isInfoFile(const std::string &fileName) { - if (strcasecmp(fileName.c_str(),"license.txt") == 0) + if (strcasecmp(fileName.c_str(), "license.txt") == 0) { return true; } - if (strcasecmp(fileName.c_str(),"license.md") == 0) + if (strcasecmp(fileName.c_str(), "license.md") == 0) { return true; } - if (strcasecmp(fileName.c_str(),"readme.txt") == 0) + if (strcasecmp(fileName.c_str(), "readme.txt") == 0) { return true; } - if (strcasecmp(fileName.c_str(),"readme.md") == 0) + if (strcasecmp(fileName.c_str(), "readme.md") == 0) { return true; } @@ -3021,11 +3037,13 @@ std::string Storage::GetTone3000Auth() const std::filesystem::path Storage::FromAbstractPathString(const std::string &stringPath) { - if (stringPath.empty()) { + if (stringPath.empty()) + { return ""; } - fs::path path { stringPath}; - if (path.is_absolute()) { + fs::path path{stringPath}; + if (path.is_absolute()) + { return path; } return GetPluginUploadDirectory() / path; @@ -3048,6 +3066,125 @@ std::string Storage::FromAbstractPathJson(const std::string &pathJson) return v.to_string(); } +void Storage::SetChannelRouterSettings(ChannelRouterSettings::ptr settings) +{ + this->channelRouterSettings = settings; + this->channelSelection = ChannelSelection(*settings); + SaveChannelRouterSettings(settings); +} +ChannelRouterSettings::ptr Storage::GetChannelRouterSettings() +{ + return channelRouterSettings; +} + +std::filesystem::path Storage::GetChannelRouterSettingsPath() const +{ + return GetDataRoot() / "ChannelRouterSettings.json"; +} + +void Storage::SaveChannelRouterSettings(ChannelRouterSettings::ptr settings) +{ + std::ofstream os(GetChannelRouterSettingsPath()); + json_writer writer(os); + writer.write(settings); +} +ChannelRouterSettings::ptr Storage::LoadChannelRouterSettings() +{ + auto path = GetChannelRouterSettingsPath(); + if (!fs::exists(path)) + { + // See UpgradeChannelRouterSettings(), which will upgrade from old settings + // or create a default instance. + return nullptr; // will be upgraded later. + } + try { + std::ifstream is(path); + json_reader reader(is); + ChannelRouterSettings::ptr result; + reader.read(&result); + return result; + } catch (const std::exception &e) { + Lv2Log::error("Failed to load Channel Router settings: %s", e.what()); + return std::make_shared(); + } +} + +static int64_t GetUpgradedPortIndex(const std::string &portName) +{ + auto nPos = portName.find_last_of('_'); + if (nPos != std::string::npos) + { + std::string channelStr = portName.substr(nPos + 1); + try + { + int64_t channelIndex = std::stoll(channelStr); + return channelIndex; + } catch (const std::exception &) { + return -1; + } + } + return -1; +} +void Storage::UpgradeChannelRouterSettings() +{ + if (channelRouterSettings == nullptr) + { + channelRouterSettings = std::make_shared(); + if (jackChannelSelection.isValid()) + { + channelRouterSettings->configured(true); + channelRouterSettings->mainInputChannels().resize(2); + const std::vector& oldInputs = jackChannelSelection.GetInputAudioPorts(); + std::vector& newInputs = channelRouterSettings->mainInputChannels(); + + if (oldInputs.size() ==1) + { + int64_t leftIndex = GetUpgradedPortIndex(oldInputs[0]); + newInputs.resize(2); + newInputs[0] = leftIndex; + newInputs[1] = leftIndex; + } else if (oldInputs.size() == 2) { + newInputs.resize(2); + for (size_t i = 0; i < std::min(oldInputs.size(), newInputs.size()); ++i) + { + int64_t portIndex = GetUpgradedPortIndex(oldInputs[i]); + newInputs[i] = portIndex; + } + } + const std::vector& oldOutputs = jackChannelSelection.GetOutputAudioPorts(); + auto &newMainOutputs = channelRouterSettings->mainOutputChannels(); + if (oldOutputs.size() == 1) { + int64_t leftIndex = GetUpgradedPortIndex(oldOutputs[0]); + newMainOutputs.resize(2); + newMainOutputs[0] = leftIndex; + newMainOutputs[1] = leftIndex; + } else if (oldOutputs.size() == 2) { + newMainOutputs.resize(2); + for (size_t i = 0; i < std::min(oldOutputs.size(), newMainOutputs.size()); ++i) + { + int64_t portIndex = GetUpgradedPortIndex(oldOutputs[i]); + newMainOutputs[i] = portIndex; + } + } + + auto &newAuxInputs = channelRouterSettings->auxInputChannels(); + newAuxInputs.resize(2); + newAuxInputs[0] = -1; + newAuxInputs[1] = -1; + auto &newAuxOutputs = channelRouterSettings->auxOutputChannels(); + newAuxOutputs.resize(2); + newAuxOutputs[0] = -1; + newAuxOutputs[1] = -1; + SaveChannelRouterSettings(channelRouterSettings); + } + channelSelection = ChannelSelection(*channelRouterSettings); + } +} + +const ChannelSelection& Storage::GetChannelSelection() const { + return channelSelection; +} + JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) diff --git a/src/Storage.hpp b/src/Storage.hpp index 21ae5ba..76b4199 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -32,6 +32,8 @@ #include #include "FilePropertyDirectoryTree.hpp" #include "AlsaSequencer.hpp" +#include "ChannelRouterSettings.hpp" + namespace pipedal { @@ -117,6 +119,15 @@ private: WifiDirectConfigSettings wifiDirectConfigSettings; UserSettings userSettings; + + ChannelRouterSettings::ptr channelRouterSettings; + ChannelSelection channelSelection; + + void UpgradeChannelRouterSettings(); + std::filesystem::path GetChannelRouterSettingsPath() const; + void SaveChannelRouterSettings(ChannelRouterSettings::ptr settings); + ChannelRouterSettings::ptr LoadChannelRouterSettings(); + public: Storage(); void Initialize(); @@ -124,8 +135,8 @@ public: void SetDataRoot(const std::filesystem::path& path); void SetConfigRoot(const std::filesystem::path& path); - const std::filesystem::path&GetConfigRoot(); - const std::filesystem::path&GetDataRoot(); + const std::filesystem::path&GetConfigRoot() const; + const std::filesystem::path&GetDataRoot() const; const std::filesystem::path &GetPluginUploadDirectory() const; @@ -206,6 +217,11 @@ public: void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); AlsaSequencerConfiguration GetAlsaSequencerConfiguration() const; + void SetChannelRouterSettings(ChannelRouterSettings::ptr settings); + ChannelRouterSettings::ptr GetChannelRouterSettings(); + + const ChannelSelection& GetChannelSelection() const; + //std::string MapPropertyFileName(Lv2PluginInfo*pluginInfo, const std::string&path); diff --git a/todo.txt b/todo.txt index 8423926..d3b331d 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,4 @@ +A way to force input to mono? ls diff --git a/vite/src/pipedal/ChannelMixerSettings.tsx b/vite/src/pipedal/ChannelMixerSettings.tsx deleted file mode 100644 index 983131f..0000000 --- a/vite/src/pipedal/ChannelMixerSettings.tsx +++ /dev/null @@ -1,55 +0,0 @@ - -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 deleted file mode 100644 index 2d34295..0000000 --- a/vite/src/pipedal/ChannelMixerSettingsDialog.tsx +++ /dev/null @@ -1,524 +0,0 @@ -// 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/ChannelRouterSettings.tsx b/vite/src/pipedal/ChannelRouterSettings.tsx new file mode 100644 index 0000000..bea7ef7 --- /dev/null +++ b/vite/src/pipedal/ChannelRouterSettings.tsx @@ -0,0 +1,64 @@ + +import { Pedalboard, ControlValue } from "./Pedalboard.tsx"; + + + + +export default class ChannelRouterSettings { + configured: boolean = false; + channelRouterPresetId: number = -1; + + mainInputChannels: number[] = [1,1]; + mainOutputChannels: number[] = [0,1]; + mainInserts: Pedalboard = new Pedalboard(); + + auxInputChannels: number[] = [0,0]; + auxOutputChannels: number[] = [-1,-1]; + auxInserts: Pedalboard = new Pedalboard(); + + sendInputChannels: number[] = [-1,-1]; + sendOutputChannels: number[] = [-1,-1]; + + controlValues: ControlValue[] = []; + // Inserts... + + deserialize(obj: any): ChannelRouterSettings { + this.configured = obj.configured; + this.channelRouterPresetId = obj.channelRouterPresetId ? obj.channelRouterPresetId : -1; + this.mainInputChannels = obj.mainInputChannels.slice(); + this.mainOutputChannels = obj.mainOutputChannels.slice(); + this.mainInserts = new Pedalboard().deserialize(obj.mainInserts); + this.auxInputChannels = obj.auxInputChannels.slice(); + this.auxOutputChannels = obj.auxOutputChannels.slice(); + this.auxInserts = new Pedalboard().deserialize(obj.auxInserts); + this.sendInputChannels = obj.sendInputChannels.slice(); + this.sendOutputChannels = obj.sendOutputChannels.slice(); + this.controlValues = ControlValue.deserializeArray(obj.controlValues); + return this; + } + getControlValue(symbol: string): number | null { + for (let control of this.controlValues) { + if (control.key === symbol) { + return control.value; + } + } + return null; + } + setControlValue(symbol: string, value: number): boolean { + for (let control of this.controlValues) { + if (control.key === symbol) { + if (control.value === value) { + return false; + } + control.value = value; + this.controlValues = this.controlValues.slice(); // trigger observers. + return true; + } + } + let newValue = new ControlValue(symbol,value); + this.controlValues.push(newValue); + this.controlValues = this.controlValues.slice(); // trigger observers. + return true; + } + +} \ No newline at end of file diff --git a/vite/src/pipedal/ChannelRouterSettingsDialog.tsx b/vite/src/pipedal/ChannelRouterSettingsDialog.tsx new file mode 100644 index 0000000..5fbb414 --- /dev/null +++ b/vite/src/pipedal/ChannelRouterSettingsDialog.tsx @@ -0,0 +1,853 @@ +// 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 Divider from '@mui/material/Divider'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import IconButtonEx from './IconButtonEx'; +import Button from '@mui/material/Button'; +import useWindowSize from "./UseWindowSize"; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import ChannelRouterSettingsHelpDialog from './ChannelRouterSettingsHelpDialog'; +import SpeakerIcon from '@mui/icons-material/Speaker'; +import MicIcon from '@mui/icons-material/Mic'; + +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import Typography from '@mui/material/Typography'; +import ChannelRouterSettings from './ChannelRouterSettings'; +import { Pedalboard, ControlValue } from './Pedalboard'; + +import DialogEx from './DialogEx'; + +import { PiPedalModelFactory } from './PiPedalModel'; + +export interface ChannelRouterSettingsDialogProps { + open: boolean; + onClose: () => void; +} + +enum RouteType { + Main, + Aux, + Send +} + +interface ChannelRouterSettingsPreset { + id: number; + name: string; + settings: ChannelRouterSettings; +} + +let LEFT_LEFT_PRESET_ID = -2; +let RIGHT_STEREO_PRESET_ID = -3; + +let channelRouterPresets: ChannelRouterSettingsPreset[] = [ + { + id: -2, name: "Left -> Left", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -2, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 0], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, -1], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, -1], + sendOutputChannels: [-1, -1], + controlValues: [], + }) + }, + { + id: -3, name: "Left -> Stereo", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -3, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, -1], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, -1], + sendOutputChannels: [-1, -1], + controlValues: [], + }) + }, + { + id: -4, name: "Right -> Stereo", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -4, + mainInputChannels: [1, 1], + mainOutputChannels: [0, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, -1], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, -1], + sendOutputChannels: [-1, -1], + controlValues: [], + }) + }, + { + id: -5, name: "Right -> Left + re-amp", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -5, + mainInputChannels: [1, 1], + mainOutputChannels: [0, 0], + mainInserts: new Pedalboard(), + auxInputChannels: [1, 1], + auxOutputChannels: [1, 1], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, - 1], + sendOutputChannels: [-1, -1], + controlValues: [], + }) + }, + { + id: -6, name: "Left -> Left + send", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -6, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 0], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, - 1], + auxInserts: new Pedalboard(), + sendInputChannels: [1, 1], + sendOutputChannels: [1, 1], + controlValues: [], + }) + }, + { + id: -7, name: "Right -> Right + send", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -7, + mainInputChannels: [1, 1], + mainOutputChannels: [1, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, - 1], + auxInserts: new Pedalboard(), + sendInputChannels: [0, 0], + sendOutputChannels: [0, 0], + controlValues: [], + }) + }, + { + id: -8, name: "Left -> Stereo + re-amp", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -8, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [0, 0], + auxOutputChannels: [2, 2], + auxInserts: new Pedalboard(), + sendInputChannels: [-1, -1], + sendOutputChannels: [-1, -1], + controlValues: [], + + }) + }, + { + id: -9, name: "Left -> Stereo + stereo send", settings: new ChannelRouterSettings().deserialize({ + configured: true, + channelRouterPresetId: -9, + mainInputChannels: [0, 0], + mainOutputChannels: [0, 1], + mainInserts: new Pedalboard(), + auxInputChannels: [-1, -1], + auxOutputChannels: [-1, - 1], + auxInserts: new Pedalboard(), + sendInputChannels: [2, 3], + sendOutputChannels: [2, 3], + controlValues: [], + + }) + }, +]; + +let cellPortraitInOutDiv: React.CSSProperties = { + width: 80, display: "flex", columnGap: 4, flexDirection: "row", alignItems: "center", justifyContent: "right", +}; +let cellPortraitSectionHead: React.CSSProperties = { + border: "0px", + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 0, + paddingRight: 12, + width: 50, + 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: 12, + margin: "0px", + textAlign: "left", + whiteSpace: "nowrap", + verticalAlign: "middle" +}; +let cellLandscapeTitle: React.CSSProperties = { + border: "0px", + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 4, + paddingRight: 20, + margin: "0px", + textAlign: "left", + verticalAlign: "middle" +}; +let cellPortraitInOut: React.CSSProperties = { + border: "0px", + paddingTop: 4, + paddingBottom: 4, + paddingLeft: 12, + paddingRight: 12, + margin: "0px", + width: 80, + textAlign: "right", + 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 ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { + //const classes = useStyles(); + const { open, onClose } = props; + let model = PiPedalModelFactory.getInstance(); + let config = model.jackConfiguration.get(); + + + const [settings, setSettings] = useState(model.channelRouterSettings.get()); + const [controlValues, setControlValues] = useState(model.channelRouterControlValues.get()); + const [showHelp, setShowHelp] = useState(false); + + if (settings) { + } + if (controlValues) { + + } + + + React.useEffect(() => { + if (open) { + let handleSettingsChanged = () => { + setSettings(model.channelRouterSettings.get()); + }; + model.channelRouterSettings.addOnChangedHandler(handleSettingsChanged); + let handleControlsChanged = () => { + setControlValues(model.channelRouterControlValues.get()); + }; + model.channelRouterControlValues.addOnChangedHandler(handleControlsChanged); + return () => { + model.channelRouterSettings.removeOnChangedHandler(handleSettingsChanged); + model.channelRouterControlValues.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 ChannelSelect = (routeType: RouteType, channelIndex: number, input: boolean, icon: boolean = true) => { + let channelCount = input ? config.inputAudioPorts.length : config.outputAudioPorts.length; + + let value: number; + + switch (routeType) { + case RouteType.Main: + value = input ? settings.mainInputChannels[channelIndex] : settings.mainOutputChannels[channelIndex]; + break; + case RouteType.Aux: + value = input ? settings.auxInputChannels[channelIndex] : settings.auxOutputChannels[channelIndex]; + break; + case RouteType.Send: + value = input ? settings.sendInputChannels[channelIndex] : settings.sendOutputChannels[channelIndex]; + break; + } + if (value >= channelCount) { + value = -1; + } + return ( + + ) + } + let Vu = () => { + return ( +
+
+ {/* */} +
+ ); + } + + let ApplyPresetId = (presetId: number | string | null | undefined) => { + if (typeof presetId !== 'number') { + return; + } + for (let preset of channelRouterPresets) { + if (preset.id === presetId) { + model.setChannelRouterSettings(preset.settings); + return; + } + } + } + + let CanUsePreset = (preset: ChannelRouterSettingsPreset): boolean => { + let inputChannels = config.inputAudioPorts.length; + let outputChannels = config.outputAudioPorts.length; + return ( + (preset.settings.mainInputChannels.every((ch) => ch < inputChannels || ch === -1)) && + (preset.settings.mainOutputChannels.every((ch) => ch < outputChannels || ch === -1)) && + (preset.settings.auxInputChannels.every((ch) => ch < inputChannels || ch === -1)) && + (preset.settings.auxOutputChannels.every((ch) => ch < outputChannels || ch === -1)) && + (preset.settings.sendInputChannels.every((ch) => ch < inputChannels || ch === -1)) && + (preset.settings.sendOutputChannels.every((ch) => ch < outputChannels || ch === -1)) + ); + } + + let GeneratePresetMenuItems = (): React.ReactElement[] => { + let items: React.ReactElement[] = []; + + for (let preset of channelRouterPresets) { + if (CanUsePreset(preset)) + items.push( + + {preset.name} + + ); + } + if (settings.channelRouterPresetId === -1) { + items.push( + ( + (Custom)) + ) + } + + return items; + } + + let PresetSelect = (width: number | string | undefined) => { + return ( +
+ + + { }} /> + +
+ ) + } + + + let LandscapeView = () => { + return ( +
+ + + + + + + + + + + + + + {/* Main */} + + + + + + + + + + + + + + + + {/* Spacer ---------------- */} + + + {/* Aux ----------------------*/} + + + + + + + + + + + + + + + + {/* Spacer ---------------- */} + + + {/* Send ---------------- */} + + + + + + + + + + + + + + + +
+ Main + + {ChannelSelect(RouteType.Main, 0, true)} + {Vu()} + + {Vu()} + {ChannelSelect(RouteType.Main, 0, false)} +
+ {(ChannelSelect(RouteType.Main, 1, true))} + + {(ChannelSelect(RouteType.Main, 1, false))} +
+ Aux + + {(ChannelSelect(RouteType.Aux, 0, true))} + {Vu()} + + {Vu()} + {(ChannelSelect(RouteType.Aux, 0, false))} +
+ {(ChannelSelect(RouteType.Aux, 1, true))} + + {(ChannelSelect(RouteType.Aux, 1, false))} +
+ Send + + {(ChannelSelect(RouteType.Send, 0, false))} + {Vu()} + {/* No button */} + {Vu()} + {(ChannelSelect(RouteType.Send, 0, true))} +
+ {(ChannelSelect(RouteType.Send, 1, false))} + + {(ChannelSelect(RouteType.Send, 1, true))} +
+
+ ); + }; + let PortraitView = () => { + return ( +
+ + + + + + + + + + {/* Main */} + + + + + + + + + + + + + + + + + + + + + + + {/* Spacer ---------------- */} + + {/* Aux */} + + + + + + + + + + + + + + + + + + + + + + {/* Spacer ---------------- */} + + + {/* Send */} + + + + + + + + + + + + + + + + + + + + + + +
+ Main + + +
+
+ In + +
+
+ {ChannelSelect(RouteType.Main, 0, true, false)} + + {ChannelSelect(RouteType.Main, 1, true, false)} +
+ + + + + + + + + + +
{Vu()} +
+ +
+
{Vu()}
+
+
+ Out + +
+
+ + {ChannelSelect(RouteType.Main, 0, false, false)} + + {ChannelSelect(RouteType.Main, 1, false, false)} +
+ Aux +
+
+ In + +
+
+ {(ChannelSelect(RouteType.Aux, 0, true, false))} + + {(ChannelSelect(RouteType.Aux, 1, true, false))} +
+ + + + + + + + + + +
{Vu()} +
+ +
+
{Vu()}
+
+
+ Out + +
+
+ {ChannelSelect(RouteType.Aux, 0, false, false)} + + {ChannelSelect(RouteType.Aux, 1, false, false)} +
+ Send +
+
+ Send + +
+
+ {(ChannelSelect(RouteType.Send, 0, false, false))} + + {(ChannelSelect(RouteType.Send, 1, false, false))} +
+ + + + + + + + + + +
{Vu()} +
+ +
+
{Vu()}
+
+
+ Return + +
+
+ {ChannelSelect(RouteType.Send, 0, true, false)} + + {ChannelSelect(RouteType.Send, 1, false, false)} +
+
+ ); + }; + + return ( + + +
+ { onClose(); }} + > + + + + + Channel Routing + + { + landscape ? ( + <> +
 
+
+ {PresetSelect(200)} +
+
 
+ + + ) : ( +
 
+ ) + } + + { setShowHelp(true); }} + > + + + +
+
+ {!landscape && ( + <> +
+ {PresetSelect("60%")} +
+ + + )} + + + {landscape ? + LandscapeView() + : + PortraitView() + } + + { + showHelp && ( + setShowHelp(false)} /> + ) + } +
+ ); +} + +export default ChannelRouterSettingsDialog; diff --git a/vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx b/vite/src/pipedal/ChannelRouterSettingsHelpDialog.tsx similarity index 80% rename from vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx rename to vite/src/pipedal/ChannelRouterSettingsHelpDialog.tsx index 345737b..f2d98c2 100644 --- a/vite/src/pipedal/ChannelMixerSettingsHelpDialog.tsx +++ b/vite/src/pipedal/ChannelRouterSettingsHelpDialog.tsx @@ -18,33 +18,30 @@ // 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 { +export interface ChannelRouterSettingsHelpDialogProps { open: boolean, onClose: () => void, }; -export interface ChannelMixerSettingsHelpDialogState { +export interface ChannelRouterSettingsHelpDialogState { fullScreen: boolean; }; -export default class ChannelMixerSettingsHelpDialog extends ResizeResponsiveComponent { +export default class ChannelRouterSettingsHelpDialog extends ResizeResponsiveComponent { refText: React.RefObject; - constructor(props: ChannelMixerSettingsHelpDialogProps) { + constructor(props: ChannelRouterSettingsHelpDialogProps) { super(props); this.state = { fullScreen: false @@ -137,6 +134,16 @@ export default class ChannelMixerSettingsHelpDialog extends ResizeResponsiveComp 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.

+

The Send route, in conjunction with the TooB Send plugin allows you to send and return audio signals to an external physical device + from within a PiPedal preset. This allows you to integrate external hardware effects into your PiPedal presets. + Connect a cable from the audio interface output Send channel(s) selected here to the input of your + hardware effect, and a cable from the output of your hardware effect to the audio interface's Return channel + assigned here. Then insert the TooB Send plugin into your PiPedal preset. The input of the + TooB Send plugin will be sent to the external hardware effect, and the output the external hardware + effect will be returned to PiPedal at the output of the TooB Send plugin. PiPedal currently supports + only one Send plugin instance per preset; and the Send and Return channels may not be shared with the Main + or Aux routes. +

diff --git a/vite/src/pipedal/ChannelMixerUiControl.tsx b/vite/src/pipedal/ChannelRouterUiControl.tsx similarity index 97% rename from vite/src/pipedal/ChannelMixerUiControl.tsx rename to vite/src/pipedal/ChannelRouterUiControl.tsx index 731a3f3..186e891 100644 --- a/vite/src/pipedal/ChannelMixerUiControl.tsx +++ b/vite/src/pipedal/ChannelRouterUiControl.tsx @@ -19,13 +19,12 @@ 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 const ChannelRouterUiUri = "uri://two-play/pipedal/ppChannelRouter"; export function makeChanelMixerUiPlugin(): UiPlugin { return new UiPlugin().deserialize({ - uri: ChannelMixerUiUri, + uri: ChannelRouterUiUri, name: "Channel Mixer", brand: "", label: "", diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index 31ae610..c96f0d0 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -357,14 +357,36 @@ export class PedalboardSplitItem extends PedalboardItem { } +let TWO_POWER_52 = 4503599627370496; // Maximum Javascript integer value. +let TWO_POWER_48 = 281474976710656; // Number of possible instance IDs for Main Pedalboards. +let MAX_INSTANCE_ID = TWO_POWER_48; +export enum InstanceType { + MainPedalboard, + MainInsert, + AuxInsert +} + + export class Pedalboard implements Deserializable { + static readonly CHANNEL_ROUTER_CONTROLS_INSTANCE_ID = -4; // Reserved Instance ID for Router Main Inserts. + static readonly CHANNEL_ROUTER_MAIN_INSERT_ID = -5; // Reserved Instance ID for Router Main Inserts. + static readonly CHANNEL_ROUTER_AUX_INSERT_ID = -6; // Reserved Instance ID for Router Aux inserts. + + static readonly MAIN_INSERT_INSTANCE_BASE = TWO_POWER_52-2*MAX_INSTANCE_ID; + static readonly AUX_INSERT_INSTANCE_BASE = TWO_POWER_52-1*MAX_INSTANCE_ID; + 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 MAIN_INSERT_START_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; + static readonly MAIN_INSERT_END_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + static readonly AUX_INSERT_START_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; + static readonly AUX_INSERT_END_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + + static readonly START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start"; static readonly END_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#End"; @@ -373,7 +395,7 @@ export class Pedalboard implements Deserializable { this.input_volume_db = input.input_volume_db; this.output_volume_db = input.output_volume_db; this.items = PedalboardItem.deserializeArray(input.items); - this.nextInstanceId = input.nextInstanceId ?? -1; + this.nextInstanceId = input.nextInstanceId ?? 0; this.snapshots = input.snapshots ? Snapshot.deserializeArray(input.snapshots): []; this.selectedSnapshot = input.selectedSnapshot; this.pathProperties = input.pathProperties; @@ -381,6 +403,25 @@ export class Pedalboard implements Deserializable { return this; } + static getInstanceTypeFromInstanceId(instanceId: number): InstanceType { + if (instanceId >= Pedalboard.MAIN_INSERT_INSTANCE_BASE) { + return InstanceType.MainInsert; + } + if (instanceId >= Pedalboard.AUX_INSERT_INSTANCE_BASE) { + return InstanceType.AuxInsert; + } + return InstanceType.MainPedalboard; + } + getInstanceType(): InstanceType { + if (this.nextInstanceId >= Pedalboard.MAIN_INSERT_INSTANCE_BASE) { + return InstanceType.MainInsert; + } + if (this.nextInstanceId >= Pedalboard.AUX_INSERT_INSTANCE_BASE) { + return InstanceType.AuxInsert; + } + return InstanceType.MainPedalboard; + } + clone(): Pedalboard { return new Pedalboard().deserialize(this); } diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index 083603e..569a2e3 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -46,7 +46,7 @@ import AudioFileMetadata from './AudioFileMetadata'; import { pathFileName } from './FileUtils'; import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer'; import { getDefaultModGuiPreference } from './ModGuiHost'; -import ChannelMixerSettings from './ChannelMixerSettings'; +import ChannelRouterSettings from './ChannelRouterSettings'; export enum State { Loading, @@ -530,13 +530,9 @@ export class PiPedalModel //implements PiPedalModel jackServerSettings: ObservableProperty = new ObservableProperty(new JackServerSettings()); - channelMixerControlValues: ObservableProperty = new ObservableProperty([]); - channelMixerSettings: ObservableProperty = new ObservableProperty(new ChannelMixerSettings()); + channelRouterControlValues: ObservableProperty = new ObservableProperty([]); + channelRouterSettings: ObservableProperty = new ObservableProperty(new ChannelRouterSettings()); - 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()); @@ -1255,6 +1251,11 @@ export class PiPedalModel //implements PiPedalModel await this.getWebSocket().request("getJackServerSettings") ) ); + this.channelRouterSettings.set( + new ChannelRouterSettings().deserialize( + await this.getWebSocket().request("getChannelRouterSettings") + ) + ) this.jackConfiguration.set(new JackConfiguration().deserialize( await this.getWebSocket().request("getJackConfiguration") )); @@ -1651,15 +1652,15 @@ 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); + private _setChannelRouterControlValue(key: string, value: number, notifyServer: boolean) : void { + let channelRouterSettings = this.channelRouterSettings.get(); + let changed = channelRouterSettings.setControlValue(key, value); if (changed) { - this.channelMixerControlValues.set(this.channelMixerSettings.get().controls); + this.channelRouterControlValues.set(this.channelRouterSettings.get().controlValues); if (notifyServer) { this.lastControlMessageWasSentbyMe = true; - this._setServerControl("setControl", Pedalboard.CHANNEL_MIXER_INSTANCE_ID, key, value); + this._setServerControl("setControl", Pedalboard.CHANNEL_ROUTER_CONTROLS_INSTANCE_ID, key, value); } } } @@ -1667,8 +1668,8 @@ export class PiPedalModel //implements PiPedalModel let pedalboard = this.pedalboard.get(); if (pedalboard === undefined) throw new PiPedalStateError("Pedalboard not ready."); - if (instanceId == Pedalboard.CHANNEL_MIXER_INSTANCE_ID) { - this._setChannelMixerControlValue(key,value,notifyServer); + if (instanceId == Pedalboard.CHANNEL_ROUTER_CONTROLS_INSTANCE_ID) { + this._setChannelRouterControlValue(key,value,notifyServer); return; } else { let changed: boolean; @@ -2316,12 +2317,6 @@ export class PiPedalModel //implements PiPedalModel } - setJackSettings(jackSettings: JackChannelSelection): void { - this.expectDisconnect(ReconnectReason.LoadingSettings); - this.webSocket?.send("setJackSettings", jackSettings); - } - - monitorPortSubscriptions: MonitorPortHandleImpl[] = []; monitorPort(instanceId: number, key: string, updateRateSeconds: number, onUpdated: (value: number) => void): MonitorPortHandle { @@ -3310,7 +3305,6 @@ export class PiPedalModel //implements PiPedalModel if (this.webSocket) { this.webSocket.send("setFavorites", newFavorites); } - // stub: update server. } @@ -3680,6 +3674,15 @@ export class PiPedalModel //implements PiPedalModel } this.removeInvalidSidechains(pedalboard); } + + setChannelRouterSettings(settings: ChannelRouterSettings) { + this.channelRouterSettings.set(settings); + this.channelRouterControlValues.set(settings.controlValues); + if (this.webSocket) { + this.webSocket.send("setChannelRouterSettings", settings); + } + } + }; let instance: PiPedalModel | undefined = undefined; diff --git a/vite/src/pipedal/SettingsDialog.tsx b/vite/src/pipedal/SettingsDialog.tsx index 7a47b71..dc579ee 100644 --- a/vite/src/pipedal/SettingsDialog.tsx +++ b/vite/src/pipedal/SettingsDialog.tsx @@ -20,8 +20,8 @@ import React, { SyntheticEvent, Component } from 'react'; import Switch from "@mui/material/Switch"; -import ChannelMixerSettings from './ChannelMixerSettings'; -import ChannelMixerSettingsDialog from './ChannelMixerSettingsDialog'; +import ChannelRouterSettings from './ChannelRouterSettings'; +import ChannelRouterSettingsDialog from './ChannelRouterSettingsDialog'; import OkCancelDialog from './OkCancelDialog'; import RadioSelectDialog from './RadioSelectDialog'; import IconButtonEx from './IconButtonEx'; @@ -78,7 +78,7 @@ interface SettingsDialogState { showStatusMonitorDialog: boolean; jackConfiguration: JackConfiguration; jackSettings: JackChannelSelection | null; - channelMixerSettings: ChannelMixerSettings | null; + channelRouterSettings: ChannelRouterSettings | null; jackServerSettings: JackServerSettings; alsaSequencerConfiguration: AlsaSequencerConfiguration; keepScreenOn: boolean; @@ -101,7 +101,7 @@ interface SettingsDialogState { showMidiSelectDialog: boolean; showThemeSelectDialog: boolean; showJackServerSettingsDialog: boolean; - showChannelMixerSettingsDialog: boolean; + showChannelRouterSettingsDialog: boolean; shuttingDown: boolean; restarting: boolean; isAndroidHosted: boolean; @@ -194,7 +194,7 @@ const SettingsDialog = withStyles( showStatusMonitorDialog: false, jackServerSettings: this.model.jackServerSettings.get(), - channelMixerSettings: this.model.channelMixerSettings.get(), + channelRouterSettings: this.model.channelRouterSettings.get(), jackConfiguration: this.model.jackConfiguration.get(), jackStatus: undefined, jackSettings: this.model.jackSettings.get(), @@ -215,7 +215,7 @@ const SettingsDialog = withStyles( showMidiSelectDialog: false, showThemeSelectDialog: false, showJackServerSettingsDialog: false, - showChannelMixerSettingsDialog: false, + showChannelRouterSettingsDialog: false, shuttingDown: false, restarting: false, showShutdownOkDialog: false, @@ -228,7 +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.handleChannelRouterSettingsChanged = this.handleChannelRouterSettingsChanged.bind(this); this.handleWifiConfigSettingsChanged = this.handleWifiConfigSettingsChanged.bind(this); this.handleWifiDirectConfigSettingsChanged = this.handleWifiDirectConfigSettingsChanged.bind(this); this.handleGovernorSettingsChanged = this.handleGovernorSettingsChanged.bind(this); @@ -330,10 +330,10 @@ const SettingsDialog = withStyles( continueDisabled: !this.model.jackServerSettings.get().valid }); } - handleChannelMixerSettingsChanged(): void { + handleChannelRouterSettingsChanged(): void { this.setState({ - channelMixerSettings: this.model.channelMixerSettings.get(), - continueDisabled: !this.model.channelMixerSettings.get().configured + channelRouterSettings: this.model.channelRouterSettings.get(), + continueDisabled: !this.model.channelRouterSettings.get().configured }); } @@ -378,7 +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.channelRouterSettings.addOnChangedHandler(this.handleChannelRouterSettingsChanged); this.model.wifiConfigSettings.addOnChangedHandler(this.handleWifiConfigSettingsChanged); this.model.wifiDirectConfigSettings.addOnChangedHandler(this.handleWifiDirectConfigSettingsChanged); this.model.governorSettings.addOnChangedHandler(this.handleGovernorSettingsChanged); @@ -401,7 +401,7 @@ const SettingsDialog = withStyles( this.handleJackSettingsChanged(); this.handleShowStatusMonitorChanged(); this.handleJackServerSettingsChanged(); - this.handleChannelMixerSettingsChanged(); + this.handleChannelRouterSettingsChanged(); this.handleWifiConfigSettingsChanged(); this.handleWifiDirectConfigSettingsChanged(); this.setState({ hasWifiDevice: this.model.hasWifiDevice.get() }); @@ -420,7 +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.channelRouterSettings.removeOnChangedHandler(this.handleChannelRouterSettingsChanged); this.model.wifiConfigSettings.removeOnChangedHandler(this.handleWifiConfigSettingsChanged); this.model.wifiDirectConfigSettings.removeOnChangedHandler(this.handleWifiDirectConfigSettingsChanged); this.model.governorSettings.removeOnChangedHandler(this.handleGovernorSettingsChanged); @@ -720,10 +720,10 @@ const SettingsDialog = withStyles( {this.state.jackServerSettings.getSummaryText()} - {this.state.showChannelMixerSettingsDialog && ( - this.setState({ showChannelMixerSettingsDialog: false })} + {this.state.showChannelRouterSettingsDialog && ( + this.setState({ showChannelRouterSettingsDialog: false })} /> )} {this.state.showJackServerSettingsDialog && ( @@ -744,7 +744,7 @@ const SettingsDialog = withStyles( { this.setState({ - showChannelMixerSettingsDialog: true + showChannelRouterSettingsDialog: true }); }} disabled={!hasAudioConfig} diff --git a/vite/src/pipedal/ToobParametricEqView.tsx b/vite/src/pipedal/ToobParametricEqView.tsx index 7bced53..8a6bb16 100644 --- a/vite/src/pipedal/ToobParametricEqView.tsx +++ b/vite/src/pipedal/ToobParametricEqView.tsx @@ -372,7 +372,7 @@ const ToobParametricEqView = ])} {panel("", [ controls.gain - ])} + ])} {divider()} {panel("Low", [controls.low_level, From 96cb9967aab87efa8ffc6d7335c3f753e22908a1 Mon Sep 17 00:00:00 2001 From: "Robin E.R. Davies" Date: Tue, 27 Jan 2026 17:14:56 -0500 Subject: [PATCH 5/6] Exclude .h and .a files in install. --- modules/CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index f9aee83..7570ab5 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -1,6 +1,9 @@ cmake_minimum_required(VERSION 3.16.0) -add_subdirectory("SQLiteCpp") +# Prevent installation of SQLite built components. (we use the .a library files directly) + +add_subdirectory("SQLiteCpp" EXCLUDE_FROM_ALL) + set(SDBUSCPP_BUILD_CODEGEN off) set(SDBUSCPP_INSTALL off) From 6c441ecfee6b35fb4d86d788ff6337c8de9dd24f Mon Sep 17 00:00:00 2001 From: "Robin E.R. Davies" Date: Tue, 27 Jan 2026 23:45:13 -0500 Subject: [PATCH 6/6] WIP Sync --- .vscode/c_cpp_properties.json | 1 - CMakeLists.txt | 19 +- src/AlsaDriver.cpp | 5 - src/AlsaDriver.hpp | 3 +- src/AlsaDriverTest.cpp | 203 ++++++++++-------- src/AudioHost.cpp | 8 +- src/Banks.hpp | 2 +- src/CMakeLists.txt | 12 +- src/ChannelRouterSettings.cpp | 4 +- src/ChannelRouterSettings.hpp | 40 ++-- src/DummyAudioDriver.hpp | 1 + src/Lv2Pedalboard.cpp | 43 +++- src/Lv2Pedalboard.hpp | 4 + src/Pedalboard.cpp | 10 +- src/Pedalboard.hpp | 21 +- src/PiLatencyMain.cpp | 28 ++- src/PiPedalModel.cpp | 22 +- src/PiPedalModel.hpp | 4 +- src/PiPedalSocket.cpp | 27 ++- src/RingBufferReader.hpp | 10 +- src/StateInterface.hpp | 3 +- src/Storage.cpp | 7 +- src/Storage.hpp | 2 +- vite/src/pipedal/ChannelRouterSettings.tsx | 131 ++++++++++- .../pipedal/ChannelRouterSettingsDialog.tsx | 21 +- vite/src/pipedal/Pedalboard.tsx | 8 +- vite/src/pipedal/PiPedalModel.tsx | 13 +- vite/src/pipedal/SettingsDialog.tsx | 90 ++++---- 28 files changed, 480 insertions(+), 262 deletions(-) diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index a709077..fac3cf6 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -13,7 +13,6 @@ "/usr/include/x86_64-linux-gnu", "/usr/lib" ], - "compilerPath": "/usr/bin/gcc-12", "cStandard": "c17", "cppStandard": "c++20", "intelliSenseMode": "linux-gcc-arm64", diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ed3d0b..bc7919d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,17 @@ set (DISPLAY_VERSION "PiPedal v1.5.99-Beta") set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set (CMAKE_INSTALL_PREFIX "/usr/") +set (LIBZIP_DO_INSTALL OFF CACHE BOOL "Disable libzip install" FORCE) +set (SQLITECPP_INSTALL OFF CACHE BOOL "Disable SQLiteCpp install" FORCE) + +set(SUBMODULE_INSTALL OFF CACHE BOOL "" FORCE) + +# Remove SQLiteCPP install files. +set(CPACK_SOURCE_IGNORE_FILES + "*.a;sqlite3.h;$(CPACK_SOURCE_IGNORE_FILES)" +) + + set (PIPEDAL_EXCLUDE_TESTS false) include(CTest) enable_testing() @@ -96,9 +107,9 @@ set(CPACK_DEBIAN_PACKAGE_SECTION sound) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE) -# Accept either libzip4 or libzip5 to satisfy libzip 4 linkage (because Ubuntu nonsense) -set(CPACK_DEBIAN_PACKAGE_DEPENDS "libicu-dev ,lv2-dev, ffmpeg, authbind, curl, gpg, alsa-base| pipewire, alsa-utils") + +set(CPACK_DEBIAN_PACKAGE_DEPENDS "libicu-dev ,lv2-dev,iw, ffmpeg, authbind, curl, gpg, alsa-base| pipewire, alsa-utils") set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE}) set(CPACK_PACKAGING_INSTALL_PREFIX /usr) set(CPACK_PROJECT_NAME ${PROJECT_NAME}) @@ -118,4 +129,8 @@ set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA ) + include(CPack) + +message(STATUS "CMAKE_INSTALL_PREFIX: ${CMAKE_INSTALL_PREFIX}") + diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index 92472d6..b75e52d 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -354,11 +354,6 @@ namespace pipedal std::vector outputBufferMixes; - - - std::vector deviceCaptureBuffers; - std::vector devicePlaybackBuffers; - std::vector rawCaptureBuffer; std::vector rawPlaybackBuffer; diff --git a/src/AlsaDriver.hpp b/src/AlsaDriver.hpp index 1b9ee0e..3d833a8 100644 --- a/src/AlsaDriver.hpp +++ b/src/AlsaDriver.hpp @@ -1,7 +1,7 @@ /* * MIT License * - * Copyright (c) 2025 Robin E. R. Davies + * Copyright (c) 2026 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 @@ -24,6 +24,7 @@ #pragma once +#include #include "AudioDriver.hpp" #include "JackServerSettings.hpp" diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp index 129fa37..fbe3a72 100644 --- a/src/AlsaDriverTest.cpp +++ b/src/AlsaDriverTest.cpp @@ -1,18 +1,18 @@ /* * 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 @@ -27,28 +27,34 @@ #include #include #include +#include #include "AlsaDriver.hpp" -#include "JackDriver.hpp" +#include "ChannelRouterSettings.hpp" using namespace pipedal; using namespace std; - -class AlsaTester: private AudioDriverHost { +class AlsaTester : private AudioDriverHost +{ public: - enum class TestType { Oscillator, LatencyMonitor, NullTest}; + enum class TestType + { + Oscillator, + LatencyMonitor, + NullTest + }; + private: AudioDriver *audioDriver = nullptr; TestType testType; public: AlsaTester(TestType testType) - : testType(testType) + : testType(testType) { // audioDriver = CreateAlsaDriver(this); // audioDriver = CreateJackDriver(this); - } ~AlsaTester() { @@ -57,47 +63,47 @@ public: delete[] outputBuffers; } - bool useJack = false; + void initializeChannelSelection(JackConfiguration &jackConfiguration, ChannelSelection &channelSelection) + { + size_t nInputs = std::max(jackConfiguration.inputAudioPorts().size(),size_t(2)); + size_t nOutputs = std::max(jackConfiguration.outputAudioPorts().size(),size_t(2)); + auto &inputSelections = channelSelection.mainInputChannels(); + auto &outputSelections = channelSelection.mainOutputChannels(); + inputSelections.resize(nInputs); + for (size_t i = 0; i < nInputs; ++i) + { + inputSelections[i] = i; + } + outputSelections.resize(nOutputs); + for (size_t i = 0; i < nOutputs; ++i) + { + outputSelections[i] = i; + } + + } void Test() { AlsaFormatEncodeDecodeTest(this); - JackServerSettings serverSettings("hw:M2","hw:M2",48000,32,3); + JackServerSettings serverSettings("hw:M2", "hw:M2", 48000, 32, 3); JackConfiguration jackConfiguration; - if (useJack) - { - jackConfiguration.JackInitialize(); - } else { - jackConfiguration.AlsaInitialize(serverSettings); - } + jackConfiguration.AlsaInitialize(serverSettings); - JackChannelSelection channelSelection( - jackConfiguration.inputAudioPorts(), - jackConfiguration.outputAudioPorts(), - jackConfiguration.inputMidiDevices()); + ChannelSelection channelSelection; -#if JACK_HOST - if (useJack) - { - audioDriver = CreateJackDriver(this); - - } else { - audioDriver = CreateAlsaDriver(this); - } -#else audioDriver = CreateAlsaDriver(this); -#endif + initializeChannelSelection(jackConfiguration, channelSelection); - oscillator.Init(440,jackConfiguration.sampleRate()); + oscillator.Init(440, jackConfiguration.sampleRate()); latencyMonitor.Init(jackConfiguration.sampleRate()); - audioDriver->Open(serverSettings,channelSelection); + audioDriver->Open(serverSettings, channelSelection); - inputBuffers = new float*[channelSelection.GetInputAudioPorts().size()]; - outputBuffers = new float*[channelSelection.GetOutputAudioPorts().size()]; + inputBuffers = new float *[channelSelection.mainInputChannels().size()]; + outputBuffers = new float *[channelSelection.mainOutputChannels().size()]; audioDriver->Activate(); @@ -107,7 +113,7 @@ public: if (testType == TestType::LatencyMonitor) { auto latency = this->latencyMonitor.GetLatency(); - double ms = 1000.0*latency/jackConfiguration.sampleRate(); + double ms = 1000.0 * latency / jackConfiguration.sampleRate(); cout << "Latency: " << latency << " samples " << ms << "ms" << " xruns: " << GetXruns() << " Cpu: " << audioDriver->CpuUse() << "%" << endl; } @@ -115,37 +121,40 @@ public: audioDriver->Deactivate(); audioDriver->Close(); - } - float**inputBuffers = nullptr; - float**outputBuffers = nullptr; + float **inputBuffers = nullptr; + float **outputBuffers = nullptr; - class Oscillator { + class Oscillator + { private: double dx = 0; double x = 0; double dx2 = 0; double x2 = 0; - public: + public: void Init(float frequency, size_t sampleRate) { - dx = frequency*3.141592736*2/sampleRate; - dx2 = 0.5*3.141592736*2/sampleRate; + dx = frequency * 3.141592736 * 2 / sampleRate; + dx2 = 0.5 * 3.141592736 * 2 / sampleRate; } - float Next() { + float Next() + { float result = (float)std::cos(x); float env = (float)std::cos(x2); x += dx; x2 += dx2; - return result*env; + return result * env; } }; - class LatencyMonitor { - enum class State { + class LatencyMonitor + { + enum class State + { Idle, Waiting, }; @@ -156,67 +165,74 @@ public: size_t current_latency = 0; size_t latency = 0; std::mutex sync; + public: void Init(uint64_t sampleRate) { - idle_samples = (uint64_t)sampleRate*2; - waiting_samples = (uint64_t)sampleRate*2; + idle_samples = (uint64_t)sampleRate * 2; + waiting_samples = (uint64_t)sampleRate * 2; state = State::Idle; t = idle_samples; latency = 0; } - size_t GetLatency() { - std::lock_guard lock { sync}; + size_t GetLatency() + { + std::lock_guard lock{sync}; return latency; } float Next(float input) { - switch(state) + switch (state) { - default: - case State::Idle: + default: + case State::Idle: + { + if (t-- == 0) { - if (t-- == 0) { - state = State::Waiting; - current_latency = 0; - } - return 0.01; + state = State::Waiting; + current_latency = 0; } - break; - case State::Waiting: + return 0.01; + } + break; + case State::Waiting: + { + if (std::abs(input) > 0.1 || current_latency > 500) { - if (std::abs(input) > 0.1 || current_latency > 500) { - { - std::lock_guard lock { sync}; - latency = current_latency; - } - state = State::Idle; - t = idle_samples; - } else { - ++current_latency; + { + std::lock_guard lock{sync}; + latency = current_latency; } - return current_latency < 100 ? 0.25 : 0.0; + state = State::Idle; + t = idle_samples; } - break; + else + { + ++current_latency; + } + return current_latency < 100 ? 0.25 : 0.0; + } + break; } } }; - Oscillator oscillator; LatencyMonitor latencyMonitor; - virtual void OnAudioTerminated() { - + virtual void OnAudioTerminated() + { } - virtual void OnAlsaDriverStopped() { + virtual void OnAlsaDriverStopped() + { } - virtual void OnProcess(size_t nFrames) { - if (testType == TestType::NullTest) return; - + virtual void OnProcess(size_t nFrames) + { + if (testType == TestType::NullTest) + return; size_t inputs = audioDriver->MainInputBufferCount(); size_t outputs = audioDriver->MainOutputBufferCount(); @@ -233,14 +249,15 @@ public: { for (size_t i = 0; i < nFrames; ++i) { - float v = oscillator.Next()*0.25f; + float v = oscillator.Next() * 0.25f; for (size_t c = 0; c < outputs; ++c) { outputBuffers[c][i] = v; } } } - else { + else + { for (size_t i = 0; i < nFrames; ++i) { float v = latencyMonitor.Next(inputBuffers[0][i]); @@ -249,36 +266,34 @@ public: outputBuffers[c][i] = v; } } - } - } std::mutex sync; uint64_t xruns; - uint64_t GetXruns() { - lock_guard lock { sync}; + uint64_t GetXruns() + { + lock_guard lock{sync}; return xruns; } - virtual void OnUnderrun() { - lock_guard lock { sync }; + virtual void OnUnderrun() + { + lock_guard lock{sync}; ++xruns; - } }; +TEST_CASE("alsa_test", "[alsa_test]") +{ -TEST_CASE( "alsa_test", "[alsa_test]" ) { - AlsaTester alsaDriver(AlsaTester::TestType::Oscillator); alsaDriver.Test(); } - -TEST_CASE( "alsa_midi_test", "[alsa_midi_test]" ) { +TEST_CASE("alsa_midi_test", "[alsa_midi_test]") +{ AlsaTester alsaDriver(AlsaTester::TestType::Oscillator); MidiDecoderTest(); } - diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index c1eb2ca..8a0f37c 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -1900,11 +1900,11 @@ public: PIPEDAL_NON_INLINE RealtimePedalboardItemIndex GetRealtimeItemIndex(int64_t instanceId) { - Pedalboard::InstanceType instanceType = Pedalboard::GetInstanceTypeFromInstanceId(instanceId); + Pedalboard::PedalboardType instanceType = Pedalboard::GetInstanceTypeFromInstanceId(instanceId); int64_t index = -1; switch (instanceType) { - case Pedalboard::InstanceType::MainPedalboard: + case Pedalboard::PedalboardType::MainPedalboard: if (this->currentPedalboard) { if (instanceId == Pedalboard::START_CONTROL_ID) { index = Pedalboard::START_CONTROL_ID; @@ -1915,7 +1915,7 @@ public: } } break; - case Pedalboard::InstanceType::MainInsert: + case Pedalboard::PedalboardType::MainInsert: if (this->currentMainInsertPedalboard) { if (instanceId == Pedalboard::MAIN_INSERT_START_CONTROL_ID) { index = Pedalboard::MAIN_INSERT_START_CONTROL_ID; @@ -1926,7 +1926,7 @@ public: } } break; - case Pedalboard::InstanceType::AuxInsert: + case Pedalboard::PedalboardType::AuxInsert: if (this->currentAuxInsertPedalboard) { if (instanceId == Pedalboard::AUX_INSERT_START_CONTROL_ID) { index = Pedalboard::START_CONTROL_ID; diff --git a/src/Banks.hpp b/src/Banks.hpp index 18951f3..cc24fce 100644 --- a/src/Banks.hpp +++ b/src/Banks.hpp @@ -240,7 +240,7 @@ namespace pipedal { // zero length? We can never have a zero-length bank. // Add a default preset and make it the selected preset. - Pedalboard pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); + Pedalboard pedalboard = Pedalboard::MakeDefault(Pedalboard::PedalboardType::MainPedalboard); this->addPreset(pedalboard); newSelection = presets_[0]->instanceId(); } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e8f0bae..2ed9781 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -352,8 +352,9 @@ target_compile_definitions(libpipedald PUBLIC "_REENTRANT") target_include_directories(libpipedald PUBLIC ${PIPEDAL_INCLUDES}) target_link_libraries(libpipedald - PUBLIC + PUBLIC PiPedalCommon + PRIVATE SQLiteCpp ${PipeWire_LIBRARIES} ) @@ -762,6 +763,7 @@ add_executable(pipedal_latency_test target_link_libraries(pipedal_latency_test PRIVATE pthread asound PiPedalCommon) +target_include_directories(pipedal_latency_test PRIVATE ${PIPEDAL_INCLUDES}) add_executable(pipedal_alsa_info alsaCheck.cpp alsaCheck.hpp @@ -859,10 +861,14 @@ add_custom_target ( ) + install (TARGETS pipedalconfig pipedal_kconfig pipedal_latency_test DESTINATION ${CMAKE_INSTALL_PREFIX}/bin - EXPORT pipedalTargets) + EXPORT pipedalTargets + ) install (TARGETS pipedald pipedaladmind pipedal_update DESTINATION ${CMAKE_INSTALL_PREFIX}/sbin - EXPORT pipedalSbinTargets) + EXPORT pipedalSbinTargets + ) + diff --git a/src/ChannelRouterSettings.cpp b/src/ChannelRouterSettings.cpp index 0b9e9be..9410e3b 100644 --- a/src/ChannelRouterSettings.cpp +++ b/src/ChannelRouterSettings.cpp @@ -129,8 +129,8 @@ void ChannelSelection::normalizeChannelSelection() { ChannelRouterSettings::ChannelRouterSettings() -: mainInserts_(Pedalboard::InstanceType::MainInsert) -, auxInserts_(Pedalboard::InstanceType::AuxInsert) +: mainInserts_(Pedalboard::PedalboardType::MainInsert) +, auxInserts_(Pedalboard::PedalboardType::AuxInsert) { } diff --git a/src/ChannelRouterSettings.hpp b/src/ChannelRouterSettings.hpp index 885b6e9..0c15f3d 100644 --- a/src/ChannelRouterSettings.hpp +++ b/src/ChannelRouterSettings.hpp @@ -49,7 +49,7 @@ namespace pipedal std::vector controlValues_; - public: + public: using self = ChannelRouterSettings; using ptr = std::shared_ptr; @@ -57,7 +57,7 @@ namespace pipedal uint64_t numberOfAudioInputChannels() const; uint64_t numberOfAudioOutputChannels() const; - + JSON_GETTER_SETTER(configured) JSON_GETTER_SETTER(channelRouterPresetId) JSON_GETTER_SETTER_REF(mainInputChannels) @@ -70,28 +70,34 @@ namespace pipedal JSON_GETTER_SETTER_REF(sendOutputChannels) JSON_GETTER_SETTER_REF(controlValues) - DECLARE_JSON_MAP(ChannelRouterSettings); - }; - // just the channel selecttions. - class ChannelSelection { + // just the channel selecttions. + class ChannelSelection + { public: ChannelSelection() = default; - ChannelSelection(ChannelRouterSettings&settings); - ChannelSelection(const ChannelSelection&other) = default; - ChannelSelection(ChannelSelection&&other) = default; - ChannelSelection& operator=(const ChannelSelection&other) = default; - ChannelSelection& operator=(ChannelSelection&&other) = default; + ChannelSelection(ChannelRouterSettings &settings); + ChannelSelection(const ChannelSelection &other) = default; + ChannelSelection(ChannelSelection &&other) = default; + ChannelSelection &operator=(const ChannelSelection &other) = default; + ChannelSelection &operator=(ChannelSelection &&other) = default; ~ChannelSelection() = default; - const std::vector&mainInputChannels() const { return mainInputChannels_; } - const std::vector&mainOutputChannels() const { return mainOutputChannels_; } - const std::vector&auxInputChannels() const { return auxInputChannels_; } - const std::vector&auxOutputChannels() const { return auxOutputChannels_; } - const std::vector&sendInputChannels() const { return sendInputChannels_; } - const std::vector&sendOutputChannels() const { return sendOutputChannels_; } + const std::vector &mainInputChannels() const { return mainInputChannels_; } + const std::vector &mainOutputChannels() const { return mainOutputChannels_; } + const std::vector &auxInputChannels() const { return auxInputChannels_; } + const std::vector &auxOutputChannels() const { return auxOutputChannels_; } + const std::vector &sendInputChannels() const { return sendInputChannels_; } + const std::vector &sendOutputChannels() const { return sendOutputChannels_; } + + std::vector &mainInputChannels() { return mainInputChannels_; } + std::vector &mainOutputChannels() { return mainOutputChannels_; } + std::vector &auxInputChannels() { return auxInputChannels_; } + std::vector &auxOutputChannels() { return auxOutputChannels_; } + std::vector &sendInputChannels() { return sendInputChannels_; } + std::vector &sendOutputChannels() { return sendOutputChannels_; } private: void normalizeChannelSelection(); diff --git a/src/DummyAudioDriver.hpp b/src/DummyAudioDriver.hpp index 35f2d31..59d5f09 100644 --- a/src/DummyAudioDriver.hpp +++ b/src/DummyAudioDriver.hpp @@ -24,6 +24,7 @@ #pragma once +#include #include "AudioDriver.hpp" #include "JackServerSettings.hpp" diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 34d0055..ad2b5cf 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -320,6 +320,7 @@ std::vector Lv2Pedalboard::PrepareItems( void Lv2Pedalboard::Prepare(IHost *pHost, Pedalboard &pedalboard, Lv2PedalboardErrorList &errorList, ExistingEffectMap *existingEffects) { this->pHost = pHost; + this->pedalboardType = pedalboard.GetInstanceType(); inputVolume.SetSampleRate((float)(this->pHost->GetSampleRate())); outputVolume.SetSampleRate((float)(this->pHost->GetSampleRate())); @@ -329,13 +330,15 @@ void Lv2Pedalboard::Prepare(IHost *pHost, Pedalboard &pedalboard, Lv2PedalboardE inputVolume.SetTarget(pedalboard.input_volume_db()); outputVolume.SetTarget(pedalboard.output_volume_db()); - for (int i = 0; i < pHost->GetNumberOfInputAudioChannels(); ++i) + size_t nInputs = std::max(GetNumberOfAudioInputChannels(),(size_t)1); + + for (size_t i = 0; i < nInputs; ++i) { this->pedalboardInputBuffers.push_back(bufferPool.AllocateBuffer(pHost->GetMaxAudioBufferSize())); } auto outputs = PrepareItems(pedalboard.items(), this->pedalboardInputBuffers, errorList, existingEffects); - int nOutputs = pHost->GetNumberOfOutputAudioChannels(); + size_t nOutputs = GetNumberOfAudioOutputChannels(); if (nOutputs == 1) { this->pedalboardOutputBuffers.push_back(outputs[0]); @@ -572,7 +575,12 @@ void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samp { for (size_t i = 0; i < vuConfiguration->enabledIndexes.size(); ++i) { - int index = vuConfiguration->enabledIndexes[i]; + auto& rtIndex = vuConfiguration->enabledIndexes[i]; + if (rtIndex.instanceType != this->pedalboardType) + { + continue; + } + int index = rtIndex.index; VuUpdate *pUpdate = &vuConfiguration->vuUpdateWorkingData[i]; if (index == Pedalboard::START_CONTROL_ID) { @@ -1016,3 +1024,32 @@ void Lv2Pedalboard::handleTapTempo(uint8_t value, const MidiTimestamp& timestamp mapping.lastTapTimestamp = timestamp; } } + + +size_t Lv2Pedalboard::GetNumberOfAudioInputChannels() const { + switch (pedalboardType) + { + case Pedalboard::PedalboardType::MainPedalboard: + return pHost->GetChannelSelection().mainInputChannels().size(); + case Pedalboard::PedalboardType::MainInsert: + return pHost->GetChannelSelection().mainOutputChannels().size(); + case Pedalboard::PedalboardType::AuxInsert: + return pHost->GetChannelSelection().auxInputChannels().size(); + default: + throw std::runtime_error("Invalid argument"); + } +} + +size_t Lv2Pedalboard::GetNumberOfAudioOutputChannels() const { + switch (pedalboardType) + { + case Pedalboard::PedalboardType::MainPedalboard: + return pHost->GetChannelSelection().mainOutputChannels().size(); + case Pedalboard::PedalboardType::MainInsert: + return pHost->GetChannelSelection().mainOutputChannels().size(); + case Pedalboard::PedalboardType::AuxInsert: + return pHost->GetChannelSelection().auxOutputChannels().size(); + default: + throw std::runtime_error("Invalid argument"); + } +} \ No newline at end of file diff --git a/src/Lv2Pedalboard.hpp b/src/Lv2Pedalboard.hpp index c893824..620c035 100644 --- a/src/Lv2Pedalboard.hpp +++ b/src/Lv2Pedalboard.hpp @@ -51,6 +51,7 @@ namespace pipedal class Lv2Pedalboard { private: + Pedalboard::PedalboardType pedalboardType; IHost *pHost = nullptr; size_t currentFrameOffset = 0; DbDezipper inputVolume; @@ -128,6 +129,9 @@ namespace pipedal std::vector &GetEffects() { return realtimeEffects; } std::vector> &GetSharedEffectList() { return effects; } + size_t GetNumberOfAudioInputChannels() const; + size_t GetNumberOfAudioOutputChannels() const; + int GetIndexOfInstanceId(uint64_t instanceId) { for (int i = 0; i < this->realtimeEffects.size(); ++i) diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 5cdc8fb..6a3f0fb 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -210,7 +210,7 @@ PedalboardItem Pedalboard::MakeSplit() -Pedalboard Pedalboard::MakeDefault(InstanceType instanceType) +Pedalboard Pedalboard::MakeDefault(PedalboardType instanceType) { // copy insanity. but it happens so rarely. Pedalboard result; @@ -510,17 +510,17 @@ Snapshot Pedalboard::MakeSnapshotFromCurrentSettings(const Pedalboard &previousP } -Pedalboard::Pedalboard(InstanceType instanceType = InstanceType::MainPedalboard) +Pedalboard::Pedalboard(PedalboardType instanceType) { switch (instanceType) { - case InstanceType::MainPedalboard: + case PedalboardType::MainPedalboard: nextInstanceId_ = 0; break; - case InstanceType::MainInsert: + case PedalboardType::MainInsert: nextInstanceId_ = MAIN_INSERT_INSTANCE_BASE;; break; - case InstanceType::AuxInsert: + case PedalboardType::AuxInsert: nextInstanceId_ = AUX_INSERT_INSTANCE_BASE;; break; } diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 9532efb..df7f425 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -230,13 +230,14 @@ class Pedalboard { int64_t selectedPlugin_ = -1; + static constexpr int64_t TWO_POWER_52 = 4503599627370496; // Maximum Javascript integer value. static constexpr int64_t TWO_POWER_48 = 281474976710656; // Number of possible instance IDs for Main Pedalboards. static constexpr int64_t MAX_INSTANCE_ID = TWO_POWER_48; public: - enum class InstanceType { + enum class PedalboardType { MainPedalboard, MainInsert, AuxInsert @@ -253,25 +254,25 @@ public: static constexpr int64_t AUX_INSERT_START_CONTROL_ID = AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; static constexpr int64_t AUX_INSERT_END_CONTROL_ID = AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; - static InstanceType GetInstanceTypeFromInstanceId(int64_t instanceId) { - if (instanceId == START_CONTROL_ID) return InstanceType::MainPedalboard; - if (instanceId == END_CONTROL_ID) return InstanceType::MainPedalboard; + static PedalboardType GetInstanceTypeFromInstanceId(int64_t instanceId) { + if (instanceId == START_CONTROL_ID) return PedalboardType::MainPedalboard; + if (instanceId == END_CONTROL_ID) return PedalboardType::MainPedalboard; if (instanceId >= MAIN_INSERT_INSTANCE_BASE) { - return InstanceType::MainInsert; + return PedalboardType::MainInsert; } else if (instanceId >= AUX_INSERT_INSTANCE_BASE) { - return InstanceType::AuxInsert; + return PedalboardType::AuxInsert; } else { - return InstanceType::MainPedalboard; + return PedalboardType::MainPedalboard; } } - InstanceType GetInstanceType() const { + PedalboardType GetInstanceType() const { return GetInstanceTypeFromInstanceId(this->nextInstanceId_); // instanceId is irrelevant here. } - Pedalboard(InstanceType instanceType = InstanceType::MainPedalboard); + Pedalboard(PedalboardType instanceType = PedalboardType::MainPedalboard); // deep copy, breaking shared pointers. Pedalboard DeepCopy(); @@ -307,7 +308,7 @@ public: PedalboardItem MakeSplit(); - static Pedalboard MakeDefault(Pedalboard::InstanceType instanceType); + static Pedalboard MakeDefault(Pedalboard::PedalboardType instanceType = Pedalboard::PedalboardType::MainPedalboard); }; #undef GETTER_SETTER_REF diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index edc6d4f..8c93f1c 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -34,6 +34,7 @@ #include #include #include +#include "ChannelRouterSettings.hpp" using namespace pipedal; @@ -169,19 +170,26 @@ public: delete[] inputBuffers; delete[] outputBuffers; } - std::vector SelectChannels(const std::vector &available, const std::vector &selection) + std::vector SelectChannels(const std::vector &available, const std::vector &selection) { + std::vector result; if (selection.size() == 0) - return available; + { + result.resize(available.size()); + for (size_t i = 0; i < result.size(); ++i) + { + result[i] = (int64_t)i; + } + return result; + } - std::vector result; for (int sel : selection) { if (sel < 0 || sel >= available.size()) { throw PiPedalArgumentException(SS("Invalid channel: " + sel)); } - result.push_back(available[sel]); + result.push_back(sel); } return result; } @@ -199,22 +207,22 @@ public: auto &availableInputs = jackConfiguration.inputAudioPorts(); auto &availableOutputs = jackConfiguration.outputAudioPorts(); - std::vector inputAudioPorts, outputAudioPorts; + std::vector inputAudioPorts, outputAudioPorts; inputAudioPorts = SelectChannels(availableInputs, this->inputChannels); outputAudioPorts = SelectChannels(availableOutputs, this->outputChannels); - JackChannelSelection channelSelection( - inputAudioPorts, outputAudioPorts, - std::vector()); + ChannelSelection channelSelection; + channelSelection.mainInputChannels() = inputAudioPorts; + channelSelection.mainOutputChannels() = outputAudioPorts; audioDriver = CreateAlsaDriver(this); latencyMonitor.Init(jackConfiguration.sampleRate()); audioDriver->Open(serverSettings, channelSelection); - inputBuffers = new float *[channelSelection.GetInputAudioPorts().size()]; - outputBuffers = new float *[channelSelection.GetOutputAudioPorts().size()]; + inputBuffers = new float *[channelSelection.mainInputChannels().size()]; + outputBuffers = new float *[channelSelection.mainOutputChannels().size()]; audioDriver->Activate(); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index f07b645..8262dae 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -81,7 +81,7 @@ PiPedalModel::PiPedalModel() this->updater = Updater::Create(); this->updater->Start(); this->currentUpdateStatus = updater->GetCurrentStatus(); - this->pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); + this->pedalboard = Pedalboard::MakeDefault(Pedalboard::PedalboardType::MainPedalboard); #if JACK_HOST this->jackServerSettings = this->storage.GetJackServerSettings(); // to get onboarding flag. this->jackServerSettings.ReadJackDaemonConfiguration(); @@ -216,14 +216,20 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) storage.Initialize(); pluginHost.SetPluginStoragePath(storage.GetPluginUploadDirectory()); - this->channelRouterSettings = storage.GetChannelRouterSettings(); this->systemMidiBindings = storage.GetSystemMidiBindings(); #if JACK_HOST this->jackConfiguration = this->jackConfiguration.JackInitialize(); #else this->jackServerSettings = storage.GetJackServerSettings(); + this->jackConfiguration.AlsaInitialize(jackServerSettings); #endif + + this->channelRouterSettings = storage.GetChannelRouterSettings(); + pluginHost.OnConfigurationChanged( + jackConfiguration, + storage.GetChannelSelection()); + } void PiPedalModel::LoadLv2PluginInfo() @@ -299,7 +305,7 @@ void PiPedalModel::Load() if (CrashGuard::HasCrashed()) { // ignore the current preset, and load a blank pedalboard in order to avoid a potential plugin crash. - this->pedalboard = Pedalboard::MakeDefault(Pedalboard::InstanceType::MainPedalboard); + this->pedalboard = Pedalboard::MakeDefault(Pedalboard::PedalboardType::MainPedalboard); } else { @@ -1429,7 +1435,7 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver) this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection); - FireChannelSelectionChanged(-1); + FireChannelRouterSettingsChanged(-1); LoadCurrentPedalboard(); this->UpdateRealtimeVuSubscriptions(); @@ -1526,17 +1532,17 @@ std::vector PiPedalModel::GetAlsaSequencerPorts() } -void PiPedalModel::FireChannelSelectionChanged(int64_t clientId) +void PiPedalModel::FireChannelRouterSettingsChanged(int64_t clientId) { std::lock_guard guard{mutex}; { // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) - JackChannelSelection channelSelection = storage.GetJackChannelSelection(this->jackConfiguration); + ChannelRouterSettings::ptr channelRouterSettings = this->channelRouterSettings; std::vector t{subscribers.begin(), subscribers.end()}; for (auto &subscriber : t) { - subscriber->OnChannelSelectionChanged(clientId, channelSelection); + subscriber->OnChannelRouterSettingsChanged(clientId, *channelRouterSettings); } } } @@ -3331,7 +3337,7 @@ void PiPedalModel::SetChannelRouterSettings(int64_t clientId,ChannelRouterSettin } RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications.. - this->FireChannelSelectionChanged(clientId); + this->FireChannelRouterSettingsChanged(clientId); } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 17f1cf9..3aab379 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -73,7 +73,7 @@ namespace pipedal virtual void OnSnapshotModified(int64_t selectedSnapshot, bool modified) = 0; virtual void OnSelectedSnapshotChanged(int64_t selectedSnapshot) = 0; virtual void OnPluginPresetsChanged(const std::string &pluginUri) = 0; - virtual void OnChannelSelectionChanged(int64_t clientId, const JackChannelSelection &channelSelection) = 0; + virtual void OnChannelRouterSettingsChanged(int64_t clientId, const ChannelRouterSettings &channelRouterSettings) = 0; virtual void OnVuMeterUpdate(const std::vector &updates) = 0; virtual void OnBankIndexChanged(const BankIndex &bankIndex) = 0; virtual void OnJackServerSettingsChanged(const JackServerSettings &jackServerSettings) = 0; @@ -206,7 +206,7 @@ namespace pipedal void FirePresetChanged(bool changed); void FirePluginPresetsChanged(const std::string &pluginUri); void FirePedalboardChanged(int64_t clientId, bool reloadAudioThread = true); - void FireChannelSelectionChanged(int64_t clientId); + void FireChannelRouterSettingsChanged(int64_t clientId); void FireBanksChanged(int64_t clientId); void FireJackConfigurationChanged(const JackConfiguration &jackConfiguration); void FireLv2StateChanged(int64_t instanceId, const Lv2PluginState &lv2State); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 85d0624..1028295 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -537,17 +537,24 @@ JSON_MAP_REFERENCE(SnapshotModifiedBody, snapshotIndex) JSON_MAP_REFERENCE(SnapshotModifiedBody, modified) JSON_MAP_END() -class ChannelSelectionChangedBody +class ChannelRouterSettingsChangedBody { public: - int64_t clientId_ = -1; - JackChannelSelection *jackChannelSelection_ = nullptr; + ChannelRouterSettingsChangedBody( + int64_t clientId, + const ChannelRouterSettings &channelRouterSettings + ): clientId_(clientId), channelRouterSettings_(channelRouterSettings) + { - DECLARE_JSON_MAP(ChannelSelectionChangedBody); + } + int64_t clientId_ = -1; + ChannelRouterSettings channelRouterSettings_; + + DECLARE_JSON_MAP(ChannelRouterSettingsChangedBody); }; -JSON_MAP_BEGIN(ChannelSelectionChangedBody) -JSON_MAP_REFERENCE(ChannelSelectionChangedBody, clientId) -JSON_MAP_REFERENCE(ChannelSelectionChangedBody, jackChannelSelection) +JSON_MAP_BEGIN(ChannelRouterSettingsChangedBody) +JSON_MAP_REFERENCE(ChannelRouterSettingsChangedBody, clientId) +JSON_MAP_REFERENCE(ChannelRouterSettingsChangedBody, channelRouterSettings) JSON_MAP_END() class PresetsChangedBody @@ -2000,11 +2007,9 @@ private: Send("onShowStatusMonitorChanged", show); } - virtual void OnChannelSelectionChanged(int64_t clientId, const JackChannelSelection &channelSelection) + virtual void OnChannelRouterSettingsChanged(int64_t clientId, const ChannelRouterSettings &channelRouterSettings) { - ChannelSelectionChangedBody body; - body.clientId_ = clientId; - body.jackChannelSelection_ = const_cast(&channelSelection); + ChannelRouterSettingsChangedBody body(clientId, channelRouterSettings); Send("onChannelSelectionChanged", body); } diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index 46f8c46..990b6fa 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -98,20 +98,20 @@ namespace pipedal struct RealtimePedalboardItemIndex { RealtimePedalboardItemIndex() - : instanceType(Pedalboard::InstanceType::MainPedalboard), index(-1) + : instanceType(Pedalboard::PedalboardType::MainPedalboard), index(-1) { } RealtimePedalboardItemIndex( - Pedalboard::InstanceType instanceType, - int index) + Pedalboard::PedalboardType instanceType, + int64_t index) : instanceType(instanceType), index(index) { } RealtimePedalboardItemIndex(const RealtimePedalboardItemIndex& other) = default; RealtimePedalboardItemIndex& operator=(const RealtimePedalboardItemIndex& other) = default; - Pedalboard::InstanceType instanceType = Pedalboard::InstanceType::MainPedalboard; - int index = -1; + Pedalboard::PedalboardType instanceType = Pedalboard::PedalboardType::MainPedalboard; + int64_t index = -1; }; struct RealtimeMidiEventRequest diff --git a/src/StateInterface.hpp b/src/StateInterface.hpp index c09cc33..311455d 100644 --- a/src/StateInterface.hpp +++ b/src/StateInterface.hpp @@ -19,13 +19,12 @@ #pragma once -#include "lilv/lilv.h" #include "lv2/state/state.h" #include #include "json_variant.hpp" #include "MapFeature.hpp" #include "IHost.hpp" -#include "lilv/lilv.h" +#include namespace pipedal { diff --git a/src/Storage.cpp b/src/Storage.cpp index 83a9524..3c726cf 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -377,7 +377,7 @@ void Storage::Initialize() LoadTone3000Auth(); try { - LoadChannelSelection(); + LoadJackChannelSelection(); } catch (const std::exception &) { @@ -476,7 +476,7 @@ void Storage::LoadBankIndex() if (bankIndex.entries().size() == 0) { currentBank.clear(); - Pedalboard defaultPedalboard = Pedalboard::MakeDefault(); + Pedalboard defaultPedalboard = Pedalboard::MakeDefault(Pedalboard::PedalboardType::MainPedalboard); int64_t instanceId = currentBank.addPreset(defaultPedalboard); currentBank.selectedPreset(instanceId); @@ -1135,7 +1135,7 @@ AlsaSequencerConfiguration Storage::GetAlsaSequencerConfiguration() const return this->alsaSequencerConfiguration; } -void Storage::LoadChannelSelection() +void Storage::LoadJackChannelSelection() { auto fileName = this->GetChannelSelectionFileName(); if (std::filesystem::exists(fileName)) @@ -3102,6 +3102,7 @@ ChannelRouterSettings::ptr Storage::LoadChannelRouterSettings() json_reader reader(is); ChannelRouterSettings::ptr result; reader.read(&result); + this->channelSelection = ChannelSelection(*result); return result; } catch (const std::exception &e) { Lv2Log::error("Failed to load Channel Router settings: %s", e.what()); diff --git a/src/Storage.hpp b/src/Storage.hpp index 76b4199..c2459dc 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -100,7 +100,7 @@ private: void LoadCurrentBank(); void SaveCurrentBank(); - void LoadChannelSelection(); + void LoadJackChannelSelection(); void SaveChannelSelection(); void LoadAlsaSequencerConfiguration(); diff --git a/vite/src/pipedal/ChannelRouterSettings.tsx b/vite/src/pipedal/ChannelRouterSettings.tsx index bea7ef7..26bc7d9 100644 --- a/vite/src/pipedal/ChannelRouterSettings.tsx +++ b/vite/src/pipedal/ChannelRouterSettings.tsx @@ -1,23 +1,61 @@ import { Pedalboard, ControlValue } from "./Pedalboard.tsx"; +import JackServerSettings from "./JackServerSettings.tsx"; +import JackConfiguration from "./Jack.tsx"; +function isActiveChannel(channels: number[]): boolean { + if (channels.length < 2) return false; + return channels[1] >= 0 || channels[0] >= 0; +} + +function chName(ch: number): string { + if (ch === -1) { + return "None"; + } + return "Ch " + (ch + 1); +} +function channelPairName(channels: number[], maxChannels: number): string { + if (channels.length !== 2) { + return "Invalid"; + } + if (channels[0] === -1 && channels[1] === -1) { + return "None"; + } + + if (maxChannels === 2) { + if (channels[0] === 0 && channels[1] === 1) { + return "Stereo"; + } + if (channels[0] === 0 && (channels[0] === channels[1] || channels[1] === -1)) { + return "Left"; + } + if (channels[1] === 1 && (channels[0] === channels[1])) { + return "Right"; + } + if (channels[0] === 1 && channels[1] === 0) { + return "Right,Left"; + } + } + return chName(channels[0]) + "," + chName(channels[1]); +} + export default class ChannelRouterSettings { configured: boolean = false; channelRouterPresetId: number = -1; - mainInputChannels: number[] = [1,1]; - mainOutputChannels: number[] = [0,1]; + mainInputChannels: number[] = [1, 1]; + mainOutputChannels: number[] = [0, 1]; mainInserts: Pedalboard = new Pedalboard(); - auxInputChannels: number[] = [0,0]; - auxOutputChannels: number[] = [-1,-1]; + auxInputChannels: number[] = [0, 0]; + auxOutputChannels: number[] = [-1, -1]; auxInserts: Pedalboard = new Pedalboard(); - sendInputChannels: number[] = [-1,-1]; - sendOutputChannels: number[] = [-1,-1]; + sendInputChannels: number[] = [-1, -1]; + sendOutputChannels: number[] = [-1, -1]; controlValues: ControlValue[] = []; // Inserts... @@ -55,10 +93,89 @@ export default class ChannelRouterSettings { return true; } } - let newValue = new ControlValue(symbol,value); + let newValue = new ControlValue(symbol, value); this.controlValues.push(newValue); this.controlValues = this.controlValues.slice(); // trigger observers. return true; } + getDescription(jackConfiguration: JackConfiguration): string { + if (!this.configured) { + return "Not configured"; + } + if (!jackConfiguration.isValid) + { + return "Not configured" + } + if (!this.isValid(jackConfiguration)) { + return "Invalid configuration"; + } + let nInputs = jackConfiguration.inputAudioPorts.length; + let nOutputs = jackConfiguration.outputAudioPorts.length; + let description = channelPairName(this.mainInputChannels, nInputs) + " -> " + + channelPairName(this.mainOutputChannels, nOutputs); + if (isActiveChannel(this.auxInputChannels) && isActiveChannel(this.auxOutputChannels)) + { + description += " " + "+ re-amp: " + channelPairName(this.auxInputChannels, nInputs) + " -> " + + channelPairName(this.auxOutputChannels, nOutputs); + } + if (isActiveChannel(this.sendInputChannels) && isActiveChannel(this.sendOutputChannels)) { + description += " " + "+ send: " +channelPairName(this.sendOutputChannels, nOutputs) + + " -> " + channelPairName(this.sendInputChannels, nInputs); + } + return description; + } + canEdit(jackConfiguration: JackConfiguration): boolean { + return jackConfiguration.isValid; + } + + isValid(jackConfiguration: JackConfiguration): boolean { + if (!this.configured) { + return false; + } + let maxInputChannels = jackConfiguration.inputAudioPorts.length; + let maxOutputChannels = jackConfiguration.outputAudioPorts.length; + + let hasInput = false; + let hasOutput = false; + for (let ch of this.mainInputChannels) { + if (ch >= 0) { + hasInput = true; + if (ch >= maxInputChannels) { + return false; + } + } + } + if (!hasInput) { + return false; + } + for (let ch of this.mainOutputChannels) { + if (ch >= 0) { + hasOutput = true; + if (ch >= maxOutputChannels) { + return false; + } + } + } + if (!hasOutput) { + return false; + } + for (let ch of this.auxInputChannels) { + if (ch >= maxInputChannels) { + return false; + } + } + for (let ch of this.auxOutputChannels) { + if (ch >= maxOutputChannels) { + return false; + } + } + for (let ch of this.sendInputChannels) { + if (ch >= maxInputChannels) { + return false; + } + } + return true; + } + } \ No newline at end of file diff --git a/vite/src/pipedal/ChannelRouterSettingsDialog.tsx b/vite/src/pipedal/ChannelRouterSettingsDialog.tsx index 5fbb414..b1b51d1 100644 --- a/vite/src/pipedal/ChannelRouterSettingsDialog.tsx +++ b/vite/src/pipedal/ChannelRouterSettingsDialog.tsx @@ -42,6 +42,11 @@ import DialogEx from './DialogEx'; import { PiPedalModelFactory } from './PiPedalModel'; + +let debugInputChannels: number | null = 2; +let debugOutputChannels: number | null = 4; + + export interface ChannelRouterSettingsDialogProps { open: boolean; onClose: () => void; @@ -309,6 +314,12 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { let ChannelSelect = (routeType: RouteType, channelIndex: number, input: boolean, icon: boolean = true) => { let channelCount = input ? config.inputAudioPorts.length : config.outputAudioPorts.length; + if (input && debugInputChannels != null) { + channelCount = debugInputChannels; + } + if (!input && debugOutputChannels != null) { + channelCount = debugOutputChannels; + } let value: number; @@ -336,7 +347,7 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { } onChange={(event) => { let newValue = event.target.value as number; - let newSettings = Object.assign(new ChannelRouterSettings(), settings); + let newSettings = new ChannelRouterSettings().deserialize(settings); switch (routeType) { case RouteType.Main: if (input) { @@ -385,7 +396,11 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { } for (let preset of channelRouterPresets) { if (preset.id === presetId) { - model.setChannelRouterSettings(preset.settings); + let newPreset = Object.assign(new ChannelRouterSettings(), preset.settings); + // mark the presets as inserts. + newPreset.mainInserts.nextInstanceId = Pedalboard.MAIN_INSERT_INSTANCE_BASE; + newPreset.auxInserts.nextInstanceId = Pedalboard.AUX_INSERT_INSTANCE_BASE; + model.setChannelRouterSettings(newPreset); return; } } @@ -759,7 +774,7 @@ function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { {ChannelSelect(RouteType.Send, 0, true, false)} - {ChannelSelect(RouteType.Send, 1, false, false)} + {ChannelSelect(RouteType.Send, 1, true, false)} diff --git a/vite/src/pipedal/Pedalboard.tsx b/vite/src/pipedal/Pedalboard.tsx index c96f0d0..423640f 100644 --- a/vite/src/pipedal/Pedalboard.tsx +++ b/vite/src/pipedal/Pedalboard.tsx @@ -381,10 +381,10 @@ 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 MAIN_INSERT_START_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; - static readonly MAIN_INSERT_END_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; - static readonly AUX_INSERT_START_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -2; - static readonly AUX_INSERT_END_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + MAX_INSTANCE_ID -3; + static readonly MAIN_INSERT_START_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + -2 + MAX_INSTANCE_ID; + static readonly MAIN_INSERT_END_CONTROL_ID = Pedalboard.MAIN_INSERT_INSTANCE_BASE + -3 + MAX_INSTANCE_ID; + static readonly AUX_INSERT_START_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + -2 + MAX_INSTANCE_ID + static readonly AUX_INSERT_END_CONTROL_ID = Pedalboard.AUX_INSERT_INSTANCE_BASE + -3 + MAX_INSTANCE_ID; static readonly START_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Start"; diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index 569a2e3..d764a5e 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -431,9 +431,9 @@ interface MonitorPortOutputBody { } -interface ChannelSelectionChangedBody { +interface ChannelRouterSettingsChangedBody { clientId: number; - jackChannelSelection: JackChannelSelection; + channelRouterSettings: ChannelRouterSettings; } interface RenamePresetBody { clientId: number; @@ -730,10 +730,11 @@ export class PiPedalModel //implements PiPedalModel } else if (message === "onShowStatusMonitorChanged") { let value = body as boolean; this.showStatusMonitor.set(value); - } else if (message === "onChannelSelectionChanged") { - let channelSelectionBody = body as ChannelSelectionChangedBody; - let channelSelection = new JackChannelSelection().deserialize(channelSelectionBody.jackChannelSelection); - this.jackSettings.set(channelSelection); + } else if (message === "onChannelRouterSettingsChanged") { + let channelRouterSettingChangedBody = body as ChannelRouterSettingsChangedBody; + let channelRouterSettings = new ChannelRouterSettings().deserialize( + channelRouterSettingChangedBody.channelRouterSettings); + this.channelRouterSettings.set(channelRouterSettings); } else if (message === "onSnapshotModified") { let { snapshotIndex, modified } = (body as { snapshotIndex: number, modified: boolean }); let snapshots = this.pedalboard.get().snapshots; diff --git a/vite/src/pipedal/SettingsDialog.tsx b/vite/src/pipedal/SettingsDialog.tsx index dc579ee..d008cfc 100644 --- a/vite/src/pipedal/SettingsDialog.tsx +++ b/vite/src/pipedal/SettingsDialog.tsx @@ -519,29 +519,10 @@ 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) { - newSelection.inputAudioPorts = channels; - } else { - newSelection.outputAudioPorts = channels; - } - this.setState({ - jackSettings: newSelection, - showInputSelectDialog: false, - showOutputSelectDialog: false - - }); - this.model.setJackSettings(newSelection); - } else { - this.setState({ - showInputSelectDialog: false, - showOutputSelectDialog: false - - }); - - } + this.setState({ + showInputSelectDialog: false, + showOutputSelectDialog: false + }); } midiSummary(): string { @@ -618,8 +599,10 @@ const SettingsDialog = withStyles( let disableShutdown = this.state.shuttingDown || this.state.restarting; let canKeepScreenOn = this.model.canKeepScreenOn; - let hasAudioConfig = isConfigValid && this.state.jackConfiguration.outputAudioPorts.length >= 1 + let hasAudioConfig = isConfigValid && this.state.jackConfiguration.inputAudioPorts.length >= 1 + && this.state.jackConfiguration.outputAudioPorts.length >= 1; + return (
Channel Routing - { - this.state.jackSettings == null ? "" : - this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)} + { + this.state.channelRouterSettings?.getDescription(this.state.jackConfiguration)??""}
+ {/* Old Input and Output selection + this.handleInputSelection()} + disabled={!isConfigValid || this.state.jackConfiguration.outputAudioPorts.length <= 1} + style={{ opacity: !isConfigValid ? 0.6 : 1.0 }} - this.handleInputSelection()} - disabled={!isConfigValid || this.state.jackConfiguration.outputAudioPorts.length <= 1} - style={{ opacity: !isConfigValid ? 0.6 : 1.0 }} - - > - -
- Input channels - { - this.state.jackSettings == null ? "" : - this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)} -
-
- this.handleOutputSelection()} - disabled={!isConfigValid || this.state.jackConfiguration.outputAudioPorts.length <= 1} - style={{ opacity: !isConfigValid ? 0.6 : 1.0 }} - > - -
- Output channels - { - this.state.jackSettings == null ? "" : - this.state.jackSettings.getAudioOutputDisplayValue(this.state.jackConfiguration)} -
-
+ > + +
+ Input channels + { + this.state.jackSettings == null ? "" : + this.state.jackSettings.getAudioInputDisplayValue(this.state.jackConfiguration)} +
+
+ this.handleOutputSelection()} + disabled={!isConfigValid || this.state.jackConfiguration.outputAudioPorts.length <= 1} + style={{ opacity: !isConfigValid ? 0.6 : 1.0 }} + > + +
+ Output channels + { + this.state.jackSettings == null ? "" : + this.state.jackSettings.getAudioOutputDisplayValue(this.state.jackConfiguration)} +
+
+ */}
MIDI