diff --git a/react/src/ObservableEvent.tsx b/react/src/ObservableEvent.tsx new file mode 100644 index 0000000..a9e3be9 --- /dev/null +++ b/react/src/ObservableEvent.tsx @@ -0,0 +1,54 @@ +// 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. + + + +export type ObservableEventHandler = (newValue: VALUE_TYPE) => void; + + +export default class ObservableEvent { + + private _on_event_handlers: ObservableEventHandler[] = []; + + + addEventHandler(handler: ObservableEventHandler ) : void + { + this._on_event_handlers.push(handler); + } + removeEventHandler(handler: ObservableEventHandler ) : void + { + let newArray: ObservableEventHandler[] = []; + + for (let myHandler of this._on_event_handlers) + { + if (myHandler !== handler) + { + newArray.push(myHandler); + } + } + this._on_event_handlers = newArray; + } + fire(value: VALUE_TYPE) + { + for (let handler of this._on_event_handlers) + { + handler(value); + } + } +}; \ No newline at end of file diff --git a/react/src/ObservableProperty.tsx b/react/src/ObservableProperty.tsx index 31bb745..3808bd6 100644 --- a/react/src/ObservableProperty.tsx +++ b/react/src/ObservableProperty.tsx @@ -12,7 +12,7 @@ // // 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 +// 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. diff --git a/react/src/PerformanceView.tsx b/react/src/PerformanceView.tsx index 8ca17aa..e89d776 100644 --- a/react/src/PerformanceView.tsx +++ b/react/src/PerformanceView.tsx @@ -19,6 +19,7 @@ import { Theme } from '@mui/material/styles'; +import SaveIconOutline from '@mui/icons-material/Save'; import { WithStyles } from '@mui/styles'; import createStyles from '@mui/styles/createStyles'; import withStyles from '@mui/styles/withStyles'; @@ -78,6 +79,7 @@ interface PerformanceViewState { banks: BankIndex; showSnapshotEditor: boolean; snapshotEditorIndex: number; + presetModified: boolean; } @@ -97,11 +99,13 @@ export const PerformanceView = banks: this.model.banks.get(), wrapSelects: false, showSnapshotEditor: false, - snapshotEditorIndex: 0 + snapshotEditorIndex: 0, + presetModified: this.model.presetChanged.get() }; this.onPresetsChanged = this.onPresetsChanged.bind(this); this.onBanksChanged = this.onBanksChanged.bind(this); this.handlePopState = this.handlePopState.bind(this); + this.onPresetChangedChanged = this.onPresetChangedChanged.bind(this); } @@ -181,6 +185,9 @@ export const PerformanceView = onBanksChanged(newValue: BankIndex) { this.setState({ banks: this.model.banks.get() }) } + onPresetChangedChanged(newValue: boolean) { + this.setState({ presetModified:newValue}); + } private mounted: boolean = false; componentDidMount(): void { @@ -188,9 +195,12 @@ export const PerformanceView = this.mounted = true; this.model.presets.addOnChangedHandler(this.onPresetsChanged); this.model.banks.addOnChangedHandler(this.onBanksChanged); + this.model.presetChanged.addOnChangedHandler(this.onPresetChangedChanged) this.setState({ presets: this.model.presets.get(), - banks: this.model.banks.get() + banks: this.model.banks.get(), + presetModified: this.model.presetChanged.get() + }); this.updateHooks(); @@ -200,7 +210,7 @@ export const PerformanceView = componentWillUnmount(): void { - + this.model.presetChanged.removeOnChangedHandler(this.onPresetChangedChanged) this.model.presets.removeOnChangedHandler(this.onPresetsChanged); this.model.banks.removeOnChangedHandler(this.onBanksChanged); @@ -332,13 +342,27 @@ export const PerformanceView = { this.handleNextBank(); }} - size="large" color="inherit" style={{ borderTopLeftRadius: "3px", borderBottomLeftRadius: "3px", marginLeft: 4 }} > + + {/** spacer */} + { this.handleNextBank(); }} + size="medium" + color="inherit" + style={{visibility: "hidden"}} + > + + + + + + {/********* PRESETS *******************/}
@@ -367,9 +391,14 @@ export const PerformanceView = > { presets.presets.map((preset) => { + let name = preset.name; + if (this.state.presets.selectedInstanceId === preset.instanceId && this.state.presetModified) + { + name += "*"; + } return ( - {preset.name} + {name} ); }) @@ -385,6 +414,16 @@ export const PerformanceView = + { this.model.saveCurrentPreset(); }} + size="medium" + color="inherit" + style={{flexShrink: 0,visibility: this.state.presetModified? "visible": "hidden"}} + > + + +
diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 6a7ad4a..d54a494 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -21,6 +21,7 @@ import { UiPlugin, UiControl, PluginType, UiFileProperty } from './Lv2Plugin'; import { PiPedalArgumentError, PiPedalStateError } from './PiPedalError'; import { UpdateStatus, UpdatePolicyT } from './Updater'; +import ObservableEvent from './ObservableEvent'; import { ObservableProperty } from './ObservableProperty'; import { Pedalboard, PedalboardItem, ControlValue, Snapshot } from './Pedalboard' import PluginClass from './PluginClass'; @@ -362,6 +363,11 @@ export interface FavoritesList { [url: string]: boolean; }; +export interface SnapshotModifiedEvent { + snapshotIndex: number; + modified: boolean; +}; + export class PiPedalModel //implements PiPedalModel { @@ -376,6 +382,9 @@ export class PiPedalModel //implements PiPedalModel webSocket?: PiPedalSocket; + + onSnapshotModified: ObservableEvent = new ObservableEvent(); + ui_plugins: ObservableProperty = new ObservableProperty([]); state: ObservableProperty = new ObservableProperty(State.Loading); @@ -503,6 +512,7 @@ export class PiPedalModel //implements PiPedalModel private setModelPedalboard(pedalboard: Pedalboard) { this.pedalboard.set(pedalboard); this.selectedSnapshot.set(pedalboard.selectedSnapshot); + } onSocketMessage(header: PiPedalMessageHeader, body?: any) { if (this.visibilityState.get() === VisibilityState.Hidden) return; @@ -564,6 +574,21 @@ export class PiPedalModel //implements PiPedalModel let channelSelectionBody = body as ChannelSelectionChangedBody; let channelSelection = new JackChannelSelection().deserialize(channelSelectionBody.jackChannelSelection); this.jackSettings.set(channelSelection); + } else if (message === "onSnapshotModified") { + let {snapshotIndex,modified} = (body as {snapshotIndex: number, modified: boolean}); + let snapshots = this.pedalboard.get().snapshots; + if (snapshotIndex >= 0 && snapshotIndex < snapshots.length) + { + let snapshot = snapshots[snapshotIndex] + if (snapshot) + { + if (snapshot.isModified !== modified) + { + snapshot.isModified = modified; + this.onSnapshotModified.fire({snapshotIndex: snapshotIndex,modified: modified}); + } + } + } } else if (message === "onSelectedSnapshotChanged") { let selectedSnapshot = body as number; this.pedalboard.get().selectedSnapshot = selectedSnapshot; @@ -581,6 +606,7 @@ export class PiPedalModel //implements PiPedalModel let presetsChangedBody = body as PresetsChangedBody; let presets = new PresetIndex().deserialize(presetsChangedBody.presets); this.presets.set(presets); + this.presetChanged.set(presets.presetChanged); } else if (message === "onPluginPresetsChanged") { let pluginUri = body as string; this.handlePluginPresetsChanged(pluginUri); @@ -1286,7 +1312,6 @@ export class PiPedalModel //implements PiPedalModel changed = item.setControlValue(controlValue.key, controlValue.value) || changed; } if (changed) { - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); } } @@ -1428,7 +1453,6 @@ export class PiPedalModel //implements PiPedalModel if (pedalboard.input_volume_db !== volume_db) { let newPedalboard = pedalboard.clone(); newPedalboard.input_volume_db = volume_db; - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); changed = true; } @@ -1457,7 +1481,6 @@ export class PiPedalModel //implements PiPedalModel if (pedalboard.output_volume_db !== volume_db) { let newPedalboard = pedalboard.clone(); newPedalboard.output_volume_db = volume_db; - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); changed = true; } @@ -1494,7 +1517,6 @@ export class PiPedalModel //implements PiPedalModel if (notifyServer) { this._setServerControl("setControl", instanceId, key, value); } - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); for (let i = 0; i < this._controlValueChangeItems.length; ++i) { let item = this._controlValueChangeItems[i]; @@ -1547,7 +1569,6 @@ export class PiPedalModel //implements PiPedalModel let changed = value !== item.isEnabled; if (changed) { item.isEnabled = value; - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); if (notifyServer) { let body: PedalboardItemEnableBody = { @@ -1605,7 +1626,6 @@ export class PiPedalModel //implements PiPedalModel // null -> we've never seen a value. item.pathProperties[fileProperty.patchProperty] = "null"; } - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard() return item.instanceId; @@ -1646,7 +1666,6 @@ export class PiPedalModel //implements PiPedalModel let result = newPedalboard.deleteItem(instanceId); if (result !== null) { - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); } @@ -1669,7 +1688,6 @@ export class PiPedalModel //implements PiPedalModel newPedalboard.deleteItem(fromInstanceId); newPedalboard.addBefore(fromItem, toInstanceId); - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); @@ -1690,7 +1708,6 @@ export class PiPedalModel //implements PiPedalModel newPedalboard.addAfter(fromItem, toInstanceId); - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); } @@ -1712,7 +1729,6 @@ export class PiPedalModel //implements PiPedalModel newPedalboard.addToStart(fromItem); - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); } @@ -1732,7 +1748,6 @@ export class PiPedalModel //implements PiPedalModel newPedalboard.addToEnd(fromItem); - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); @@ -1761,7 +1776,6 @@ export class PiPedalModel //implements PiPedalModel newPedalboard.replaceItem(fromInstanceId, emptyItem); newPedalboard.replaceItem(toInstanceId, fromItem); - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); @@ -1785,7 +1799,6 @@ export class PiPedalModel //implements PiPedalModel } let newItem = newPedalboard.createEmptyItem(); newPedalboard.addItem(newItem, instanceId, append); - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); return newItem.instanceId; @@ -1811,7 +1824,6 @@ export class PiPedalModel //implements PiPedalModel } let newItem = newPedalboard.createEmptySplit(); newPedalboard.addItem(newItem, instanceId, append); - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); return newItem.instanceId; @@ -1829,7 +1841,6 @@ export class PiPedalModel //implements PiPedalModel } newPedalboard.setItemEmpty(item); - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); this.updateServerPedalboard(); return item.instanceId; @@ -2332,7 +2343,6 @@ export class PiPedalModel //implements PiPedalModel let newPedalboard = pedalboard.clone(); this.updateVst3State(newPedalboard); if (newPedalboard.setMidiBinding(instanceId, midiBinding)) { - newPedalboard.selectedSnapshot = -1; this.setModelPedalboard(newPedalboard); diff --git a/react/src/SnapshotButton.tsx b/react/src/SnapshotButton.tsx new file mode 100644 index 0000000..1feafb4 --- /dev/null +++ b/react/src/SnapshotButton.tsx @@ -0,0 +1,212 @@ +/* +* MIT License +* +* 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 Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import ButtonBase from '@mui/material/ButtonBase'; +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import SaveIconOutline from '@mui/icons-material/Save'; +import Button from "@mui/material/Button"; +import EditIconOutline from '@mui/icons-material/Edit'; +import { Snapshot } from './Pedalboard'; + +import { colorKeys, getBackgroundColor, getBorderColor } from './MaterialColors'; +import { PiPedalModel, PiPedalModelFactory, SnapshotModifiedEvent } from './PiPedalModel'; + +export interface SnapshotButtonProps { + snapshot: Snapshot | null; + snapshotIndex: number; + selected: boolean; + largeText: boolean; + collapseButtons: boolean; + + onEditSnapshot: (index: number) => void; + onSaveSnapshot: (index: number) => void; + onSelectSnapshot: (index: number) => void; +}; + +export interface SnapshotButtonState { + modified: boolean; +}; + +export default class SnapshotButton extends ResizeResponsiveComponent { + private model: PiPedalModel; + + constructor(props: SnapshotButtonProps) { + super(props); + this.state = { + modified: this.props.snapshot?.isModified ?? false, + }; + this.model = PiPedalModelFactory.getInstance(); + this.onSnapshotModified = this.onSnapshotModified.bind(this); + + } + + onWindowSizeChanged(width: number, height: number): void { + + } + + onSnapshotModified(event: SnapshotModifiedEvent) { + if (event.snapshotIndex === this.props.snapshotIndex) { + this.setState({ modified: event.modified }); + } + } + componentDidMount(): void { + super.componentDidMount?.(); + this.model.onSnapshotModified.addEventHandler(this.onSnapshotModified); + } + componentWillUnmount(): void { + this.model.onSnapshotModified.removeEventHandler(this.onSnapshotModified); + super.componentWillUnmount(); + } + componentDidUpdate( + prevProps: Readonly, + prevState: Readonly): void { + super.componentDidUpdate?.(prevProps, prevState); + + if (this.props.snapshot !== prevProps.snapshot) { + this.setState({ + modified: this.props.snapshot?.isModified ?? false + }) + } + } + + render() { + let { snapshot, snapshotIndex, selected } = this.props; + let { modified } = this.state; + + // (snapshot: Snapshot | null, index: number) { + //let state = this.state; + let color: string; + let bordercolor: string; + if (snapshot) { + if (colorKeys.indexOf(snapshot.color) === -1) { + bordercolor = color = snapshot.color; + } else { + color = getBackgroundColor(snapshot.color); + bordercolor = getBorderColor(snapshot.color); + } + } else { + bordercolor = color = getBackgroundColor("grey"); + } + let title = snapshot ? snapshot.name : ""; + if (modified) { + title = title + "*"; + } + let disabled = snapshot === null; + return ( +
+ + { + if (!selected) ev.stopPropagation(); + }} + + onClick={() => { if (snapshot) this.props.onSelectSnapshot(snapshotIndex); }} + > +
{ + if (disabled) ev.stopPropagation(); + }} + > +
+
+ + {title} +
+ + {(snapshotIndex + 1).toString()} + +
+ {/* placeholder where we will float the buttons, which can't be witin another button (tons of DOM validation warning messages in chrome) */} +
+ +
+
+
+ +
+ {!this.props.collapseButtons ? ( +
+ + +
+ + ) : ( +
+
 
+ { ev.stopPropagation(); /*don't prop to card*/ }} + onClick={() => { this.props.onSaveSnapshot(snapshotIndex); }} + > + + + { ev.stopPropagation(); /*don't prop to card*/ }} + onClick={() => { this.props.onEditSnapshot(snapshotIndex); }} + + > + + +
+ )} +
+
+ ); + } +} + diff --git a/react/src/SnapshotEditor.tsx b/react/src/SnapshotEditor.tsx index c386c12..d078ff7 100644 --- a/react/src/SnapshotEditor.tsx +++ b/react/src/SnapshotEditor.tsx @@ -216,8 +216,23 @@ const SnapshotEditor = withStyles(appStyles)(class extends ResizeResponsiveCompo } handleOk() { - let selectedSnapshot = this.props.snapshotIndex; let currentPedalboard = this.model_.pedalboard.get(); + let selectedSnapshot = this.props.snapshotIndex; + let currentSnapshot: Snapshot | null = currentPedalboard.snapshots[selectedSnapshot]; + if (!currentSnapshot) + { + this.props.onClose(); + return; + } + + let changed = this.state.name !== currentSnapshot.name + || this.state.color !== currentSnapshot.color + || currentSnapshot.isModified; + if (!changed) + { + this.props.onClose(); + return; + } let newSnapshots = Snapshot.cloneSnapshots(currentPedalboard.snapshots); let newSnapshot = this.model_.pedalboard.get().makeSnapshot(); newSnapshot.name = this.state.name; diff --git a/react/src/SnapshotPanel.tsx b/react/src/SnapshotPanel.tsx index 205d99b..f1316f4 100644 --- a/react/src/SnapshotPanel.tsx +++ b/react/src/SnapshotPanel.tsx @@ -22,18 +22,12 @@ * SOFTWARE. */ -import Typography from '@mui/material/Typography'; -import IconButton from '@mui/material/IconButton'; -import ButtonBase from '@mui/material/ButtonBase'; +import SnapshotButton from './SnapshotButton'; import { Pedalboard, Snapshot } from './Pedalboard'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import ResizeResponsiveComponent from './ResizeResponsiveComponent'; -import SaveIconOutline from '@mui/icons-material/Save'; -import Button from "@mui/material/Button"; -import EditIconOutline from '@mui/icons-material/Edit'; import SnapshotPropertiesDialog from './SnapshotPropertiesDialog'; -import { colorKeys, getBackgroundColor, getBorderColor } from './MaterialColors'; @@ -100,7 +94,7 @@ export default class SnapshotPanel extends ResizeResponsiveComponent"; - let disabled = snapshot === null; - let selected = this.state.selectedSnapshot === index; return ( -
- - { - if (!selected) ev.stopPropagation(); - }} - - onClick={() => { if (snapshot) this.selectSnapshot(index); }} - > -
{ - if (disabled) ev.stopPropagation(); - }} - > -
-
- - {title} -
-
- {/* placeholder where we will float the buttons, which can't be witin another button (tons of DOM validation warning messages in chrome) */} -
- -
-
-
-
- {(index+1).toString()} -
- -
- {!this.state.collapseButtons ? ( -
-
 
- - -
- - ) : ( -
-
 
- { ev.stopPropagation(); /*don't prop to card*/ }} - onClick={() => { this.onSaveSnapshot(index); }} - > - - - { ev.stopPropagation(); /*don't prop to card*/ }} - onClick={() => { this.onEditSnapshot(index); }} - - > - - -
- - )} - -
-
+ {this.onEditSnapshot(index)}} + onSaveSnapshot={(index)=>{ this.onSaveSnapshot(index); }} + onSelectSnapshot={(index)=>{this.model.selectSnapshot(index);}} + /> ); } diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 7f69178..0e559b4 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -177,16 +177,24 @@ namespace pipedal std::istringstream ss(pathProperty.second); json_reader reader(ss); reader.read(&vProperty); - vProperty = pluginHost.MapPath(vProperty); + if (!vProperty.is_null()) + { + try { + vProperty = pluginHost.MapPath(vProperty); - // now to atom format (what we want on the rt thread0) - AtomConverter atomConverter(pluginHost.GetMapFeature()); - LV2_Atom *atomValue = atomConverter.ToAtom(vProperty); + // now to atom format (what we want on the rt thread0) + AtomConverter atomConverter(pluginHost.GetMapFeature()); + LV2_Atom *atomValue = atomConverter.ToAtom(vProperty); - size_t atomBufferSize = (atomValue->size + sizeof(LV2_Atom) + 3) / 4 * 4; - pathPatchProperty.atomBuffer.resize(atomValue->size + sizeof(LV2_Atom)); - memcpy(pathPatchProperty.atomBuffer.data(), atomValue, pathPatchProperty.atomBuffer.size()); - this->pathPatchProperties.push_back(std::move(pathPatchProperty)); + size_t atomBufferSize = (atomValue->size + sizeof(LV2_Atom) + 3) / 4 * 4; + pathPatchProperty.atomBuffer.resize(atomValue->size + sizeof(LV2_Atom)); + memcpy(pathPatchProperty.atomBuffer.data(), atomValue, pathPatchProperty.atomBuffer.size()); + this->pathPatchProperties.push_back(std::move(pathPatchProperty)); + } catch (const std::exception &e) + { + Lv2Log::info(SS("IndexedSnapshotValue: Failed to map path property " << pathProperty.first << ". " << e.what())); + } + } } } } diff --git a/src/HotspotManager.cpp b/src/HotspotManager.cpp index b4db376..d4e76a5 100644 --- a/src/HotspotManager.cpp +++ b/src/HotspotManager.cpp @@ -37,7 +37,7 @@ using namespace dbus::networkmanager; namespace pipedal::impl { - #define PIPEDAL_HOTSPOT_NAME "PiPedal Hotspot" +#define PIPEDAL_HOTSPOT_NAME "PiPedal Hotspot" class HotspotManagerImpl : public HotspotManager { @@ -164,19 +164,20 @@ void HotspotManagerImpl::Open() void HotspotManagerImpl::onClose() { - this->closed = true; // avoids a memory barrier probelm. + this->closed = true; // avoids a memory barrier probelm. CancelDeviceChangedTimer(); CancelWaitForNetworkManagerTimer(); CancelAccessPointsChangedTimer(); - if (networkManager && activeConnection) { - try { + try + { networkManager->DeactivateConnection(activeConnection->getObjectPath()); activeConnection = nullptr; - } catch (const std::exception&e) + } + catch (const std::exception &e) { // nothrow. } @@ -443,16 +444,31 @@ void HotspotManagerImpl::onReload() if (closed) return; WifiConfigSettings oldSettings = this->wifiConfigSettings; - ; - wifiConfigSettings.Load(); + this->wifiConfigSettings.Load(); + if (wifiConfigSettings == oldSettings) + { + return; + } switch (state) { - case State::Initial: case State::Error: - // ignore. + Lv2Log::error("NetworkManager is in an error state. Reboot and try again."); return; + case State::Initial: + case State::WaitingForNetworkManager: + // ignore. new config will be picked up once initialization completes. + return; + + case State::Closed: + return; + + case State::HotspotConnecting: + case State::HotspotConnected: default: + // force a reload. + StopHotspot(); + SetState(State::Monitoring); MaybeStartHotspot(); return; } @@ -536,7 +552,8 @@ void HotspotManagerImpl::UpdateKnownNetworks( } for (auto &accessPoint : allAccessPoints) { - try { + try + { uint8_t strength = accessPoint->Strength(); auto vSsid = accessPoint->Ssid(); std::string ssid = ssidToString(vSsid); @@ -547,7 +564,8 @@ void HotspotManagerImpl::UpdateKnownNetworks( record.visibleNetwork = true; record.strength = strength; } - } catch (const std::exception&ignored) + } + catch (const std::exception &ignored) { // race to get the info before it changes. np. } @@ -819,8 +837,6 @@ void HotspotManagerImpl::StartHotspot() std::vector vSsid{ssid.begin(), ssid.end()}; wireless["ssid"] = vSsid; wireless["mode"] = "ap"; - wireless["band"] = "a"; - wireless["band"] = "bg"; uint32_t iChannel = 0; auto channel = this->wifiConfigSettings.channel_; @@ -829,9 +845,14 @@ void HotspotManagerImpl::StartHotspot() std::stringstream ss{channel}; ss >> iChannel; } - if (iChannel != 0) + if (iChannel <= 1) { wireless["channel"] = iChannel; + wireless["band"] = iChannel > 14 ? "a" : "bg"; + } + else + { + wireless["band"] = iChannel == 0 ? "bg" : "a"; } std::map &wirelessSecurity = settings["802-11-wireless-security"]; @@ -840,6 +861,26 @@ void HotspotManagerImpl::StartHotspot() settings["ipv4"]["method"] = "shared"; settings["ipv6"]["method"] = "shared"; + + //////////////////////////////////////////////////////////////// + + // Create connection + settings["ipv4"]["method"] = sdbus::Variant(std::string("shared")); + //settings["ipv6"]["method"] = sdbus::Variant(std::string("ignore")); + + settings["ipv4"]["address-data"] = sdbus::Variant(std::vector>{{{"address", sdbus::Variant("192.168.60.1")}, + {"prefix", sdbus::Variant(uint32_t(24))}}}); + settings["ipv4"]["dhcp-send-hostname"] = sdbus::Variant(true); + settings["ipv4"]["dhcp-hostname"] = sdbus::Variant("raspberrypi"); + // settings["ipv4"]["dhcp-options"] = sdbus::Variant(std::vector{ + // "option:classless-static-route,192.168.60.0/24,0.0.0.0,0.0.0.0/0,192.168.60.1"}); + + settings["ipv4"]["route-data"] = sdbus::Variant(std::vector>{{ + {"dest", sdbus::Variant("192.168.4.0")}, + {"prefix", sdbus::Variant(uint32_t(24))}, + {"metric", sdbus::Variant((uint32_t)99)} + }}); + //////////////////////////////////////////////////////////// // settings["ipv6"]["addr-gen-mode"] = "stable-privacy"; std::map options; diff --git a/src/Ipv6Helpers.cpp b/src/Ipv6Helpers.cpp index 7634bbd..5b1dda3 100644 --- a/src/Ipv6Helpers.cpp +++ b/src/Ipv6Helpers.cpp @@ -385,6 +385,34 @@ static std::string GetNonLinkLocalAddressForIp4Interface(const std::string &name return result; } +bool pipedal::IsLinkLocalAddress(const std::string fromAddress) +{ + std::string address = fromAddress; + std::string result; + if (address[0] != '[') + { + return false; + } + else + { + // ipv6 + if (address[0] != '[' || address[address.length() - 1] != ']') + throw std::invalid_argument("Bad address."); + address = address.substr(1, address.length() - 2); + + struct in6_addr inetAddr6; + memset(&inetAddr6, 0, sizeof(inetAddr6)); + if (inet_pton(AF_INET6, address.c_str(), &inetAddr6) == 1) + { + if (IN6_IS_ADDR_LINKLOCAL(&inetAddr6)) + { + return true; + } + } + } + return false; +} + std::string pipedal::GetNonLinkLocalAddress(const std::string fromAddress) { std::string address = fromAddress; diff --git a/src/Ipv6Helpers.hpp b/src/Ipv6Helpers.hpp index abfefed..77f2924 100644 --- a/src/Ipv6Helpers.hpp +++ b/src/Ipv6Helpers.hpp @@ -27,6 +27,8 @@ namespace pipedal { std::string GetInterfaceIpv4Address(const std::string& interfaceName); + bool IsLinkLocalAddress(const std::string fromAddress); + std::string GetNonLinkLocalAddress(const std::string fromAddress); bool IsOnLocalSubnet(const std::string&fromAddress); diff --git a/src/Pedalboard.cpp b/src/Pedalboard.cpp index 6a27f1c..5349a96 100644 --- a/src/Pedalboard.cpp +++ b/src/Pedalboard.cpp @@ -87,6 +87,18 @@ ControlValue* PedalboardItem::GetControlValue(const std::string&symbol) return nullptr; } +const ControlValue* PedalboardItem::GetControlValue(const std::string&symbol) const +{ + for (size_t i = 0; i < this->controlValues().size(); ++i) + { + if (this->controlValues()[i].key() == symbol) + { + return &(this->controlValues()[i]); + } + } + return nullptr; +} + bool Pedalboard::SetItemEnabled(int64_t pedalItemId, bool enabled) { PedalboardItem*item = GetItem(pedalItemId); @@ -266,6 +278,17 @@ bool PedalboardItem::IsStructurallyIdentical(const PedalboardItem&other) const } if (this->isSplit()) // so is the other by virtue of idential uris. { + auto myValue = this->GetControlValue("splitType"); + auto otherValue = other.GetControlValue("splitType"); + if (myValue == nullptr || otherValue == nullptr) // actually an error. + { + return false; + } + // split type changes potentially trigger buffer allocation changes, + // so different split types are not structurally identical. + if (myValue->value() != otherValue->value()) { + return false; + } if (topChain().size() != other.topChain().size()) { return false; @@ -356,6 +379,30 @@ void PedalboardItem::AddResetsForMissingProperties(Snapshot&snapshot, size_t*ind } +Pedalboard Pedalboard::DeepCopy() +{ + Pedalboard result = *this; + for (size_t i= 0; i < snapshots_.size(); ++i) + { + if (snapshots_[i]) + { + result.snapshots_[i] = std::make_shared(*(snapshots_[i])); + } + } + return result; +} +void Pedalboard::SetCurrentSnapshotModified(bool modified) +{ + if (selectedSnapshot() != -1) + { + auto& snapshot = snapshots_[selectedSnapshot_]; + if (snapshot) + { + snapshot->isModified_ = modified; + } + } +} + Snapshot Pedalboard::MakeSnapshotFromCurrentSettings(const Pedalboard &previousPedalboard) { Snapshot snapshot; diff --git a/src/Pedalboard.hpp b/src/Pedalboard.hpp index 579dc2f..7e67f67 100644 --- a/src/Pedalboard.hpp +++ b/src/Pedalboard.hpp @@ -101,6 +101,7 @@ public: PropertyMap patchProperties; public: ControlValue*GetControlValue(const std::string&symbol); + const ControlValue*GetControlValue(const std::string&symbol) const; bool IsStructurallyIdentical(const PedalboardItem&other) const; @@ -166,7 +167,7 @@ class Snapshot { public: std::string name_; std::string color_; - bool isModified_ = true; + bool isModified_ = false; std::vector values_; DECLARE_JSON_MAP(Snapshot); @@ -186,10 +187,13 @@ class Pedalboard { int64_t selectedSnapshot_ = -1; public: + // 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 SetItemEnabled(int64_t pedalItemId, bool enabled); + void SetCurrentSnapshotModified(bool modified); bool IsStructureIdentical(const Pedalboard &other) const; // caan we just send a snapshot-style uddate instead of reloading plugins? All settings are ignored. Snapshot MakeSnapshotFromCurrentSettings(const Pedalboard &previousPedalboard); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index aa3e02d..e1d46e3 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -101,6 +101,26 @@ PiPedalModel::PiPedalModel() // don't actuall start the hotspotManager until after LV2 is initialized (in order to avoid logging oddities) } +void PrepareSnapshostsForSave(Pedalboard&pedalboard) +{ + if (pedalboard.selectedSnapshot() != -1) { + auto¤tSnapshot = pedalboard.snapshots()[pedalboard.selectedSnapshot()]; + if (!currentSnapshot || currentSnapshot->isModified_) + { + pedalboard.selectedSnapshot(-1); + + } + } + for (auto &snapshot: pedalboard.snapshots()) + { + if (snapshot) { + snapshot->isModified_ = false; + } + } +} + + + void PiPedalModel::Close() { std::unique_ptr oldAudioHost; @@ -515,15 +535,19 @@ void PiPedalModel::SetPedalboard(int64_t clientId, Pedalboard &pedalboard) void PiPedalModel::SetSnapshot(int64_t selectedSnapshot) { std::lock_guard lock(mutex); - if (this->pedalboard.selectedSnapshot() == selectedSnapshot) - { - return; - } if (this->pedalboard.ApplySnapshot(selectedSnapshot)) { + this->pedalboard.SetCurrentSnapshotModified(false); + this->FireSnapshotModified(selectedSnapshot,false); if (this->audioHost) { - this->audioHost->LoadSnapshot(*(this->pedalboard.snapshots()[selectedSnapshot]), this->pluginHost); // no longer own it. + if (this->previousPedalboardLoaded && this->pedalboard.IsStructureIdentical(this->previousPedalboard)) + { + this->audioHost->LoadSnapshot(*(this->pedalboard.snapshots()[selectedSnapshot]), this->pluginHost); // no longer own it. + } else { + LoadCurrentPedalboard(); + } + } SetPresetChanged(-1, true, false); this->pedalboard.selectedSnapshot(selectedSnapshot); @@ -535,43 +559,21 @@ void PiPedalModel::SetSnapshots(std::vector> &snapshot { std::lock_guard lock(mutex); - // notionally, snapshots are saved whenever they are modified, but pedalboards are not. - // so we must add the current snapshots BOTH to the current pedalbaoard, AND to the - // current saved instance of the pedalboard to create the illusion that the are stored separately. - // In practice, snapshots have to track structure changes (delete pedalboard items that have been deleted, - // and set default settings for pedalboard items that have ben added since the snapshots was last saved. - // - // This is acheived by: - // 1. updating BOTH the current pedalboard, and its saved instance. - // 2. discarding references to dangling instances as plugins are loaded (see UpdateDefaults). - // 3. Setting missing snapshot controls to default values (NOT the current pedalboard values) - // 4. (We currently don't do the right thing with patch properties if the patch property has never been set). UpdateVst3Settings(pedalboard); - { - // stealth update of the saved snapshots. - auto savedPedalboard = storage.GetCurrentPreset(); - savedPedalboard.snapshots(snapshots); // makes a shallow copy - - if (selectedSnapshot != -1) - { - // implies that this is a snapshot of the currently running pedalboard. so we can mark it as the selected pedalboard. - this->pedalboard.selectedSnapshot(selectedSnapshot); - } - // UpdateDefaults(&savedPedalboard); // The update is awfully fresh. wait until it gets loaded again before pruning. - storage.SaveCurrentPreset(savedPedalboard); - } - this->pedalboard.snapshots(std::move(snapshots)); + if (selectedSnapshot != -1) { this->pedalboard.selectedSnapshot(selectedSnapshot); } - + this->FirePedalboardChanged(-1, false); // notify clients (but don't change the running pedalboard, because it's still the same) // this means that all clients get an up-to-date copy of the snapshots AND the currently selected snapshot if that applies // (and a fresh copy of the pedalboard settings as well, which is harmless, since they have not changed) + this->SetPresetChanged(-1, true,false); + } void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalboard) @@ -634,8 +636,16 @@ void PiPedalModel::SetPresetChanged(int64_t clientId, bool value, bool changeSna { if (changeSnapshotSelect && value && this->pedalboard.selectedSnapshot() != -1) { - this->pedalboard.selectedSnapshot(-1); - FireSelectedSnapshotChanged(-1); + auto & snapshot = this->pedalboard.snapshots()[pedalboard.selectedSnapshot()]; + if (snapshot) { + if (!snapshot->isModified_) + { + snapshot->isModified_ = true; + FireSnapshotModified(pedalboard.selectedSnapshot(),true); + } + } + + this->pedalboard.SetCurrentSnapshotModified(true); } if (value != this->hasPresetChanged) { @@ -644,6 +654,20 @@ void PiPedalModel::SetPresetChanged(int64_t clientId, bool value, bool changeSna } } +void PiPedalModel::FireSnapshotModified(int64_t snapshotIndex, bool modified) +{ + 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) + std::vector t{subscribers.begin(), subscribers.end()}; + for (auto &subscriber : t) + { + subscriber->OnSnapshotModified(snapshotIndex,modified); + } + } + +} + void PiPedalModel::FireSelectedSnapshotChanged(int64_t selectedSnapshot) { std::lock_guard guard{mutex}; @@ -762,6 +786,8 @@ void PiPedalModel::SaveCurrentPreset(int64_t clientId) UpdateVst3Settings(this->pedalboard); SyncLv2State(); + PrepareSnapshostsForSave(pedalboard); + storage.SaveCurrentPreset(this->pedalboard); this->SetPresetChanged(clientId, false); @@ -791,12 +817,15 @@ int64_t PiPedalModel::SavePluginPresetAs(int64_t instanceId, const std::string & return presetId; } + int64_t PiPedalModel::SaveCurrentPresetAs(int64_t clientId, const std::string &name, int64_t saveAfterInstanceId) { std::lock_guard guard{mutex}; SyncLv2State(); - auto pedalboard = this->pedalboard; + auto pedalboard = this->pedalboard.DeepCopy(); + PrepareSnapshostsForSave(pedalboard); + UpdateVst3Settings(pedalboard); pedalboard.name(name); int64_t result = storage.SaveCurrentPresetAs(pedalboard, name, saveAfterInstanceId); @@ -1280,6 +1309,7 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver) this->audioHost->SetPedalboard(nullptr); + previousPedalboardLoaded = false; auto jackServerSettings = this->jackServerSettings; if (useDummyAudioDriver) { @@ -2359,6 +2389,8 @@ bool PiPedalModel::LoadCurrentPedalboard() // return true if the error messages have changed CheckForResourceInitialization(this->pedalboard); audioHost->SetPedalboard(lv2Pedalboard); + previousPedalboard = this->pedalboard; + previousPedalboardLoaded = true; return true; } diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 6cdc8cf..cc874fb 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -68,6 +68,7 @@ namespace pipedal virtual void OnPedalboardChanged(int64_t clientId, const Pedalboard &pedalboard) = 0; virtual void OnPresetsChanged(int64_t clientId, const PresetIndex &presets) = 0; virtual void OnPresetChanged(bool changed) = 0; + 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; @@ -180,6 +181,7 @@ namespace pipedal std::vector> subscribers; void SetPresetChanged(int64_t clientId, bool value, bool changeSnapshotSelect = true); + void FireSnapshotModified(int64_t snapshotIndex, bool modified); void FireSelectedSnapshotChanged(int64_t selectedSnapshot); void FirePresetsChanged(int64_t clientId); void FirePresetChanged(bool changed); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 4c00411..67bd417 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -396,6 +396,19 @@ JSON_MAP_REFERENCE(SetSnapshotsBody, snapshots) JSON_MAP_REFERENCE(SetSnapshotsBody, selectedSnapshot) JSON_MAP_END() +class SnapshotModifiedBody { +public: + int64_t snapshotIndex_; + bool modified_; + + DECLARE_JSON_MAP(SnapshotModifiedBody); +}; + +JSON_MAP_BEGIN(SnapshotModifiedBody) +JSON_MAP_REFERENCE(SnapshotModifiedBody, snapshotIndex) +JSON_MAP_REFERENCE(SnapshotModifiedBody, modified) +JSON_MAP_END() + class ChannelSelectionChangedBody { public: @@ -1762,6 +1775,15 @@ private: body.jackChannelSelection_ = const_cast(&channelSelection); Send("onChannelSelectionChanged", body); } + + virtual void OnSnapshotModified(int64_t snapshotIndex, bool modified) + { + SnapshotModifiedBody body; + body.snapshotIndex_ = snapshotIndex; + body.modified_ = modified; + Send("onSnapshotModified", body); + } + virtual void OnSelectedSnapshotChanged(int64_t selectedSnapshot) override { Send("onSelectedSnapshotChanged", selectedSnapshot); diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 806f260..c623922 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -936,6 +936,20 @@ namespace pipedal return result.str(); } + static void splitAddressAndPort(const std::string &address, std::string&addrOnly, std::string&port) + { + size_t portPos = address.length(); + for (size_t i = address.length(); i > 0; --i) + { + if (address[i-1] == ':') + { + portPos = i-1; + break; + } + } + addrOnly = address.substr(0,portPos); + port = address.substr(portPos); + } void on_http(connection_hdl hdl) { // Upgrade our connection handle to a full connection_ptr @@ -974,6 +988,31 @@ namespace pipedal std::string fromAddress = SS(con->get_remote_endpoint()); + // redirect requests to IPV6 local connections to a better address. + + std::string addrOnly,portStr; + splitAddressAndPort(fromAddress,addrOnly,portStr); + + if (IsLinkLocalAddress(addrOnly)) + { + try { + uri_builder builder(requestUri); + + std::string redirectAddress = GetNonLinkLocalAddress(addrOnly); + builder.set_authority(redirectAddress+portStr); + std::string redirectUri = builder.str(); + + res.set(HttpField::location,redirectUri.c_str()); + Lv2Log::info(SS("Redirecting from " << fromAddress << " to " << redirectUri)); + res.keepAlive(false); + con->set_status(websocketpp::http::status_code::temporary_redirect); + return; + } catch (const std::exception&e) + { + ServerError(*con,"Invalid request on link-local address."); + return; + } + } if (req.method() == HttpVerb::options) { res.set(HttpField::access_control_allow_origin, origin); @@ -984,6 +1023,7 @@ namespace pipedal return; } + for (auto requestHandler : this->request_handlers) { diff --git a/src/WebServer.hpp b/src/WebServer.hpp index d7d402d..f85657a 100644 --- a/src/WebServer.hpp +++ b/src/WebServer.hpp @@ -70,6 +70,7 @@ public: constexpr static const char* origin = "Origin"; constexpr static const char* date = "Date"; constexpr static const char* referer = "Referer"; + constexpr static const char * location = "Location"; }; diff --git a/src/WifiChannelSelectors.cpp b/src/WifiChannelSelectors.cpp index 8a04c99..2398325 100644 --- a/src/WifiChannelSelectors.cpp +++ b/src/WifiChannelSelectors.cpp @@ -74,12 +74,12 @@ std::vector pipedal::getWifiChannelSelectors(const char*cou if (forCommandline) { WifiChannelSelector autoSelect; autoSelect.channelId_ = "0"; - autoSelect.channelName_ = "0 Select automatically"; + autoSelect.channelName_ = "0 Automatic"; result.push_back(autoSelect); } else { WifiChannelSelector autoSelect; autoSelect.channelId_ = "0"; - autoSelect.channelName_ = "Select automatically"; + autoSelect.channelName_ = "Automatic"; result.push_back(autoSelect); } WifiInfo wifiInfo = getWifiInfo(countryIso3661);