// 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 VuMeter from './VuMeter'; 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 SaveIconOutline from '@mui/icons-material/Save'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import Typography from '@mui/material/Typography'; import ChannelRouterSettings, { MAIN_OUT_LEFT_CHANNEL, MAIN_OUT_RIGHT_CHANNEL } from './ChannelRouterSettings'; import { Pedalboard } from './Pedalboard'; import DialogEx from './DialogEx'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { JackConfiguration } from './Jack'; let debugInputChannels: number | null = null; let debugOutputChannels: number | null = null; export interface ChannelRouterSettingsDialogProps { open: boolean; onClose: () => void; } enum RouteType { Main, Aux, Send } interface ChannelRouterSettingsPreset { id: number; name: string; settings: ChannelRouterSettings; } let DEFAULT_MONO_TO_MONO_ROUTING_PRESET = -2; // corresponds to Input->Output. let DEFAULT_LEFT_TO_STEREO_ROUTING_PRESET = -3; // corresponds to Input->Stereo out. let DEFAULT_RIGHT_TO_STEREO_ROUTING_PRESET = -4; // corresponds to Right->Stereo out. let factoryRouterPresets: ChannelRouterSettings[] = [ // Input->Output new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -2, mainInputChannels: [0, -1], mainOutputChannels: [0, -1], mainInserts: new Pedalboard(), auxInputChannels: [-1, -1], auxOutputChannels: [-1, -1], auxInserts: new Pedalboard(), sendInputChannels: [-1, -1], sendOutputChannels: [-1, -1], controlValues: [] }) , // Input->Stereo out. new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -3, mainInputChannels: [0, -1], mainOutputChannels: [0, 1], mainInserts: new Pedalboard(), auxInputChannels: [-1, -1], auxOutputChannels: [-1, -1], auxInserts: new Pedalboard(), sendInputChannels: [-1, -1], sendOutputChannels: [-1, -1], controlValues: [] }) , // Right->Stereo out. 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: [], }) , new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -5, 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: [], }) , new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -6, mainInputChannels: [1,-1], mainOutputChannels: [1,-1], mainInserts: new Pedalboard(), auxInputChannels: [0, -1], auxOutputChannels: [0, -1], auxInserts: new Pedalboard(), sendInputChannels: [-1, -1], sendOutputChannels: [-1, -1], controlValues: [], }) , new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -7, mainInputChannels: [1,-1], mainOutputChannels: [0, -1], mainInserts: new Pedalboard(), auxInputChannels: [0, -1], auxOutputChannels: [1,-1], auxInserts: new Pedalboard(), sendInputChannels: [-1, -1], sendOutputChannels: [-1, -1], controlValues: [] }) , new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -8, mainInputChannels: [0, -1], mainOutputChannels: [0, -1], mainInserts: new Pedalboard(), auxInputChannels: [1,-1], auxOutputChannels: [1,-1], auxInserts: new Pedalboard(), sendInputChannels: [-1, -1], sendOutputChannels: [-1, -1], controlValues: [] }) , new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -9, mainInputChannels: [0, -1], mainOutputChannels: [0, -1], mainInserts: new Pedalboard(), auxInputChannels: [-1, -1], auxOutputChannels: [-1, -1], auxInserts: new Pedalboard(), sendInputChannels: [1,-1], sendOutputChannels: [1,-1], controlValues: [] }) , new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -10, mainInputChannels: [1,-1], mainOutputChannels: [1,-1], mainInserts: new Pedalboard(), auxInputChannels: [-1, -1], auxOutputChannels: [-1, -1], auxInserts: new Pedalboard(), sendInputChannels: [0, -1], sendOutputChannels: [0, -1], controlValues: [] }) , new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -11, mainInputChannels: [0, -1], mainOutputChannels: [0, 1], mainInserts: new Pedalboard(), auxInputChannels: [0, -1], auxOutputChannels: [2, -1], auxInserts: new Pedalboard(), sendInputChannels: [-1, -1], sendOutputChannels: [-1, -1], controlValues: [] }) , new ChannelRouterSettings().deserialize({ configured: true, channelRouterPresetId: -12, mainInputChannels: [0, -1], 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 normalizeChannelSelection(channelSelection: number[]) { if (channelSelection.length === 2) { if (channelSelection[0] === -1) { channelSelection[0] = channelSelection[1]; channelSelection[1] = -1; } else { if (channelSelection[0] === channelSelection[1]) { channelSelection[1] = -1; } } } } function makeRoutingPresets(audioConfig: JackConfiguration): ChannelRouterSettingsPreset[] { let result: ChannelRouterSettingsPreset[] = []; for (let preset of factoryRouterPresets) { if (preset.isValid(audioConfig)) { normalizeChannelSelection(preset.mainInputChannels); normalizeChannelSelection(preset.mainOutputChannels); normalizeChannelSelection(preset.auxInputChannels); normalizeChannelSelection(preset.auxOutputChannels); normalizeChannelSelection(preset.sendInputChannels); normalizeChannelSelection(preset.sendOutputChannels); let newPreset = { id: preset.channelRouterPresetId, name: preset.getDescription(audioConfig), settings: preset }; result.push(newPreset); } } return result; } function MakeChannelMenu(channelCount: number, routeType: RouteType, input: boolean, disallowedSelections: number[]): React.ReactElement[] { let items: React.ReactElement[] = [(None)] if (channelCount === 0) { return items; } if (channelCount === 1) { if (input) { items.push((Input)); } else { items.push((Output)); } } else if (channelCount === 2) { items.push((Left)); items.push((Right)); return items; } else { for (let i = 0; i < channelCount; ++i) { items.push((Ch {i + 1})); } } if (routeType === RouteType.Aux && input) { items.push((Main Out L)); items.push((Main Out R)); } return items; } function makeDebugConfig(baseConfig: JackConfiguration): JackConfiguration { let config = new JackConfiguration().deserialize(baseConfig); // clone. for (let i = config.inputAudioPorts.length; i < (debugInputChannels ?? 0); ++i) { config.inputAudioPorts.push("ch" + (i + 1)); } if (config.inputAudioPorts.length > (debugInputChannels ?? 0)) { config.inputAudioPorts = config.inputAudioPorts.slice(0, debugInputChannels ?? 0); } for (let i = config.outputAudioPorts.length; i < (debugOutputChannels ?? 0); ++i) { config.outputAudioPorts.push("ch" + (i + 1)); } if (config.outputAudioPorts.length > (debugOutputChannels ?? 0)) { config.outputAudioPorts = config.outputAudioPorts.slice(0, debugOutputChannels ?? 0); } return config; } function getChannelRouterSettingsPresetById(presetId: number): ChannelRouterSettings { for (let preset of factoryRouterPresets) { if (preset.channelRouterPresetId === presetId) { return preset.clone(); } } throw new Error("Error setting defaults. Preset not found: " + presetId); } function GetDefaultChannelRouterSettings(model: PiPedalModel) : ChannelRouterSettings { let currentSettings = model.channelRouterSettings.get(); if (currentSettings.configured) { return currentSettings; } // we're onboarding the user, so set an appropriate default channel routing preset based on the number of ports on the audio interface. let jackSettings = model.jackConfiguration.get(); let numInputs = jackSettings.inputAudioPorts.length; let numOutputs = jackSettings.outputAudioPorts.length; if (numInputs < 2 || numOutputs < 2) { currentSettings = getChannelRouterSettingsPresetById(DEFAULT_MONO_TO_MONO_ROUTING_PRESET); } else if (numInputs === 2 && numOutputs === 2) { currentSettings = getChannelRouterSettingsPresetById(DEFAULT_RIGHT_TO_STEREO_ROUTING_PRESET); } else { currentSettings = getChannelRouterSettingsPresetById(DEFAULT_LEFT_TO_STEREO_ROUTING_PRESET); } model.setChannelRouterSettings(currentSettings); return model.channelRouterSettings.get(); } function ChannelRouterSettingsDialog(props: ChannelRouterSettingsDialogProps) { //const classes = useStyles(); const { open, onClose } = props; let model = PiPedalModelFactory.getInstance(); let config = model.jackConfiguration.get(); if (debugInputChannels != null || debugOutputChannels != null) { config = makeDebugConfig(config); } let channelRouterPresets = makeRoutingPresets(config); const [settings, setSettings] = useState(GetDefaultChannelRouterSettings(model)); const [showHelp, setShowHelp] = useState(false); React.useEffect(() => { if (open) { let handleSettingsChanged = () => { setSettings(model.channelRouterSettings.get()); }; model.channelRouterSettings.addOnChangedHandler(handleSettingsChanged); return () => { model.channelRouterSettings.removeOnChangedHandler(handleSettingsChanged); } } 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; if (input && debugInputChannels != null) { channelCount = debugInputChannels; } if (!input && debugOutputChannels != null) { channelCount = debugOutputChannels; } let value: number; let disallowedSelections: number[] = []; let channelSelection: number[] = []; switch (routeType) { case RouteType.Main: channelSelection = input ? settings.mainInputChannels : settings.mainOutputChannels; break; case RouteType.Aux: channelSelection = input ? settings.auxInputChannels : settings.auxOutputChannels; break; case RouteType.Send: channelSelection = input ? settings.sendInputChannels : settings.sendOutputChannels; break; } value = channelSelection[channelIndex]; if (value >= channelCount) { value = -1; } if (routeType === RouteType.Send) { if (input) { disallowedSelections = settings.mainInputChannels.concat(settings.auxInputChannels); } else { disallowedSelections = settings.mainOutputChannels.concat(settings.auxOutputChannels); } } if (channelIndex === 1) { if (channelSelection[0] !== -1) { disallowedSelections.push(channelSelection[0]); } } let error = false; if (routeType === RouteType.Main && channelSelection[0] === -1 && channelSelection[1] === -1) { error = true; } return ( ) } let Vu = (routeType: RouteType, input: boolean) => { let instanceId: number; switch (routeType) { case RouteType.Main: instanceId = input ? Pedalboard.START_CONTROL : Pedalboard.MAIN_INSERT_END_CONTROL_ID; break; case RouteType.Aux: instanceId = input ? Pedalboard.AUX_INSERT_START_CONTROL_ID : Pedalboard.AUX_INSERT_END_CONTROL_ID; break; case RouteType.Send: instanceId = input ? Pedalboard.SEND_RETURN_CONTROL : Pedalboard.SEND_OUTPUT_CONTROL; } return (
); } let ApplyPresetId = (presetId: number | string | null | undefined) => { if (typeof presetId !== 'number') { return; } for (let preset of channelRouterPresets) { if (preset.id === presetId) { 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; } } } 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 handleSave = () => { }; let PresetSelect = (width: number | string | undefined) => { return (
{ handleSave(); }} size="large">
{ }} />
) } let LandscapeView = () => { let auxInsertDisabled = !settings.isAuxActive(); return (
{/* Main */} {/* Spacer ---------------- */} {/* Aux ----------------------*/} {/* Spacer ---------------- */} {/* Send ---------------- */}
Main {ChannelSelect(RouteType.Main, 0, true)} {Vu(RouteType.Main, true)} {Vu(RouteType.Main, false)} {ChannelSelect(RouteType.Main, 0, false)}
{(ChannelSelect(RouteType.Main, 1, true))} {(ChannelSelect(RouteType.Main, 1, false))}
Aux {(ChannelSelect(RouteType.Aux, 0, true))} {Vu(RouteType.Aux, true)} {Vu(RouteType.Aux, false)} {(ChannelSelect(RouteType.Aux, 0, false))}
{(ChannelSelect(RouteType.Aux, 1, true))} {(ChannelSelect(RouteType.Aux, 1, false))}
Send {(ChannelSelect(RouteType.Send, 0, false))} {Vu(RouteType.Send,false)} {/* No button */} {Vu(RouteType.Send,true)} {(ChannelSelect(RouteType.Send, 0, true))}
{(ChannelSelect(RouteType.Send, 1, false))} {(ChannelSelect(RouteType.Send, 1, true))}
); }; let PortraitView = () => { let auxInsertDisabled = !settings.isAuxActive(); return (
{/* Main */} {/* Spacer ---------------- */} {/* Aux */} {/* Spacer ---------------- */} {/* Send */}
Main
In
{ChannelSelect(RouteType.Main, 0, true, false)} {ChannelSelect(RouteType.Main, 1, true, false)}
{Vu(RouteType.Main,true)}
{Vu(RouteType.Main,false)}
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(RouteType.Aux, true)}
{Vu(RouteType.Aux, false)}
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(RouteType.Send, false)}
{Vu(RouteType.Send, true)}
Return
{ChannelSelect(RouteType.Send, 0, true, false)} {ChannelSelect(RouteType.Send, 1, true, false)}
); }; return (
{ onClose(); }} > Channel Routing { landscape ? ( <>
{PresetSelect("325px")}
) : (
 
) } { setShowHelp(true); }} >
{!landscape && ( <>
{PresetSelect("60%")}
)} {landscape ? LandscapeView() : PortraitView() } { showHelp && ( setShowHelp(false)} /> ) }
); } export default ChannelRouterSettingsDialog;