From 9d64cf1f92360e5c7d23c51f5206604d59a45a9a Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Thu, 5 Dec 2024 10:52:34 -0500 Subject: [PATCH] MIDI plugins -- partial implemetnation, temporarily disabled. --- PiPedalCommon/src/include/json.hpp | 26 +++ react/src/ChannelBindingsHelpDialog.tsx | 135 +++++++++++++ react/src/MidiChannelBinding.tsx | 51 +++++ react/src/MidiChannelBindingControl.tsx | 138 ++++++++++++++ react/src/MidiChannelBindingDialog.tsx | 240 ++++++++++++++++++++++++ react/src/Pedalboard.tsx | 8 + react/src/PluginControl.tsx | 12 +- react/src/PluginControlView.tsx | 131 ++++++++++--- react/src/PluginIcon.tsx | 1 - src/AudioHost.cpp | 5 +- src/Base64.hpp | 5 +- src/IEffect.hpp | 7 +- src/Lv2Effect.cpp | 8 +- src/Lv2Effect.hpp | 6 + src/Lv2Pedalboard.cpp | 42 +---- src/MidiBinding.cpp | 31 ++- src/MidiBinding.hpp | 88 +++++++++ src/Pedalboard.hpp | 2 + src/PiPedalModel.cpp | 12 ++ src/PluginHost.cpp | 7 +- src/PluginHost.hpp | 1 + src/SplitEffect.hpp | 5 + src/StateInterface.cpp | 2 +- src/vst3/Vst3EffectImpl.hpp | 13 ++ 24 files changed, 892 insertions(+), 84 deletions(-) create mode 100644 react/src/ChannelBindingsHelpDialog.tsx create mode 100644 react/src/MidiChannelBinding.tsx create mode 100644 react/src/MidiChannelBindingControl.tsx create mode 100644 react/src/MidiChannelBindingDialog.tsx diff --git a/PiPedalCommon/src/include/json.hpp b/PiPedalCommon/src/include/json.hpp index 7b0860a2..f47debae 100644 --- a/PiPedalCommon/src/include/json.hpp +++ b/PiPedalCommon/src/include/json.hpp @@ -34,6 +34,7 @@ #include #include #include +#include #define DECLARE_JSON_MAP(CLASSNAME) \ static pipedal::json_map::storage_type jmap @@ -592,6 +593,17 @@ namespace pipedal write(obj.get()); } } + template + void write(const std::optional &obj) + { + if (!obj) { + write_raw("null"); + } else { + write(obj.get()); + } + } + + template void write(const std::weak_ptr &obj) { @@ -883,6 +895,20 @@ namespace pipedal } } template + void read(std::optional *pOptional) + { + if (peek() == 'n') + { + consumeToken("null", "Expecting '{' or 'null'."); + *pOptional = std::optional(); + + } else { + T value; + read(&value); + *pOptional = std::move(value); + } + } + template void read(std::weak_ptr *pUniquePtr) { throw std::domain_error("Can't read std::weak_ptr"); diff --git a/react/src/ChannelBindingsHelpDialog.tsx b/react/src/ChannelBindingsHelpDialog.tsx new file mode 100644 index 00000000..66e10877 --- /dev/null +++ b/react/src/ChannelBindingsHelpDialog.tsx @@ -0,0 +1,135 @@ +// 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 TextFieldEx from './TextFieldEx'; + + +export interface ChannelBindingHelpDialogProps { + open: boolean, + onClose: () => void, +}; + +export interface ChannelBindingHelpDialogState { + fullScreen: boolean; +}; + +export default class ChannelBindingHelpDialog extends ResizeResponsiveComponent { + + refText: React.RefObject; + + constructor(props: ChannelBindingHelpDialogProps) { + 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 ( + {}} + > + + + MIDI Control Binding Priority + +
+

MIDI Channel Filters determine which MIDI messages get sent to a plugin that accepts MIDI messages. Note that the + MIDI Channel Filters do NOT affect system MIDI bindings or control bindings. +

+

+ MIDI message processing occurs in the following order +

+
    +
  • If the message is a Program Change message, the message is first offered to any MIDI plugins in the currently loaded preset (Message Filters permitting). + The message is sent to each MIDI plugin in the current preset that wants it. If any MIDI plugin accepts the program change, + no futher processing occurs. Specifically, the program change will not change the currently selected PiPedal preset. +
    +
  • +
  • + If the message is a Program Change message, and no MIDI plugin has accepted the message, PiPedal selects the PiPedal preset that corresponds + to the requested program. +
    +
  • +
  • + The message is then checked to see if it has been bound to a Pipedal feature using the System Midi Bindings settings. If it has, + Pipedal processes the message, and it is not forwarded to MIDI plugins. +
    +
  • +
  • + Otherwise, the message is sent to each MIDI plugin in the currently loaded Pipedal preset (channel filters permitting).) +
    +
  • +
+
+ +
+ + + + +
+ ); + } +} \ No newline at end of file diff --git a/react/src/MidiChannelBinding.tsx b/react/src/MidiChannelBinding.tsx new file mode 100644 index 00000000..76feeeb8 --- /dev/null +++ b/react/src/MidiChannelBinding.tsx @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +export enum MidiDeviceSelection { + DeviceAny = 0, + DeviceNone = 1, + DeviceList = 2 +} + +export default class MidiChannelBinding { + deserialize(input: any) : MidiChannelBinding { + this.deviceSelection = input.deviceSelection; + this.midiDevices = input.midiDevices; + + this.channel = input.channel; + this.acceptProgramChanges = input.acceptProgramChanges; + this.acceptCommonMessages = input.acceptCommonMessages; + return this; + } + + clone() { return new MidiChannelBinding().deserialize(this);} + + deviceSelection: number = MidiDeviceSelection.DeviceAny as number; + midiDevices: string[] = []; + channel: number = -1; + acceptProgramChanges: boolean = true; + acceptCommonMessages: boolean = true; + + + static CreateMissingValue() : MidiChannelBinding { + let result = new MidiChannelBinding(); + return result; + } +} diff --git a/react/src/MidiChannelBindingControl.tsx b/react/src/MidiChannelBindingControl.tsx new file mode 100644 index 00000000..85501a2d --- /dev/null +++ b/react/src/MidiChannelBindingControl.tsx @@ -0,0 +1,138 @@ +// Copyright (c) 2024 Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import React from 'react'; + +import { Component } from 'react'; +import { Theme } from '@mui/material/styles'; +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import { pluginControlStyles } from './PluginControl'; +import MidiChannelBinding from './MidiChannelBinding'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import ButtonBase from '@mui/material/ButtonBase'; +import { ReactComponent as MidiIcon } from "./svg/ic_midi.svg"; +import MidiChannelBindingDialog from './MidiChannelBindingDialog'; + + +export const StandardItemSize = { width: 80, height: 140 } + + + +export interface MidiChannelBindingControlProps extends WithStyles { + midiChannelBinding: MidiChannelBinding + theme: Theme; + onChange: (result: MidiChannelBinding) => void; +} +type MidiChannelBindingControlState = { + dialogOpen: boolean; +}; + +const MidiChannelBindingControl = + withStyles(pluginControlStyles, { withTheme: true })( + class extends Component { + model: PiPedalModel; + + constructor(props: MidiChannelBindingControlProps) { + super(props); + + this.state = { + dialogOpen: false + }; + this.model = PiPedalModelFactory.getInstance(); + + } + + render() { + let classes = this.props.classes; + let item_width = 80; + return ( +
+ {/* TITLE SECTION */} +
+ + title + + Controls which MIDI messages get forward to the instrument. + + + )} + placement="top-start" arrow enterDelay={1500} enterNextDelay={1500} + > + MIDI + + +
+ {/* CONTROL SECTION */} + +
+ { + this.setState({dialogOpen: true}); + }} + sx={{ + ':hover': { + bgcolor: 'action.hover', // theme.palette.primary.main + color: 'white', + }, + }} + > + + +
+
+ OMNI +
+ {this.state.dialogOpen&&( + { this.setState({dialogOpen: false});}} + onChanged={(midiChannelBinding: MidiChannelBinding)=> {}} + midiDevices={[]} + + /> + + )} +
+ ); + } + }); + +export default MidiChannelBindingControl; + diff --git a/react/src/MidiChannelBindingDialog.tsx b/react/src/MidiChannelBindingDialog.tsx new file mode 100644 index 00000000..f804697e --- /dev/null +++ b/react/src/MidiChannelBindingDialog.tsx @@ -0,0 +1,240 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// Copyright (c) 2024 Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import { useState } from 'react'; +import Button from '@mui/material/Button'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogActions from '@mui/material/DialogActions'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import ChannelBindingHelpDialog from './ChannelBindingsHelpDialog'; + +import IconButton from '@mui/material/IconButton'; +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; + +import Checkbox from '@mui/material/Checkbox'; +import { AlsaMidiDeviceInfo } from './AlsaMidiDeviceInfo'; +import DialogEx from './DialogEx'; +import MidiChannelBinding, { MidiDeviceSelection } from './MidiChannelBinding'; +import DialogContent from '@mui/material/DialogContent'; +import Typography from '@mui/material/Typography'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import { styled, Theme } from '@mui/material/styles'; +import createStyles from '@mui/styles/createStyles'; +import { makeStyles } from '@mui/styles'; + + +export interface MidiChannelBindingDialogProps { + open: boolean; + midiChannelBinding: MidiChannelBinding; + onClose: () => void; + onChanged: (midiChannelBinding: MidiChannelBinding) => void; + midiDevices: AlsaMidiDeviceInfo[]; +} + + +function isChecked(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo): boolean { + for (let i = 0; i < selectedChannels.length; ++i) { + if (selectedChannels[i].equals(channel)) return true; + } + return false; +} +function addPort(availableChannels: AlsaMidiDeviceInfo[], selectedChannels: AlsaMidiDeviceInfo[], newChannel: AlsaMidiDeviceInfo) + : AlsaMidiDeviceInfo[] { + let result: AlsaMidiDeviceInfo[] = []; + for (let i = 0; i < availableChannels.length; ++i) { + let channel = availableChannels[i]; + if (isChecked(selectedChannels, channel) || channel.equals(newChannel)) { + result.push(channel); + } + } + return result; +} + +function removePort(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo) + : AlsaMidiDeviceInfo[] { + let result: AlsaMidiDeviceInfo[] = []; + for (let i = 0; i < selectedChannels.length; ++i) { + if (!selectedChannels[i].equals(channel)) { + result.push(selectedChannels[i]); + } + } + return result; +} + +const usesStyles = makeStyles((theme: Theme) => + createStyles({ + icon: { + fill: theme.palette.text.primary, + opacity: 0.6 + } + }) +); + + +function MidiChannelBindingDialog(props: MidiChannelBindingDialogProps) { + const classes = usesStyles(); + const { onClose, midiChannelBinding, open, onChanged } = props; + + const [currentChannel, setCurrentChannel] = useState(midiChannelBinding.channel); + const [allowProgramChanges, setAllowProgramChanges] = useState(midiChannelBinding.acceptCommonMessages); + const [helpDialog, setHelpDialog] = useState(false); + + const model: PiPedalModel = PiPedalModelFactory.getInstance(); + + const handleClose = (): void => { + onClose(); + }; + const handleOk = (): void => { + onClose(); + }; + + const generateDeviceSelect = () => { + let result: React.ReactElement[] = [ + (Any), + (None) + ]; + let i = 2; + for (let midiDevice of model.jackConfiguration.get().inputMidiDevices) { + result.push(({midiDevice.description})); + ++i; + } + return result; + }; + + const getDefaultDeviceSelect = (): number => { + if (midiChannelBinding.deviceSelection === MidiDeviceSelection.DeviceAny as number) { + return 0; + } + if (midiChannelBinding.deviceSelection === MidiDeviceSelection.DeviceNone as number) { + return 1; + } + if (midiChannelBinding.midiDevices.length === 0) { + return 0; + } + let selectedDevice = midiChannelBinding.midiDevices[0]; + + let ix = 2; + for (let midiDevice of model.jackConfiguration.get().inputMidiDevices) { + if (midiDevice.name === selectedDevice) { + return ix; + } + ++ix; + } + return 0; // any. + } + + const [currentDeviceSelect, setCurrentDeviceSelect] = useState(getDefaultDeviceSelect()) + + + const handleDeviceSelecChange = (event: SelectChangeEvent) => { + setCurrentDeviceSelect(parseInt(event.target.value)); + }; + const handleChannelChange = (event: SelectChangeEvent) => { + setCurrentChannel(parseInt(event.target.value)); + }; + + return ( + {}} fullWidth maxWidth="xs" + > + MIDI Channel Filters + +
+
+ MIDI Devices + +
+ +
+ Channels + +
+ +
+ { + setAllowProgramChanges(event.target.checked); + }} + /> + } label="Allow Program Changes" + /> + {false&&( // wait until the implementation stabilizes before exposing the help dialog. + { setHelpDialog(true); }} + size="large"> + + + )} +
+ +
+ +
+ + + + + + setHelpDialog(false)} + /> +
+ ); +} + +export default MidiChannelBindingDialog; diff --git a/react/src/Pedalboard.tsx b/react/src/Pedalboard.tsx index f1d6faf2..ce7ad123 100644 --- a/react/src/Pedalboard.tsx +++ b/react/src/Pedalboard.tsx @@ -19,6 +19,7 @@ import { PiPedalArgumentError } from './PiPedalError'; import MidiBinding from './MidiBinding'; +import MidiChannelBinding from './MidiChannelBinding'; const SPLIT_PEDALBOARD_ITEM_URI = "uri://two-play/pipedal/pedalboard#Split"; @@ -64,6 +65,12 @@ export class PedalboardItem implements Deserializable { this.pluginName = input.pluginName; this.isEnabled = input.isEnabled; this.midiBindings = MidiBinding.deserialize_array(input.midiBindings); + if (input.midiChannelBinding) + { + this.midiChannelBinding = input.midiChannelBinding; + } else { + this.midiChannelBinding = null; + } this.controlValues = ControlValue.deserializeArray(input.controlValues); this.vstState = input.vstState ?? ""; @@ -197,6 +204,7 @@ export class PedalboardItem implements Deserializable { pluginName?: string; controlValues: ControlValue[] = ControlValue.EmptyArray; midiBindings: MidiBinding[] = []; + midiChannelBinding: MidiChannelBinding | null = null; vstState: string = ""; stateUpdateCount: number = 0; lv2State: [boolean,any] = [false,{}]; diff --git a/react/src/PluginControl.tsx b/react/src/PluginControl.tsx index 2404b2df..abdd3b82 100644 --- a/react/src/PluginControl.tsx +++ b/react/src/PluginControl.tsx @@ -52,7 +52,7 @@ export const StandardItemSize = { width: 80, height: 140 } -const styles = (theme: Theme) => createStyles({ +export const pluginControlStyles = (theme: Theme) => createStyles({ frame: { position: "relative", margin: "12px" @@ -90,13 +90,13 @@ const styles = (theme: Theme) => createStyles({ flex: "1 1 1", display: "flex",flexFlow: "column nowrap",alignContent: "center",justifyContent: "center" }, editSection: { - flex: "0 0 0", position: "relative", width: 60, height: 28,minHeight: 28 + flex: "0 0 0", display: "flex", flexFlow: "column nowrap", justifyContent: "center",position: "relative", width: 60, height: 28,minHeight: 28 } }); -export interface PluginControlProps extends WithStyles { +export interface PluginControlProps extends WithStyles { uiControl?: UiControl; instanceId: number; value: number; @@ -110,7 +110,7 @@ type PluginControlState = { }; const PluginControl = - withStyles(styles, { withTheme: true })( + withStyles(pluginControlStyles, { withTheme: true })( class extends Component { frameRef: React.RefObject; @@ -802,8 +802,8 @@ const PluginControl = {(!(isSelect || isOnOffSwitch || isTrigger)) && ( (isAbSwitch) ? ( - {switchText} ) : (
diff --git a/react/src/PluginControlView.tsx b/react/src/PluginControlView.tsx index ae471314..0fc2f446 100644 --- a/react/src/PluginControlView.tsx +++ b/react/src/PluginControlView.tsx @@ -41,6 +41,8 @@ import PluginOutputControl from './PluginOutputControl'; import Units from './Units'; import ToobFrequencyResponseView from './ToobFrequencyResponseView'; import Tooltip from '@mui/material/Tooltip'; +import MidiChannelBindingControl from './MidiChannelBindingControl'; +import MidiChannelBinding from './MidiChannelBinding'; export const StandardItemSize = { width: 80, height: 110 }; @@ -90,35 +92,57 @@ const styles = (theme: Theme) => createStyles({ paddingTop: "8px", paddingBottom: "0px", height: "100%", + overflowX: "hidden", + overflowY: "hidden" + }, + frameScrollLandscape: { + display: "block", + position: "absolute", + left: 0, top: 0, right: 0, bottom:0, + flexDirection: "row", + flexWrap: "nowrap", + paddingTop: "0px", + paddingBottom: "0px", overflowX: "auto", + overflowY: "hidden" + }, + frameScrollPortrait: { + display: "block", + position: "absolute", + left: 0, top: 0, right: 0, bottom:0, + flexDirection: "row", + flexWrap: "nowrap", + paddingTop: "0px", + paddingBottom: "0px", + overflowX: "hidden", overflowY: "auto" }, vuMeterL: { - position: "fixed", + position: "absolute", + left: 0, top: 0, paddingLeft: 6, paddingRight: 4, - paddingBottom: 24, - left: 0, + paddingBottom: 12, // cover bottom line of a portgroup in landscape. background: theme.mainBackground, zIndex: 3 }, vuMeterR: { - position: "fixed", - right: 0, - marginRight: 22, + position: "absolute", + right: 0, top: 0, + marginRight: 20, // has to potentially clear a scrollbar. paddingLeft: 4, - paddingBottom: 24, + paddingBottom: 12, background: theme.mainBackground, zIndex: 3 }, vuMeterRLandscape: { - position: "fixed", - right: 0, - paddingRight: 22, + position: "absolute", + right: 0, top: 0, + paddingRight: 6, paddingLeft: 12, - paddingBottom: 24, + paddingBottom: 12, // cover bottom line of a portgroup in landscape. background: theme.mainBackground, zIndex: 3 @@ -126,21 +150,28 @@ const styles = (theme: Theme) => createStyles({ normalGrid: { position: "relative", - paddingLeft: 25, - paddingRight: 34, + paddingLeft: 30, + paddingRight: 30, + paddingTop: 8, flex: "1 1 auto", display: "flex", flexDirection: "row", flexWrap: "wrap", justifyContent: "flex-start", alignItems: "flex_start", - rowGap: 10 + rowGap: 14, + height: "fit-content" + }, landscapeGrid: { - paddingLeft: 40, - // marginRight: 40, : bug in chrome layout engine wrt/ right margin/padding. See the spacer div added after all controls in render() with provides the same effect. + paddingLeft: 40, paddingRight: 40, paddingTop: 8, + // marginRight: 40, : bug in chrome layout engine wrt/ right margin/padding. + // See the spacer div added after all controls in render() with provides the same effect. display: "flex", flexDirection: "row", flexWrap: "nowrap", - justifyContent: "flex-start", alignItems: "flex_start", - flex: "0 0 auto" + justifyContent: "flex-start", alignItems: "flex-start", + overflowX: "hidden", + overflowY: "hidden", + flex: "0 0 auto", + width: "fit-content" }, portgroupControlPadding: { flex: "0 0 auto", @@ -198,10 +229,12 @@ const styles = (theme: Theme) => createStyles({ border: "2pt #AAA solid", borderRadius: 8, elevation: 12, - display: "flex", + display: "inline-flex", textOverflow: "ellipsis", flexDirection: "row", flexWrap: "nowrap", - flex: "0 0 auto" + flex: "0 0 auto", + width: "fit-content", + minWidth: "max-content" }, portGroupTitle: { position: "absolute", @@ -219,6 +252,13 @@ const styles = (theme: Theme) => createStyles({ flexDirection: "row", flexWrap: "wrap", paddingTop: 6, paddingBottom: 8 + }, + portGroupControlsLandscape: { + display: "flex", + flexFlow: "row nowrap", + width: "fit-content", + paddingTop: 6, + paddingBottom: 8 } @@ -600,7 +640,8 @@ const PluginControlView = {controlGroup.name}
-
+
{ controls } @@ -626,6 +667,20 @@ const PluginControlView = static endPluginInfo: UiPlugin = makeIoPluginInfo("Output", Pedalboard.END_PEDALBOARD_ITEM_URI); + midiBindingControl(pedalboardItem: PedalboardItem): ReactNode { + if (!pedalboardItem.midiChannelBinding) { + return false; + } + return ( + { + + }} + /> + ) + } + + render(): ReactNode { this.controlKeyIndex = 0; @@ -663,6 +718,7 @@ const PluginControlView = let gridClass = this.state.landscapeGrid ? classes.landscapeGrid : classes.normalGrid; + let scrollClass = this.state.landscapeGrid ? classes.frameScrollLandscape : classes.frameScrollPortrait; let vuMeterRClass = this.state.landscapeGrid ? classes.vuMeterRLandscape : classes.vuMeterR; let controlNodes: ControlNodes; @@ -675,6 +731,16 @@ const PluginControlView = let nodes = this.controlNodesToNodes(controlNodes); + if (plugin.has_midi_input && !pedalboardItem.midiChannelBinding) + { + pedalboardItem.midiChannelBinding = MidiChannelBinding.CreateMissingValue(); + + } + if (pedalboardItem.midiChannelBinding) { + nodes.push(this.midiBindingControl(pedalboardItem)); + } + + return (
@@ -683,16 +749,19 @@ const PluginControlView =
-
- { - nodes - } -
- { - (!this.state.landscapeGrid) && ( -
- ) - } +
+
+ { + nodes + } + {/* Extra space to allow scrolling right to the end in lascape especially */} +
+ { + (!this.state.landscapeGrid) && ( +
+ ) + } +
{this.state.showFileDialog && ( diff --git a/react/src/PluginIcon.tsx b/react/src/PluginIcon.tsx index 5e6e6c0c..99f550ba 100644 --- a/react/src/PluginIcon.tsx +++ b/react/src/PluginIcon.tsx @@ -65,7 +65,6 @@ import { ReactComponent as FxEmptyIcon } from './svg/fx_empty.svg'; import { ReactComponent as FxTerminalIcon } from './svg/fx_terminal.svg'; import {isDarkMode} from './DarkMode'; -import { Color } from '@mui/material'; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 89af2c68..ded2b98d 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -1862,8 +1862,9 @@ class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchW VuUpdate v; v.instanceId_ = instanceId; // Display mono VUs if a stereo device is being fed identical L/R inputs. - v.isStereoInput_ = effect->GetNumberOfInputAudioPorts() != 1 && effect->GetAudioInputBuffer(0) != effect->GetAudioInputBuffer(1); - v.isStereoOutput_ = effect->GetNumberOfOutputAudioPorts() != 1; + v.isStereoInput_ = effect->GetNumberOfInputAudioBuffers() >= 2 + && effect->GetAudioInputBuffer(0) != effect->GetAudioInputBuffer(1); + v.isStereoOutput_ = effect->GetNumberOfOutputAudioBuffers() >= 2; vuConfig->vuUpdateWorkingData.push_back(v); vuConfig->vuUpdateResponseData.push_back(v); diff --git a/src/Base64.hpp b/src/Base64.hpp index 77d3f1ac..e546fd6b 100644 --- a/src/Base64.hpp +++ b/src/Base64.hpp @@ -41,7 +41,7 @@ namespace macaron public: static std::string Encode(const std::vector &data) { - return Encode(data.size(),&(data[0])); + return Encode(data.size(),data.data()); } static std::string Encode(size_t size, const uint8_t *data) { @@ -112,6 +112,9 @@ namespace macaron if (in_len % 4 != 0) throw std::invalid_argument("Input data size is not a multiple of 4"); + if (in_len < 4) { + return std::vector(); + } size_t out_len = in_len / 4 * 3; if (input[in_len - 1] == '=') out_len--; diff --git a/src/IEffect.hpp b/src/IEffect.hpp index d502d012..b5a4ce79 100644 --- a/src/IEffect.hpp +++ b/src/IEffect.hpp @@ -49,8 +49,11 @@ namespace pipedal { virtual void SetBypass(bool enable) = 0; virtual float GetOutputControlValue(int controlIndex) const = 0; - virtual int GetNumberOfInputAudioPorts() const = 0; - virtual int GetNumberOfOutputAudioPorts() const = 0; + virtual int GetNumberOfInputAudioPorts() const = 0; // as declared + virtual int GetNumberOfOutputAudioPorts() const = 0; // as declared. + + virtual int GetNumberOfInputAudioBuffers() const = 0; // may be different if plugin has zero inputs. + virtual int GetNumberOfOutputAudioBuffers() const = 0; // may be different if plugin has zero inputs. virtual float *GetAudioInputBuffer(int index) const = 0; virtual float *GetAudioOutputBuffer(int index) const = 0; virtual void ResetAtomBuffers() = 0; diff --git a/src/Lv2Effect.cpp b/src/Lv2Effect.cpp index 75a86c46..248e985f 100644 --- a/src/Lv2Effect.cpp +++ b/src/Lv2Effect.cpp @@ -414,12 +414,12 @@ void Lv2Effect::SetAudioInputBuffer(int index, float *buffer) void Lv2Effect::SetAudioInputBuffer(float *left) { - if (GetNumberOfInputAudioPorts() > 1) + if (GetNumberOfInputAudioBuffers() > 1) { SetAudioInputBuffer(0, left); SetAudioInputBuffer(1, left); } - else if (GetNumberOfInputAudioPorts() != 0) /// yyx MIXING! + else if (GetNumberOfInputAudioBuffers() != 0) { SetAudioInputBuffer(0, left); } @@ -427,11 +427,11 @@ void Lv2Effect::SetAudioInputBuffer(float *left) void Lv2Effect::SetAudioInputBuffers(float *left, float *right) { - if (GetNumberOfInputAudioPorts() == 1) + if (GetNumberOfInputAudioBuffers() == 1) { SetAudioInputBuffer(0, left); } - else if (GetNumberOfInputAudioPorts() > 1) + else if (GetNumberOfInputAudioBuffers() > 1) { SetAudioInputBuffer(0, left); SetAudioInputBuffer(1, right); diff --git a/src/Lv2Effect.hpp b/src/Lv2Effect.hpp index 3a7abed1..fdc2419c 100644 --- a/src/Lv2Effect.hpp +++ b/src/Lv2Effect.hpp @@ -252,16 +252,22 @@ namespace pipedal virtual uint64_t GetInstanceId() const { return instanceId; } virtual int GetNumberOfInputAudioPorts() const { return inputAudioPortIndices.size(); } virtual int GetNumberOfOutputAudioPorts() const { return outputAudioPortIndices.size(); } + virtual int GetNumberOfInputAtomPorts() const { return inputAtomPortIndices.size(); } + virtual int GetNumberOfOutputAtomPorts() const { return outputAtomPortIndices.size(); } virtual int GetNumberOfMidiInputPorts() const { return inputMidiPortIndices.size(); } virtual int GetNumberOfMidiOutputPorts() const { return outputMidiPortIndices.size(); } + virtual int GetNumberOfInputAudioBuffers() const { return this->inputAudioBuffers.size(); } + virtual int GetNumberOfOutputAudioBuffers() const {return this->outputAudioBuffers.size(); } + virtual void SetAudioInputBuffer(int index, float *buffer); virtual float *GetAudioInputBuffer(int index) const { return this->inputAudioBuffers[index]; } + virtual void SetAudioInputBuffer(float *buffer); virtual void SetAudioInputBuffers(float *left, float *right); diff --git a/src/Lv2Pedalboard.cpp b/src/Lv2Pedalboard.cpp index 32f685ba..01ffb6cc 100644 --- a/src/Lv2Pedalboard.cpp +++ b/src/Lv2Pedalboard.cpp @@ -136,11 +136,11 @@ std::vector Lv2Pedalboard::PrepareItems( if (inputBuffers.size() == 1) { - if (pLv2Effect->GetNumberOfInputAudioPorts() == 1) + if (pLv2Effect->GetNumberOfInputAudioBuffers() == 1) { pLv2Effect->SetAudioInputBuffer(0, inputBuffers[0]); } - else + else if (pLv2Effect->GetNumberOfInputAudioBuffers() >= 2) { pLv2Effect->SetAudioInputBuffer(0, inputBuffers[0]); pLv2Effect->SetAudioInputBuffer(1, inputBuffers[0]); @@ -148,13 +148,13 @@ std::vector Lv2Pedalboard::PrepareItems( } else { - if (pLv2Effect->GetNumberOfInputAudioPorts() == 1) + if (pLv2Effect->GetNumberOfInputAudioBuffers() == 1) { pLv2Effect->SetAudioInputBuffer(0, inputBuffers[0]); auto inputBuffer = inputBuffers[0]; } - else + else if (pLv2Effect->GetNumberOfInputAudioBuffers() >= 2) { pLv2Effect->SetAudioInputBuffer(0, inputBuffers[0]); pLv2Effect->SetAudioInputBuffer(1, inputBuffers[1]); @@ -178,37 +178,15 @@ std::vector Lv2Pedalboard::PrepareItems( std::vector effectOutput; -#ifdef RECYCLE_AUDIO_BUFFERS - // can't do this anymore if we're going to do pop-less bypbass. - if (pEffect->GetNumberOfOutputAudioPorts() == 1) - { - float *pLeft = inputBuffers[0]; - effectOutput.push_back(pLeft); - } - else - { - if (inputBuffers.size() == 1) - { - effectOutput.push_back(inputBuffers[0]); - effectOutput.push_back(CreateNewAudioBuffer()); - } - else - { - effectOutput.push_back(inputBuffers[0]); - effectOutput.push_back(inputBuffers[1]); - } - } -#else - if (pEffect->GetNumberOfOutputAudioPorts() == 1) + if (pEffect->GetNumberOfOutputAudioBuffers() == 1) { effectOutput.push_back(CreateNewAudioBuffer()); } - else + else if (pEffect->GetNumberOfOutputAudioBuffers() >= 2) { effectOutput.push_back(CreateNewAudioBuffer()); effectOutput.push_back(CreateNewAudioBuffer()); } -#endif for (size_t i = 0; i < effectOutput.size(); ++i) { pEffect->SetAudioOutputBuffer(i, effectOutput[i]); @@ -475,21 +453,21 @@ void Lv2Pedalboard::ComputeVus(RealtimeVuBuffers *vuConfiguration, uint32_t samp { auto effect = this->realtimeEffects[index]; - if (effect->GetNumberOfInputAudioPorts() == 1) + if (effect->GetNumberOfInputAudioBuffers() == 1) { pUpdate->AccumulateInputs(effect->GetAudioInputBuffer(0), samples); } - else + else if (effect->GetNumberOfInputAudioBuffers() >= 2) { pUpdate->AccumulateInputs( effect->GetAudioInputBuffer(0), effect->GetAudioInputBuffer(1), samples); } - if (effect->GetNumberOfOutputAudioPorts() == 1) + if (effect->GetNumberOfOutputAudioBuffers() == 1) { pUpdate->AccumulateOutputs(effect->GetAudioOutputBuffer(0), samples); } - else + else if (effect->GetNumberOfOutputAudioBuffers() >= 2) { pUpdate->AccumulateOutputs( effect->GetAudioOutputBuffer(0), diff --git a/src/MidiBinding.cpp b/src/MidiBinding.cpp index c234e4a5..cf29f1ba 100644 --- a/src/MidiBinding.cpp +++ b/src/MidiBinding.cpp @@ -17,12 +17,41 @@ // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -#include "pch.h" #include "MidiBinding.hpp" +#include "MapFeature.hpp" using namespace pipedal; +MidiChannelBinding MidiChannelBinding::DefaultForMissingValue() +{ + MidiChannelBinding result {}; + + return result; +} + + +void MidiChannelBinding::Prepare(MapFeature &mapFeature) +{ + midiDeviceUrids.resize(0); + midiDeviceUrids.reserve(midiDevices_.size()); + + for (const auto&midiDevice: midiDevices_) + { + midiDeviceUrids.push_back(mapFeature.GetUrid(midiDevice.c_str())); + } +} + + +JSON_MAP_BEGIN(MidiChannelBinding) + JSON_MAP_REFERENCE(MidiChannelBinding,deviceSelection) + JSON_MAP_REFERENCE(MidiChannelBinding,midiDevices) + JSON_MAP_REFERENCE(MidiChannelBinding,channel) + JSON_MAP_REFERENCE(MidiChannelBinding,acceptProgramChanges) + JSON_MAP_REFERENCE(MidiChannelBinding,acceptCommonMessages) +JSON_MAP_END() + + JSON_MAP_BEGIN(MidiBinding) JSON_MAP_REFERENCE(MidiBinding,channel) JSON_MAP_REFERENCE(MidiBinding,symbol) diff --git a/src/MidiBinding.hpp b/src/MidiBinding.hpp index 6aa0256e..0fd03385 100644 --- a/src/MidiBinding.hpp +++ b/src/MidiBinding.hpp @@ -20,10 +20,16 @@ #pragma once #include "json.hpp" +#include +#include +#include +#include + namespace pipedal { +class MapFeature; #define GETTER_SETTER_REF(name) \ const decltype(name##_)& name() const { return name##_;} \ @@ -33,6 +39,9 @@ namespace pipedal { decltype(name##_)& name() { return name##_;} \ const decltype(name##_)& name() const { return name##_;} + //void name(decltype(const name##_) &value) { name##_ = value; } + //void name(decltype(const name##_) &&value) { name##_ = std::move(value); } + #define GETTER_SETTER(name) \ @@ -102,6 +111,85 @@ class MidiBinding { }; + +enum class MidiDeviceSelection { + DeviceAny = 0, + DeviceNone = 1, + DeviceList = 2 +}; + +class MidiChannelBinding { +public: + static constexpr std::int32_t CHANNEL_OMNI = -1; +private: + int32_t deviceSelection_ = (int32_t)(MidiDeviceSelection::DeviceAny); + + std::vector midiDevices_; + + std::vector midiDeviceUrids; + + std::int32_t channel_ = CHANNEL_OMNI; + bool acceptProgramChanges_ = true; + bool acceptCommonMessages_ = true; +public: + MidiDeviceSelection deviceSelection() const { return (MidiDeviceSelection)deviceSelection_; } + void deviceSelection(MidiDeviceSelection value) { deviceSelection_ = (int32_t)value; } + GETTER_SETTER_VEC(midiDevices); + GETTER_SETTER(channel); + GETTER_SETTER(acceptProgramChanges); + GETTER_SETTER(acceptCommonMessages); + + void Prepare(MapFeature &mapFeature); + + static MidiChannelBinding DefaultForMissingValue(); + + bool WantsMidiMessage(uint8_t midi_cc0,uint8_t midi_cc1, uint8_t midi_cc2); + bool WantProgramChange(uint8_t midi_cc0,uint8_t midi_cc1); + + // must call Prepare first. + bool WantsDevice(LV2_URID deviceUrid); + + + DECLARE_JSON_MAP(MidiChannelBinding); + +}; +/////////////////////////////////////// + +inline bool MidiChannelBinding::WantsMidiMessage(uint8_t midi_cc0,uint8_t midi_cc1, uint8_t midi_cc2) +{ + if (midi_cc0 < 0xF0) + { + if (channel_ < 0) return true; + + return midi_cc0 & 0x0F == channel_; + } else { + return acceptCommonMessages_; + } +} + +inline bool MidiChannelBinding::WantProgramChange(uint8_t midi_cc0,uint8_t midi_cc1) +{ + if (!acceptProgramChanges_) + { + return false; + } + if (channel_ < 0) return true; + return midi_cc0 & 0x0F == channel_; +} + +inline bool MidiChannelBinding::WantsDevice(LV2_URID deviceUrid) +{ + if (this->midiDeviceUrids.size() == 0) return true; + for (LV2_URID urid: midiDeviceUrids) + { + if (urid == deviceUrid) return true; + } + return false; +} + +////////////////////////////////////////// + + #undef GETTER_SETTER_REF #undef GETTER_SETTER_VEC #undef GETTER_SETTER diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 9ae3565f..867ddc37 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -92,6 +92,7 @@ class PedalboardItem: public JsonMemberWritable { std::vector topChain_; std::vector bottomChain_; std::vector midiBindings_; + std::optional midiChannelBinding_; std::string vstState_; uint32_t stateUpdateCount_ = 0; Lv2PluginState lv2State_; @@ -122,6 +123,7 @@ class PedalboardItem: public JsonMemberWritable { GETTER_SETTER_VEC(topChain) GETTER_SETTER_VEC(bottomChain) GETTER_SETTER_VEC(midiBindings) + GETTER_SETTER_REF(midiChannelBinding) GETTER_SETTER(stateUpdateCount) GETTER_SETTER_REF(lv2State) Lv2PluginState&lv2State() { return lv2State_; } // non-const version. diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index ad0e6e5b..79b81fba 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -1876,6 +1876,18 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered } if (pPlugin) { + if (pPlugin->hasMidiInput()) + { + if (!pedalboardItem->midiChannelBinding()) + { + pedalboardItem->midiChannelBinding(MidiChannelBinding::DefaultForMissingValue()); + } + } else { + if (pedalboardItem->midiChannelBinding()) + { + pedalboardItem->midiChannelBinding(std::optional()); // clear it. + } + } for (size_t i = 0; i < pPlugin->ports().size(); ++i) { auto port = pPlugin->ports()[i]; diff --git a/src/PluginHost.cpp b/src/PluginHost.cpp index 29cc1f85..03e3876d 100644 --- a/src/PluginHost.cpp +++ b/src/PluginHost.cpp @@ -495,6 +495,10 @@ void PluginHost::Load(const char *lv2Path) { Lv2Log::debug("Plugin %s (%s) skipped. No audio i/o.", plugin->name().c_str(), plugin->uri().c_str()); + } else if (info.audio_inputs() == 0) + { + // temporarily disable this feature. + Lv2Log::debug("Plugin %s (%s) skipped. No inputs.", plugin->name().c_str(), plugin->uri().c_str()); } #elif SUPPORT_MIDI if (info.audio_inputs() == 0 && !info.has_midi_input()) @@ -520,9 +524,6 @@ void PluginHost::Load(const char *lv2Path) if (info.audio_inputs() == 0) { Lv2Log::debug("************* ZERO INPUTS: %s (%s) skipped. No audio outputs.", plugin->name().c_str(), plugin->uri().c_str()); } - if (info.audio_outputs() == 0) { - Lv2Log::debug("************* ZERO OUTPUTS: %s (%s) skipped. No audio outputs.", plugin->name().c_str(), plugin->uri().c_str()); - } ui_plugins_.push_back(std::move(info)); } } diff --git a/src/PluginHost.hpp b/src/PluginHost.hpp index c8697652..35ef4152 100644 --- a/src/PluginHost.hpp +++ b/src/PluginHost.hpp @@ -432,6 +432,7 @@ namespace pipedal return true; } } + return false; } bool hasMidiOutput() const { diff --git a/src/SplitEffect.hpp b/src/SplitEffect.hpp index b3065708..6ed253b1 100644 --- a/src/SplitEffect.hpp +++ b/src/SplitEffect.hpp @@ -381,10 +381,15 @@ namespace pipedal { return (int)this->inputs.size(); } + virtual int GetNumberOfInputAudioBuffers() const { return this->inputs.size(); } + virtual float *GetAudioInputBuffer(int index) const { return inputs[index]; } + + virtual int GetNumberOfOutputAudioBuffers() const {return this->outputBuffers.size(); } + virtual float *GetAudioOutputBuffer(int index) const { return this->outputBuffers[index]; diff --git a/src/StateInterface.cpp b/src/StateInterface.cpp index ecf8cdbc..83fe2130 100644 --- a/src/StateInterface.cpp +++ b/src/StateInterface.cpp @@ -199,7 +199,7 @@ const void *StateInterface::StateRetrieveFunction( *size = entry.value_.size(); *type = map.GetUrid(entry.atomType_.c_str()); *flags = entry.flags_; - return (void*)&entry.value_[0]; + return entry.value_.data(); } diff --git a/src/vst3/Vst3EffectImpl.hpp b/src/vst3/Vst3EffectImpl.hpp index dc9469fb..e910290a 100644 --- a/src/vst3/Vst3EffectImpl.hpp +++ b/src/vst3/Vst3EffectImpl.hpp @@ -126,8 +126,21 @@ namespace pipedal { return info.pluginInfo_.audio_outputs(); } + + virtual int GetNumberOfInputAudioBuffers() const { return buffers.inputs.size(); } + virtual int GetNumberOfOutputAudioPorts() const {return buffers.outputs.size(); } + virtual float *GetAudioInputBuffer(int index) const { return buffers.inputs[index]; } virtual float *GetAudioOutputBuffer(int index) const { return buffers.outputs[index]; } + + virtual int GetNumberOfInputAudioBuffers() const { + return buffers.inputs.size(); + } + virtual int GetNumberOfInputAudioBuffers() const { + return buffers.outputs.size(); + } + + virtual void ResetAtomBuffers() {} virtual void RequestParameter(LV2_URID uridUri) {} // no vst equivalent. virtual void GatherPatchProperties(RealtimePatchPropertyRequest *pRequest) {} // no vst equivalent.