diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ae8ba9..eba0631 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.16.0) project(pipedal - VERSION 1.0.17 + VERSION 1.0.18 DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" HOMEPAGE_URL "https://rerdavies.github.io/pipedal" ) -set (DISPLAY_VERSION "v1.0.17") +set (DISPLAY_VERSION "v1.0.18") set (CMAKE_INSTALL_PREFIX "/usr/") diff --git a/LICENSE.txt b/LICENSE.txt index cd83fdf..279f498 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2021-2022 Robin Davies +Copyright 2021-2023 Robin Davies Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/README.md b/README.md index b936afe..37022fe 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@
-Download: v1.0.17 +Download: v1.0.18 Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). diff --git a/debian/control b/debian/control index 58c4eb1..85e3207 100644 --- a/debian/control +++ b/debian/control @@ -9,6 +9,6 @@ Package: pipedal Pre-Depends: hostapd;authbind Priority: optional Section: sound -Version: 1.0.17 +Version: 1.0.18 Installed-Size: 15147 diff --git a/docs/Installing.md b/docs/Installing.md index 4cccd78..e4ca367 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -13,14 +13,14 @@ page_icon: img/Install4.jpg Download the most recent Debian (.deb) package for your platform: -- [Ubuntu/Raspberry Pi OS (64-bit) v1.0.17](https://github.com/rerdavies/pipedal/releases/download/v1.0.17/pipedal_1.0.17_arm64.deb) +- [Ubuntu/Raspberry Pi OS (64-bit) v1.0.18](https://github.com/rerdavies/pipedal/releases/download/v1.0.18/pipedal_1.0.18_arm64.deb) Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.0.17_arm64.deb + sudo apt-get install ./pipedal_1.0.18_arm64.deb ``` On Raspberry Pi OS, if you have a graphical desktop installed, you can also install the package by double-clicking on the downloaded package in the File Manager. diff --git a/docs/download.md b/docs/download.md index d13564e..330fa98 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,14 +4,14 @@ Download the most recent Debian (.deb) package for your platform: -- [Ubuntu or Raspberry Pi OS (64-bit)](https://github.com/rerdavies/pipedal/releases/download/v1.0.17/pipedal_1.0.17_arm64.deb) v1.0.17 +- [Ubuntu or Raspberry Pi OS (64-bit)](https://github.com/rerdavies/pipedal/releases/download/v1.0.18/pipedal_1.0.18_arm64.deb) v1.0.18 Install the package by running ``` sudo apt update cd ~/Downloads - sudo apt-get install ./pipedal_1.0.17_arm64.deb + sudo apt-get install ./pipedal_1.0.18_arm64.deb ``` Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. diff --git a/docs/index.md b/docs/index.md index b5359b9..ed0af00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ -v1.0.17 +v1.0.18   diff --git a/react/src/MidiBinding.tsx b/react/src/MidiBinding.tsx index ed18918..766d5b1 100644 --- a/react/src/MidiBinding.tsx +++ b/react/src/MidiBinding.tsx @@ -31,6 +31,11 @@ export default class MidiBinding { this.switchControlType = input.switchControlType; return this; } + static systemBinding(symbol: string): MidiBinding { + let result = new MidiBinding(); + result.symbol = symbol; + return result; + } static deserialize_array(input: any): MidiBinding[] { let result: MidiBinding[] = []; for (let i = 0; i < input.length; ++i) diff --git a/react/src/MidiBindingView.tsx b/react/src/MidiBindingView.tsx index c0088eb..9753b1f 100644 --- a/react/src/MidiBindingView.tsx +++ b/react/src/MidiBindingView.tsx @@ -257,7 +257,7 @@ const MidiBindingView = onChange={(e, extra) => this.handleLatchControlTypeChange(e, extra)} value={midiBinding.switchControlType} > - Latch + Toggle Momentary diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index aac1929..49606af 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -339,6 +339,7 @@ export interface PiPedalModel { favorites: ObservableProperty; presets: ObservableProperty; + systemMidiBindings: ObservableProperty; zoomedUiControl: ObservableProperty; @@ -409,6 +410,8 @@ export interface PiPedalModel { updatePluginPresets(uri: string, presets: PluginUiPresets): Promise; duplicatePluginPreset(uri: string, instanceId: number): Promise; + setSystemMidiBinding(instanceId: number, midiBinding: MidiBinding): void; + shutdown(): Promise; restart(): Promise; @@ -441,8 +444,8 @@ export interface PiPedalModel { zoomUiControl(sourceElement: HTMLElement, instanceId: number, uiControl: UiControl): void; - onPreviousZoomedControl() : void; - onNextZoomedControl() : void; + onPreviousZoomedControl(): void; + onNextZoomedControl(): void; clearZoomedControl(): void; setFavorite(pluginUrl: string, isFavorite: boolean): void; @@ -495,7 +498,13 @@ class PiPedalModelImpl implements PiPedalModel { ( new PresetIndex() ); - + systemMidiBindings: ObservableProperty = new ObservableProperty + ( + [ + MidiBinding.systemBinding("prevProgram"), + MidiBinding.systemBinding("nextProgram") + ] + ); zoomedUiControl: ObservableProperty = new ObservableProperty(undefined); svgImgUrl(svgImage: string): string { @@ -682,7 +691,10 @@ class PiPedalModelImpl implements PiPedalModel { if (header.replyTo) { this.webSocket?.reply(header.replyTo, "onVuUpdate", true); } - + } else if (message === "onSystemMidiBindingsChanged") + { + let bindings = MidiBinding.deserialize_array(body); + this.systemMidiBindings.set(bindings); } else { throw new PiPedalStateError("Unrecognized message received from server: " + message); } @@ -815,6 +827,12 @@ class PiPedalModelImpl implements PiPedalModel { .then((data) => { this.favorites.set(data); + return this.getWebSocket().request("getSystemMidiBindings"); + }) + .then((data)=> { + let bindings = MidiBinding.deserialize_array(data); + this.systemMidiBindings.set(bindings); + this.setState(State.Ready); }) .catch((what) => { @@ -944,7 +962,7 @@ class PiPedalModelImpl implements PiPedalModel { }) .then(data => { this.showStatusMonitor.set(data); - + return this.getWebSocket().request("getJackServerSettings"); }) .then(data => { @@ -977,6 +995,12 @@ class PiPedalModelImpl implements PiPedalModel { .then((data) => { this.favorites.set(data); + return this.getWebSocket().request("getSystemMidiBindings"); + }) + .then((data)=> { + let bindings = MidiBinding.deserialize_array(data); + this.systemMidiBindings.set(bindings); + if (this.webSocket) { // MUST not allow reconnect until at least one complete load has finished. this.webSocket.canReconnect = true; @@ -1215,7 +1239,7 @@ class PiPedalModelImpl implements PiPedalModel { } } - _setVst3PedalBoardControlValue(instanceId: number, key: string, value: number, state: string, notifyServer: boolean ): void { + _setVst3PedalBoardControlValue(instanceId: number, key: string, value: number, state: string, notifyServer: boolean): void { let pedalBoard = this.pedalBoard.get(); if (pedalBoard === undefined) throw new PiPedalStateError("Pedalboard not ready."); let newPedalBoard = pedalBoard.clone(); @@ -1251,7 +1275,7 @@ class PiPedalModelImpl implements PiPedalModel { let newPedalBoard = pedalBoard.clone(); let item = newPedalBoard.getItem(instanceId); - + let changed = value !== item.isEnabled; if (changed) { item.isEnabled = value; @@ -1837,8 +1861,7 @@ class PiPedalModelImpl implements PiPedalModel { }); } - updateVst3State(pedalBoard: PedalBoard) - { + updateVst3State(pedalBoard: PedalBoard) { // let it = pedalBoard.itemsGenerator(); // while (true) { // let v = it.next(); @@ -1862,6 +1885,23 @@ class PiPedalModelImpl implements PiPedalModel { } } + setSystemMidiBinding(instanceId: number, midiBinding: MidiBinding): void { + let currentBindings = this.systemMidiBindings.get(); + + let result: MidiBinding[] = []; + + for (var binding of currentBindings) + { + if (binding.symbol === midiBinding.symbol) + { + result.push(midiBinding); + } else { + result.push(binding); + } + } + this.systemMidiBindings.set(result); + this.webSocket?.send("setSystemMidiBindings",result); + } midiListeners: MidiEventListener[] = []; atomOutputListeners: AtomOutputListener[] = []; @@ -1918,8 +1958,7 @@ class PiPedalModelImpl implements PiPedalModel { break; } } - if (!found) - { + if (!found) { console.log('cancelListenForMidiEvent: event not found.'); } this.webSocket?.send("cancelListenForMidiEvent", listenHandle._handle); @@ -2101,7 +2140,7 @@ class PiPedalModelImpl implements PiPedalModel { wifiDirectConfigSettings = wifiDirectConfigSettings.clone(); if ((!oldSettings.enable) && (!wifiDirectConfigSettings.enable) - && (oldSettings.hotspotName === wifiDirectConfigSettings.hotspotName) + && (oldSettings.hotspotName === wifiDirectConfigSettings.hotspotName) ) { // no effective change. resolve(); @@ -2183,19 +2222,14 @@ class PiPedalModelImpl implements PiPedalModel { } zoomUiControl(sourceElement: HTMLElement, instanceId: number, uiControl: UiControl): void { let name = uiControl.name; - if (uiControl.port_group !== "") - { + if (uiControl.port_group !== "") { let pedalboard = this.pedalBoard.get(); - if (pedalboard) - { + if (pedalboard) { let plugin = pedalboard.getItem(instanceId); let uiPlugin = this.getUiPlugin(plugin.uri); - if (uiPlugin) - { - for (let i = 0; i < uiPlugin.port_groups.length; ++i) - { - if (uiPlugin.port_groups[i].symbol === uiControl.port_group) - { + if (uiPlugin) { + for (let i = 0; i < uiPlugin.port_groups.length; ++i) { + if (uiPlugin.port_groups[i].symbol === uiControl.port_group) { name = uiPlugin.port_groups[i].name + " / " + name; break; } @@ -2205,8 +2239,7 @@ class PiPedalModelImpl implements PiPedalModel { } this.zoomedUiControl.set({ source: sourceElement, name: name, instanceId: instanceId, uiControl: uiControl }); } - onPreviousZoomedControl() : void - { + onPreviousZoomedControl(): void { let currentUiControl = this.zoomedUiControl.get(); if (!currentUiControl) return; @@ -2222,8 +2255,7 @@ class PiPedalModelImpl implements PiPedalModel { let i = 0; let ix = -1; - for (i = 0; i < uiPlugin.controls.length; ++i) - { + for (i = 0; i < uiPlugin.controls.length; ++i) { if (uiPlugin.controls[i].symbol === currentSymbol) { ix = i; break; @@ -2234,10 +2266,9 @@ class PiPedalModelImpl implements PiPedalModel { ++ix; if (ix >= uiPlugin.controls.length) return; - this.zoomUiControl(currentUiControl.source,currentUiControl.instanceId,uiPlugin.controls[ix]); + this.zoomUiControl(currentUiControl.source, currentUiControl.instanceId, uiPlugin.controls[ix]); } - onNextZoomedControl() : void - { + onNextZoomedControl(): void { let currentUiControl = this.zoomedUiControl.get(); if (!currentUiControl) return; @@ -2253,8 +2284,7 @@ class PiPedalModelImpl implements PiPedalModel { let i = 0; let ix = -1; - for (i = 0; i < uiPlugin.controls.length; ++i) - { + for (i = 0; i < uiPlugin.controls.length; ++i) { if (uiPlugin.controls[i].symbol === currentSymbol) { ix = i; break; @@ -2264,8 +2294,8 @@ class PiPedalModelImpl implements PiPedalModel { --ix; if (ix < 0) return; - - this.zoomUiControl(currentUiControl.source,currentUiControl.instanceId,uiPlugin.controls[ix]); + + this.zoomUiControl(currentUiControl.source, currentUiControl.instanceId, uiPlugin.controls[ix]); } clearZoomedControl(): void { diff --git a/react/src/SettingsDialog.tsx b/react/src/SettingsDialog.tsx index 31c55f0..e23dfd7 100644 --- a/react/src/SettingsDialog.tsx +++ b/react/src/SettingsDialog.tsx @@ -44,6 +44,7 @@ import WifiDirectConfigDialog from './WifiDirectConfigDialog'; import DialogEx from './DialogEx' import GovernorSettings from './GovernorSettings'; import { AlsaMidiDeviceInfo } from './AlsaMidiDeviceInfo'; +import SystemMidiBindingsDialog from './SystemMidiBindingsDialog'; import Slide, { SlideProps } from '@mui/material/Slide'; import { createStyles, Theme } from '@mui/material/styles'; @@ -84,6 +85,7 @@ interface SettingsDialogState { isAndroidHosted: boolean; showRestartOkDialog: boolean; showShutdownOkDialog: boolean; + showSystemMidiBindingsDialog: boolean; }; @@ -182,6 +184,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( restarting: false, showShutdownOkDialog: false, showRestartOkDialog: false, + showSystemMidiBindingsDialog: false, isAndroidHosted: this.model.isAndroidHosted() @@ -376,6 +379,12 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( showMidiSelectDialog: true }) } + handleMidiMessageSettings() { + this.setState({ + showSystemMidiBindingsDialog: true + }); + + } handleJackServerSettings() { this.setState({ showJackServerSettingsDialog: true, @@ -634,6 +643,14 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( {this.midiSummary()} + this.handleMidiMessageSettings()} > + +
+ System MIDI Bindings + +
+
+ @@ -840,6 +857,10 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( onOk={() => { this.setState({ showShutdownOkDialog: false }); this.handleShutdownOk(); }} onClose={() => { this.setState({ showShutdownOkDialog: false }); }} /> + { this.setState({showSystemMidiBindingsDialog: false});}} + /> ); diff --git a/react/src/SystemMidiBindingView.tsx b/react/src/SystemMidiBindingView.tsx new file mode 100644 index 0000000..a0e2b9f --- /dev/null +++ b/react/src/SystemMidiBindingView.tsx @@ -0,0 +1,218 @@ +/* + * MIT License + * + * Copyright (c) 2022-2023 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 { Component } from 'react'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import { Theme } from '@mui/material/styles'; +import { WithStyles } from '@mui/styles'; +import withStyles from '@mui/styles/withStyles'; +import createStyles from '@mui/styles/createStyles'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import MidiBinding from './MidiBinding'; +import Utility from './Utility'; +import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'; +import MicOutlinedIcon from '@mui/icons-material/MicOutlined'; +import IconButton from '@mui/material/IconButton'; + + + +const styles = (theme: Theme) => createStyles({ + controlDiv: { flex: "0 0 auto", marginRight: 12, verticalAlign: "center", height: 48, paddingTop: 8, paddingBottom: 8 }, + controlDiv2: { + flex: "0 0 auto", marginRight: 12, verticalAlign: "center", + height: 48, paddingTop: 0, paddingBottom: 0, whiteSpace: "nowrap" + } +}); + +interface SystemMidiBindingViewProps extends WithStyles { + instanceId: number; + listen: boolean; + midiBinding: MidiBinding; + onChange: (instanceId: number, newBinding: MidiBinding) => void; + onListen: (instanceId: number, key: string, listenForControl: boolean) => void; +} + + +interface SystemMidiBindingViewState { +} + + + + +const SystemMidiBindingView = + withStyles(styles, { withTheme: true })( + class extends Component { + + model: PiPedalModel; + + constructor(props: SystemMidiBindingViewProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); + this.state = { + }; + } + + handleTypeChange(e: any, extra: any) { + let newValue = parseInt(e.target.value); + let newBinding = this.props.midiBinding.clone(); + newBinding.bindingType = newValue; + this.props.onChange(this.props.instanceId, newBinding); + } + handleNoteChange(e: any, extra: any) { + let newValue = parseInt(e.target.value); + let newBinding = this.props.midiBinding.clone(); + newBinding.note = newValue; + this.props.onChange(this.props.instanceId, newBinding); + } + handleControlChange(e: any, extra: any) { + let newValue = parseInt(e.target.value); + let newBinding = this.props.midiBinding.clone(); + newBinding.control = newValue; + this.props.onChange(this.props.instanceId, newBinding); + } + handleLatchControlTypeChange(e: any, extra: any) { + let newValue = parseInt(e.target.value); + let newBinding = this.props.midiBinding.clone(); + newBinding.switchControlType = newValue; + this.props.onChange(this.props.instanceId, newBinding); + } + handleLinearControlTypeChange(e: any, extra: any) { + let newValue = parseInt(e.target.value); + let newBinding = this.props.midiBinding.clone(); + newBinding.linearControlType = newValue; + this.props.onChange(this.props.instanceId, newBinding); + } + handleMinChange(value: number): void { + let newBinding = this.props.midiBinding.clone(); + newBinding.minValue = value; + this.props.onChange(this.props.instanceId, newBinding); + } + handleMaxChange(value: number): void { + let newBinding = this.props.midiBinding.clone(); + newBinding.maxValue = value; + this.props.onChange(this.props.instanceId, newBinding); + } + handleScaleChange(value: number): void { + let newBinding = this.props.midiBinding.clone(); + newBinding.rotaryScale = value; + this.props.onChange(this.props.instanceId, newBinding); + } + + + generateMidiSelects(): React.ReactNode[] { + let result: React.ReactNode[] = []; + + for (let i = 0; i < 127; ++i) { + result.push( + {Utility.midiNoteName(i)} + ) + } + + return result; + } + generateControlSelects(): React.ReactNode[] { + + return Utility.validMidiControllers.map((control) => ( + {control.displayName} + ) + ); + } + + + render() { + let classes = this.props.classes; + let midiBinding = this.props.midiBinding; + + return ( +
+
+ +
+ { + (midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) && + ( +
+ +
+ ) + } + { + (midiBinding.bindingType === MidiBinding.BINDING_TYPE_CONTROL) && + ( +
+ + +
+ ) + } + { + if (this.props.listen) { + this.props.onListen(-2, "", false) + } else { + this.props.onListen(this.props.instanceId, this.props.midiBinding.symbol, false) + } + }} + size="large"> + {this.props.listen ? ( + + ) : ( + + )} + +
+ ); + + } + }); + +export default SystemMidiBindingView; \ No newline at end of file diff --git a/react/src/SystemMidiBindingsDialog.tsx b/react/src/SystemMidiBindingsDialog.tsx new file mode 100644 index 0000000..ce9f417 --- /dev/null +++ b/react/src/SystemMidiBindingsDialog.tsx @@ -0,0 +1,338 @@ +/* + * MIT License + * + * Copyright (c) 2022-2023 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, { SyntheticEvent } from 'react'; +import DialogEx from './DialogEx'; +import ResizeResponsiveComponent from './ResizeResponsiveComponent'; +import { PiPedalModel, PiPedalModelFactory, ListenHandle } from './PiPedalModel'; +import Typography from '@mui/material/Typography'; +import { Theme } from '@mui/material/styles'; +import { WithStyles } from '@mui/styles'; +import createStyles from '@mui/styles/createStyles'; +import withStyles from '@mui/styles/withStyles'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import IconButton from '@mui/material/IconButton'; +import MidiBinding from './MidiBinding'; +import SystemMidiBindingView from './SystemMidiBindingView'; +import Snackbar from '@mui/material/Snackbar'; + +const styles = (theme: Theme) => createStyles({ + dialogAppBar: { + position: 'relative', + top: 0, left: 0 + }, + dialogTitle: { + marginLeft: theme.spacing(2), + flex: "1 1 auto", + }, + pluginTable: { + border: "collapse", + width: "100%", + + }, + pluginHead: { + borderTop: "1pt #DDD solid", paddingLeft: 22, paddingRight: 22 + }, + bindingTd: { + verticalAlign: "top", + paddingLeft: 12, paddingBottom: 8 + }, + nameTd: { + paddingLeft: 16, + verticalAlign: "top", + paddingTop: 12 + }, +}); + + +export interface SystemMidiBindingDialogProps extends WithStyles { + open: boolean, + onClose: () => void +} + +class BindingEntry { + constructor( + displayName: string, + instanceId: number, + midiBinding: MidiBinding, + ) { + this.displayName = displayName; + this.instanceId = instanceId; + this.midiBinding = midiBinding; + } + displayName: string; + instanceId: number; + midiBinding: MidiBinding; +} + +export interface SystemMidiBindingDialogState { + listenInstanceId: number; + listenSymbol: string; + listenSnackbarOpen: boolean; + systemMidiBindings: BindingEntry[]; + +} + +export const SystemMidiBindingDialog = + withStyles(styles, { withTheme: true })( + class extends ResizeResponsiveComponent { + + model: PiPedalModel; + + constructor(props: SystemMidiBindingDialogProps) { + super(props); + this.model = PiPedalModelFactory.getInstance(); + this.state = { + listenInstanceId: -2, + listenSymbol: "", + listenSnackbarOpen: false, + systemMidiBindings: this.createBindings() + }; + this.handleClose = this.handleClose.bind(this); + this.onMidiBindingsChanged = this.onMidiBindingsChanged.bind(this); + } + + createBindings(): BindingEntry[] { + let result: BindingEntry[] = []; + for (var item of this.model.systemMidiBindings.get()) + { + let displayName = ""; + let instanceId = -1; + + if (item.symbol === "prevProgram") + { + displayName = "Previous Preset"; + instanceId = 1; + } else if (item.symbol === "nextProgram") + { + displayName = "Next Preset"; + instanceId = 2; + } + if (instanceId !== -1) + { + result.push(new BindingEntry(displayName,instanceId,item)); + } + } + return result; + } + mounted: boolean = false; + + hasHooks: boolean = false; + + handleClose() { + this.cancelListenForControl(); + this.props.onClose(); + } + + listenTimeoutHandle?: NodeJS.Timeout; + + listenHandle?: ListenHandle; + + cancelListenForControl() { + if (this.listenTimeoutHandle) { + clearTimeout(this.listenTimeoutHandle); + this.listenTimeoutHandle = undefined; + } + if (this.listenHandle) + { + this.model.cancelListenForMidiEvent(this.listenHandle) + this.listenHandle = undefined; + } + + this.setState({ listenInstanceId: -2, listenSymbol: "" }); + + } + + handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number) + { + this.cancelListenForControl(); + + for (var binding of this.state.systemMidiBindings) + { + if (binding.instanceId === instanceId) + { + let newBinding = binding.midiBinding.clone(); + + if (isNote) { + newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE; + newBinding.note = noteOrControl; + } else { + newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL; + newBinding.control = noteOrControl; + } + + this.model.setSystemMidiBinding(instanceId,newBinding); + return; + } + } + } + + + handleListenForControl(instanceId: number, symbol: string, listenForControl: boolean): void { + this.cancelListenForControl(); + this.setState({ listenInstanceId: instanceId, listenSymbol: symbol, listenSnackbarOpen: true }); + this.listenTimeoutHandle = setTimeout(() => { + this.cancelListenForControl(); + }, 8000); + + this.listenHandle = this.model.listenForMidiEvent(listenForControl, + (isNote: boolean, noteOrControl: number) => { + this.handleListenSucceeded(instanceId,symbol,isNote, noteOrControl); + }); + + + + } + + onWindowSizeChanged(width: number, height: number): void { + } + + onMidiBindingsChanged() { + this.setState({systemMidiBindings: this.createBindings()}); + } + + componentDidMount() { + super.componentDidMount(); + this.mounted = true; + + this.model.systemMidiBindings.addOnChangedHandler(this.onMidiBindingsChanged); + this.onMidiBindingsChanged(); + } + componentWillUnmount() { + super.componentWillUnmount(); + + this.mounted = false; + this.model.systemMidiBindings.removeOnChangedHandler(this.onMidiBindingsChanged); + } + + componentDidUpdate() { + } + handleItemChanged(instanceId: number, newBinding: MidiBinding) { + this.model.setSystemMidiBinding(instanceId, newBinding); + } + generateTable(): React.ReactNode[] { + let classes = this.props.classes; + let result: React.ReactNode[] = []; + + let items = this.state.systemMidiBindings; + + for (var item of items) { + result.push( + + + + {item.displayName} + + + + { + if (instanceId === -2) + { + this.cancelListenForControl(); + } else { + this.handleListenForControl(instanceId, symbol, listenForControl); + } + }} + listen={item.instanceId === this.state.listenInstanceId && this.state.listenSymbol === item.midiBinding.symbol} + onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)} + /> + + + ); + } + + + return result; + } + + supressDefault(e: SyntheticEvent) { + //e.preventDefault(); + //e.stopPropagation(); + } + + render() { + let props = this.props; + let { open, classes } = props; + if (!open) { + return (
); + } + + return ( + +
+
+ + + + + + + System MIDI Bindings + + + +
+
+ + + + + + + + {this.generateTable()} + + +
+
+
+ this.setState({ listenSnackbarOpen: false })} + message="Listening for MIDI input" + /> +
+ ); + } + }); + + +export default SystemMidiBindingDialog; \ No newline at end of file diff --git a/src/JackHost.cpp b/src/AudioHost.cpp similarity index 72% rename from src/JackHost.cpp rename to src/AudioHost.cpp index 57186fc..e6b29b0 100644 --- a/src/JackHost.cpp +++ b/src/AudioHost.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2022-2023 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 @@ -17,19 +17,17 @@ // 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 "JackHost.hpp" +#include "AudioHost.hpp" #include "Lv2Log.hpp" #include "JackDriver.hpp" #include "AlsaDriver.hpp" - using namespace pipedal; #include "AudioConfig.hpp" - #include #include #include @@ -61,38 +59,42 @@ using namespace pipedal; #include "AdminClient.hpp" const double VU_UPDATE_RATE_S = 1.0 / 30; -const double OVERRUN_GRACE_PERIOD_S = 15; +const double OVERRUN_GRACE_PERIOD_S = 15; using namespace pipedal; const int MIDI_LV2_BUFFER_SIZE = 16 * 1024; - -static void GetCpuFrequency(uint64_t*freqMin,uint64_t*freqMax) +static void GetCpuFrequency(uint64_t *freqMin, uint64_t *freqMax) { uint64_t fMax = 0; uint64_t fMin = UINT64_MAX; char deviceName[128]; - try { - for (int i = 0; true; ++i) + try { - - snprintf(deviceName,sizeof(deviceName),"/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq",i); - std::ifstream f(deviceName); - if (!f) + for (int i = 0; true; ++i) { - break; - } - uint64_t freq; - f >> freq; - if (!f) break; - if (freq < fMin) fMin = freq; - if (freq > fMax) fMax = freq; - } - } catch (const std::exception &) - { + snprintf(deviceName, sizeof(deviceName), "/sys/devices/system/cpu/cpu%d/cpufreq/scaling_cur_freq", i); + std::ifstream f(deviceName); + if (!f) + { + break; + } + uint64_t freq; + f >> freq; + if (!f) + break; + if (freq < fMin) + fMin = freq; + if (freq > fMax) + fMax = freq; + } } - if (fMin == 0) fMax = 0; + catch (const std::exception &) + { + } + if (fMin == 0) + fMax = 0; *freqMin = fMin; *freqMax = fMax; } @@ -102,85 +104,157 @@ static std::string GetGovernor() } +class SystemMidiBinding { +private: + MidiBinding currentBinding; + bool controlState = false; +public: + void SetBinding(const MidiBinding&binding) { currentBinding = binding; controlState = false; } -class JackHostImpl : public JackHost, private AudioDriverHost + bool IsMatch(const MidiEvent&event); + bool IsTriggered(const MidiEvent&event); +}; + +bool SystemMidiBinding::IsTriggered(const MidiEvent &event) +{ + switch (currentBinding.bindingType()) + { + case BINDING_TYPE_NOTE: + { + if (event.size != 3) return false; + auto command = event.buffer[0] & 0xF0; + if (command != 0x90) return false; // MIDI note on + if (event.buffer[1] != currentBinding.note()) return false; + return event.buffer[2] != 0; // note-on with velicity of zero is a note-off. + } + break; + case BINDING_TYPE_CONTROL: + { + if (event.size != 3) return false; + auto command = event.buffer[0] & 0xF0; + if (command != 0xB0) return false; + if (event.buffer[1] != currentBinding.control()) return false; + bool state = event.buffer[2] >= 0x64; + if (state != this->controlState) + { + this->controlState = state; + return state; + } + return false; + } + break; + default: + return false; + } +} + +bool SystemMidiBinding::IsMatch(const MidiEvent&event) +{ + switch (currentBinding.bindingType()) + { + case BINDING_TYPE_NOTE: + { + if (event.size != 3) return false; + auto command = event.buffer[0] & 0xF0; + if (command != 0x80 && command != 0x90) return false; // note on/note off. + return event.buffer[1] == currentBinding.note(); + } + break; + case BINDING_TYPE_CONTROL: + { + if (event.size != 3) return false; + auto command = event.buffer[0] & 0xF0; + if (command != 0xB0) return false; + return event.buffer[1] == currentBinding.control(); + } + break; + default: + return false; + } +} + +class AudioHostImpl : public AudioHost, private AudioDriverHost { private: IHost *pHost = nullptr; - class Uris + static constexpr size_t DEFERRED_MIDI_BUFFER_SIZE = 1024; + + uint8_t deferredMidiMessages[DEFERRED_MIDI_BUFFER_SIZE]; + size_t deferredMidiMessageCount; + bool midiProgramChangePending = false; + int selectedBank = -1; + int64_t midiProgramChangeId = 0; + + class Uris + { + public: + Uris(IHost *pHost) { - public: - Uris(IHost *pHost) - { - atom_Blank = pHost->GetLv2Urid(LV2_ATOM__Blank); - atom_Path = pHost->GetLv2Urid(LV2_ATOM__Path); - atom_float = pHost->GetLv2Urid(LV2_ATOM__Float); - atom_Double = pHost->GetLv2Urid(LV2_ATOM__Double); - atom_Int = pHost->GetLv2Urid(LV2_ATOM__Int); - atom_Long = pHost->GetLv2Urid(LV2_ATOM__Long); - atom_Bool = pHost->GetLv2Urid(LV2_ATOM__Bool); - atom_String = pHost->GetLv2Urid(LV2_ATOM__String); - atom_Vector = pHost->GetLv2Urid(LV2_ATOM__Vector); - atom_Object = pHost->GetLv2Urid(LV2_ATOM__Object); + atom_Blank = pHost->GetLv2Urid(LV2_ATOM__Blank); + atom_Path = pHost->GetLv2Urid(LV2_ATOM__Path); + atom_float = pHost->GetLv2Urid(LV2_ATOM__Float); + atom_Double = pHost->GetLv2Urid(LV2_ATOM__Double); + atom_Int = pHost->GetLv2Urid(LV2_ATOM__Int); + atom_Long = pHost->GetLv2Urid(LV2_ATOM__Long); + atom_Bool = pHost->GetLv2Urid(LV2_ATOM__Bool); + atom_String = pHost->GetLv2Urid(LV2_ATOM__String); + atom_Vector = pHost->GetLv2Urid(LV2_ATOM__Vector); + atom_Object = pHost->GetLv2Urid(LV2_ATOM__Object); + atom_Sequence = pHost->GetLv2Urid(LV2_ATOM__Sequence); + atom_Chunk = pHost->GetLv2Urid(LV2_ATOM__Chunk); + atom_URID = pHost->GetLv2Urid(LV2_ATOM__URID); + atom_eventTransfer = pHost->GetLv2Urid(LV2_ATOM__eventTransfer); + patch_Get = pHost->GetLv2Urid(LV2_PATCH__Get); + patch_Set = pHost->GetLv2Urid(LV2_PATCH__Set); + patch_Put = pHost->GetLv2Urid(LV2_PATCH__Put); + patch_body = pHost->GetLv2Urid(LV2_PATCH__body); + patch_subject = pHost->GetLv2Urid(LV2_PATCH__subject); + patch_property = pHost->GetLv2Urid(LV2_PATCH__property); + // patch_accept = pHost->GetLv2Urid(LV2_PATCH__accept); + patch_value = pHost->GetLv2Urid(LV2_PATCH__value); + unitsFrame = pHost->GetLv2Urid(LV2_UNITS__frame); + } + // LV2_URID patch_accept; - atom_Sequence = pHost->GetLv2Urid(LV2_ATOM__Sequence); - atom_Chunk = pHost->GetLv2Urid(LV2_ATOM__Chunk); - atom_URID = pHost->GetLv2Urid(LV2_ATOM__URID); - atom_eventTransfer = pHost->GetLv2Urid(LV2_ATOM__eventTransfer); - patch_Get = pHost->GetLv2Urid(LV2_PATCH__Get); - patch_Set = pHost->GetLv2Urid(LV2_PATCH__Set); - patch_Put = pHost->GetLv2Urid(LV2_PATCH__Put); - patch_body = pHost->GetLv2Urid(LV2_PATCH__body); - patch_subject = pHost->GetLv2Urid(LV2_PATCH__subject); - patch_property = pHost->GetLv2Urid(LV2_PATCH__property); - //patch_accept = pHost->GetLv2Urid(LV2_PATCH__accept); - patch_value = pHost->GetLv2Urid(LV2_PATCH__value); - unitsFrame = pHost->GetLv2Urid(LV2_UNITS__frame); + LV2_URID unitsFrame; + LV2_URID pluginUri; + LV2_URID atom_Blank; + LV2_URID atom_Bool; + LV2_URID atom_float; + LV2_URID atom_Double; + LV2_URID atom_Int; + LV2_URID atom_Long; + LV2_URID atom_String; + LV2_URID atom_Object; + LV2_URID atom_Vector; + LV2_URID atom_Path; + LV2_URID atom_Sequence; + LV2_URID atom_Chunk; + LV2_URID atom_URID; + LV2_URID atom_eventTransfer; + LV2_URID midi_Event; + LV2_URID patch_Get; + LV2_URID patch_Set; + LV2_URID patch_Put; + LV2_URID patch_body; + LV2_URID patch_subject; + LV2_URID patch_property; + LV2_URID patch_value; + LV2_URID param_uiState; + }; - } - //LV2_URID patch_accept; + Uris uris; - LV2_URID unitsFrame; - LV2_URID pluginUri; - LV2_URID atom_Blank; - LV2_URID atom_Bool; - LV2_URID atom_float; - LV2_URID atom_Double; - LV2_URID atom_Int; - LV2_URID atom_Long; - LV2_URID atom_String; - LV2_URID atom_Object; - LV2_URID atom_Vector; - LV2_URID atom_Path; - LV2_URID atom_Sequence; - LV2_URID atom_Chunk; - LV2_URID atom_URID; - LV2_URID atom_eventTransfer; - LV2_URID midi_Event; - LV2_URID patch_Get; - LV2_URID patch_Set; - LV2_URID patch_Put; - LV2_URID patch_body; - LV2_URID patch_subject; - LV2_URID patch_property; - LV2_URID patch_value; - LV2_URID param_uiState; - }; - - Uris uris; - - - - AudioDriver*audioDriver = nullptr; + AudioDriver *audioDriver = nullptr; inherit_priority_recursive_mutex mutex; int64_t overrunGracePeriodSamples = 0; - IJackHostCallbacks *pNotifyCallbacks = nullptr; + IAudioHostCallbacks *pNotifyCallbacks = nullptr; - virtual void SetNotificationCallbacks(IJackHostCallbacks *pNotifyCallbacks) + virtual void SetNotificationCallbacks(IAudioHostCallbacks *pNotifyCallbacks) { this->pNotifyCallbacks = pNotifyCallbacks; } @@ -190,13 +264,16 @@ private: RingBuffer inputRingBuffer; RingBuffer outputRingBuffer; - RingBufferWriter x; + RingBufferWriter x; RealtimeRingBufferReader realtimeReader; RealtimeRingBufferWriter realtimeWriter; HostRingBufferReader hostReader; HostRingBufferWriter hostWriter; + SystemMidiBinding nextMidiBinding; + SystemMidiBinding prevMidiBinding; + JackChannelSelection channelSelection; bool active = false; @@ -213,27 +290,25 @@ private: std::atomic lastUnderrunTime = std::chrono::system_clock::from_time_t(0); - - std::string GetAtomObjectType(uint8_t*pData) + std::string GetAtomObjectType(uint8_t *pData) { - LV2_Atom_Object *pAtom = (LV2_Atom_Object*)pData; + LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; if (pAtom->atom.type != uris.atom_Object) { throw std::invalid_argument("Not an Lv2 Object"); } return pHost->Lv2UriudToString(pAtom->body.otype); - } - void WriteAtom(json_writer &writer, LV2_Atom*pAtom); + void WriteAtom(json_writer &writer, LV2_Atom *pAtom); - std::string AtomToJson(uint8_t*pData) + std::string AtomToJson(uint8_t *pData) { std::stringstream s; json_writer writer(s); - LV2_Atom *pAtom = (LV2_Atom*)pData; + LV2_Atom *pAtom = (LV2_Atom *)pData; - WriteAtom(writer,pAtom); + WriteAtom(writer, pAtom); return s.str(); } @@ -243,7 +318,6 @@ private: this->lastUnderrunTime = std::chrono::system_clock ::now(); } - virtual void Close() { std::lock_guard guard(mutex); @@ -286,13 +360,11 @@ private: this->inputRingBuffer.reset(); this->outputRingBuffer.reset(); - for (size_t i = 0; i < midiLv2Buffers.size(); ++i) { delete[] midiLv2Buffers[i]; } midiLv2Buffers.resize(0); - } void ZeroBuffer(float *buffer, size_t nframes) @@ -341,6 +413,9 @@ private: } } + + virtual void SetSystemMidiBindings(const std::vector&bindings); + void writeVu() { // throttling: we send one; but won't send another until the host thread @@ -459,6 +534,16 @@ private: this->realtimeMonitorPortSubscriptions = pSubscriptions; break; } + case RingBufferCommand::AckMidiProgramChange: + { + int64_t requestId; + realtimeReader.readComplete(&requestId); + if (requestId == this->midiProgramChangeId) + { + this->midiProgramChangePending = false; + } + break; + } case RingBufferCommand::SetVuSubscriptions: { RealtimeVuBuffers *configuration; @@ -485,14 +570,17 @@ private: ReplaceEffectBody body; realtimeReader.readComplete(&body); - auto oldValue = this->realtimeActivePedalBoard; - this->realtimeActivePedalBoard = body.effect; + if (body.effect != nullptr) + { + auto oldValue = this->realtimeActivePedalBoard; + this->realtimeActivePedalBoard = body.effect; - realtimeWriter.EffectReplaced(oldValue); + realtimeWriter.EffectReplaced(oldValue); - // invalidate the possibly no-good subscriptions. Model will update them shortly. - freeRealtimeVuConfiguration(); - freeRealtimeMonitorPortSubscriptions(); + // invalidate the possibly no-good subscriptions. Model will update them shortly. + freeRealtimeVuConfiguration(); + freeRealtimeMonitorPortSubscriptions(); + } break; } default: @@ -502,55 +590,138 @@ private: reEntered = false; } + virtual void AckMidiProgramRequest(uint64_t requestId) + { + hostWriter.AckMidiProgramRequest(requestId); + } + void OnMidiValueChanged(uint64_t instanceId, int controlIndex, float value) { realtimeWriter.MidiValueChanged(instanceId, controlIndex, value); } static void fnMidiValueChanged(void *data, uint64_t instanceId, int controlIndex, float value) { - ((JackHostImpl *)data)->OnMidiValueChanged(instanceId, controlIndex, value); + ((AudioHostImpl *)data)->OnMidiValueChanged(instanceId, controlIndex, value); } - void ProcessJackMidi() + static bool isBankChange(MidiEvent &event) + { + return (event.size == 3 && event.buffer[0] == 0xB0 && event.buffer[1] == 0x00); + } + + bool onMidiEvent(Lv2EventBufferWriter&eventBufferWriter, Lv2EventBufferWriter::LV2_EvBuf_Iterator &iterator, MidiEvent&event) + { + eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer); + + this->realtimeActivePedalBoard->OnMidiMessage(event.size, event.buffer, this, fnMidiValueChanged); + if (listenForMidiEvent) + { + if (event.size >= 3) + { + uint8_t cmd = (uint8_t)(event.buffer[0] & 0xF0); + bool isNote = cmd == 0x90; + bool isControl = cmd == 0xB0; + if (isNote || isControl) + { + realtimeWriter.OnMidiListen(isNote, event.buffer[1]); + } + } + } + return true; + } + + + void ProcessMidiInput() { Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids); - size_t midiInputBufferCount = audioDriver->MidiInputBufferCount(); + size_t midiInputBufferCount = audioDriver->MidiInputBufferCount(); audioDriver->FillMidiBuffers(); - - for (size_t i = 0; i < midiInputBufferCount; ++i) + + for (size_t midiDeviceIx = 0; midiDeviceIx < midiInputBufferCount; ++midiDeviceIx) { - - void *portBuffer = audioDriver->GetMidiInputBuffer(i,0); + + void *portBuffer = audioDriver->GetMidiInputBuffer(midiDeviceIx, 0); if (portBuffer) { - uint8_t *lv2Buffer = this->midiLv2Buffers[i]; + uint8_t *lv2Buffer = this->midiLv2Buffers[midiDeviceIx]; size_t n = audioDriver->GetMidiInputEventCount(portBuffer); eventBufferWriter.Reset(lv2Buffer, MIDI_LV2_BUFFER_SIZE); - auto iterator = eventBufferWriter.begin(); + Lv2EventBufferWriter::LV2_EvBuf_Iterator iterator = eventBufferWriter.begin(); + MidiEvent event; + + + // write all deferred midi messages. + if (deferredMidiMessageCount != 0 && !midiProgramChangePending) + { + for (size_t i = 0; i < deferredMidiMessageCount; /**/) + { + int8_t deviceIndex = deferredMidiMessages[i++]; + int8_t messageCount = deferredMidiMessages[i++]; + if (deviceIndex == midiDeviceIx) + { + event.size = messageCount; + event.buffer = deferredMidiMessages +i; + event.time = 0; + + onMidiEvent(eventBufferWriter,iterator,event); + } + + i += messageCount; + } + } + for (size_t frame = 0; frame < n; ++frame) { - MidiEvent event; - if (audioDriver->GetMidiInputEvent(&event,portBuffer,frame)) + if (audioDriver->GetMidiInputEvent(&event, portBuffer, frame)) { - eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer); - - this->realtimeActivePedalBoard->OnMidiMessage(event.size, event.buffer, this, fnMidiValueChanged); - if (listenForMidiEvent) + uint8_t midiCommand = (uint8_t)(event.buffer[0] & 0xF0); + if (midiCommand == 0xC0) // midi program change. { - if (event.size >= 3) + this->deferredMidiMessageCount = 0; // we can discard previous control changes. + midiProgramChangePending = true; + + this->realtimeWriter.OnMidiProgramChange(++(this->midiProgramChangeId), selectedBank,event.buffer[1]); + } + else if (isBankChange(event)) + { + this->selectedBank = event.buffer[2]; + } else if (this->nextMidiBinding.IsMatch(event)) + { + if (nextMidiBinding.IsTriggered(event)) { - uint8_t cmd = (uint8_t)(event.buffer[0] & 0xF0); - bool isNote = cmd == 0x90; - bool isControl = cmd == 0xB0; - if (isNote || isControl) + midiProgramChangePending = true; + this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId),1); + } + } else if (this->prevMidiBinding.IsMatch(event)) + { + if (prevMidiBinding.IsTriggered(event)) + { + midiProgramChangePending = true; + this->realtimeWriter.OnNextMidiProgram(++(this->midiProgramChangeId),-1); + } + } + else if (midiProgramChangePending) + { + // defer the message for processing after the program change has completed. + if (event.size > 0 + && event.size < 128 + && event.size + 2 + this->deferredMidiMessageCount < DEFERRED_MIDI_BUFFER_SIZE) + { + this->deferredMidiMessages[deferredMidiMessageCount++] = midiDeviceIx; + this->deferredMidiMessages[deferredMidiMessageCount++] = (uint8_t)event.size; + for (size_t i = 0; i < event.size; ++i) { - realtimeWriter.OnMidiListen(isNote, event.buffer[1]); + this->deferredMidiMessages[deferredMidiMessageCount++] = event.buffer[i]; } } } + else + { + onMidiEvent(eventBufferWriter,iterator,event); + } } } } @@ -563,15 +734,14 @@ private: bool IsAudioActive() { - std::lock_guard lock { audioStoppedMutex}; + std::lock_guard lock{audioStoppedMutex}; return this->active; } virtual void OnAudioStopped() { - std::lock_guard lock { audioStoppedMutex}; + std::lock_guard lock{audioStoppedMutex}; this->active = false; Lv2Log::info("Audio stopped."); - } virtual void OnProcess(size_t nframes) @@ -589,7 +759,7 @@ private: Lv2PedalBoard *pedalBoard = this->realtimeActivePedalBoard; if (pedalBoard != nullptr) { - ProcessJackMidi(); + ProcessMidiInput(); float *inputBuffers[4]; float *outputBuffers[4]; bool buffersValid = true; @@ -607,7 +777,7 @@ private: for (int i = 0; i < audioDriver->OutputBufferCount(); ++i) { - float *output = audioDriver->GetOutputBuffer(i,nframes); + float *output = audioDriver->GetOutputBuffer(i, nframes); if (output == nullptr) { buffersValid = false; @@ -622,7 +792,7 @@ private: pedalBoard->ResetAtomBuffers(); pedalBoard->ProcessParameterRequests(pParameterRequests); - processed = pedalBoard->Run(inputBuffers, outputBuffers, (uint32_t)nframes,&realtimeWriter); + processed = pedalBoard->Run(inputBuffers, outputBuffers, (uint32_t)nframes, &realtimeWriter); if (processed) { if (this->realtimeVuBuffers != nullptr) @@ -664,18 +834,16 @@ private: this->underruns = 0; } this->currentSample += nframes; - } catch (const std::exception &e) { Lv2Log::error("Fatal error while processing jack audio. (%s)", e.what()); throw; } - } - + public: - JackHostImpl(IHost *pHost) + AudioHostImpl(IHost *pHost) : inputRingBuffer(RING_BUFFER_SIZE), outputRingBuffer(RING_BUFFER_SIZE), realtimeReader(&this->inputRingBuffer), @@ -687,21 +855,19 @@ public: uris(pHost) { - #if JACK_HOST +#if JACK_HOST audioDriver = CreateJackDriver(this); - #endif - #if ALSA_HOST +#endif +#if ALSA_HOST audioDriver = CreateAlsaDriver(this); - #endif - +#endif } - virtual ~JackHostImpl() + virtual ~AudioHostImpl() { Close(); CleanRestartThreads(true); delete audioDriver; - } virtual JackConfiguration GetServerConfiguration() @@ -717,6 +883,8 @@ public: return this->sampleRate; } + + void OnAudioComplete() { // there is actually no compelling circumstance in which this should ever happen. @@ -878,7 +1046,8 @@ public: this->pNotifyCallbacks->OnNotifyVusSubscription(*updates); } this->hostWriter.AckVuUpdate(); // please sir, can I have some more? - } else if (command == RingBufferCommand::AtomOutput) + } + else if (command == RingBufferCommand::AtomOutput) { uint64_t instanceId; hostReader.read(&instanceId); @@ -888,15 +1057,14 @@ public: { atomBuffer.resize(extraBytes); } - hostReader.read(extraBytes,&(atomBuffer[0])); + hostReader.read(extraBytes, &(atomBuffer[0])); - IEffect *pEffect = currentPedalBoard->GetEffect(instanceId); - if (pEffect != nullptr &&this->pNotifyCallbacks && listenForAtomOutput) + if (pEffect != nullptr && this->pNotifyCallbacks && listenForAtomOutput) { std::string atomType = GetAtomObjectType(&atomBuffer[0]); - auto json = AtomToJson(&(atomBuffer[0])); - this->pNotifyCallbacks->OnNotifyAtomOutput(instanceId,atomType,json); + auto json = AtomToJson(&(atomBuffer[0])); + this->pNotifyCallbacks->OnNotifyAtomOutput(instanceId, atomType, json); } } else if (command == RingBufferCommand::FreeVuSubscriptions) @@ -923,6 +1091,16 @@ public: hostReader.read(&body); OnAudioComplete(); return; + } else if (command == RingBufferCommand::MidiProgramChange) + { + RealtimeMidiProgramRequest programRequest; + hostReader.read(&programRequest); + OnMidiProgramRequest(programRequest); + } else if (command == RingBufferCommand::NextMidiProgram) + { + RealtimeNextMidiProgramRequest request; + hostReader.read(&request); + pNotifyCallbacks->OnNotifyNextMidiProgram(request); } else { @@ -982,13 +1160,11 @@ public: return result; } - - virtual void Open(const JackServerSettings &jackServerSettings,const JackChannelSelection &channelSelection) + virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) { std::lock_guard guard(mutex); - if (channelSelection.GetInputAudioPorts().size() == 0 - || channelSelection.GetOutputAudioPorts().size() == 0) + if (channelSelection.GetInputAudioPorts().size() == 0 || channelSelection.GetOutputAudioPorts().size() == 0) { return; } @@ -1009,27 +1185,25 @@ public: StartReaderThread(); - try { + try + { - audioDriver->Open(jackServerSettings,this->channelSelection); + audioDriver->Open(jackServerSettings, this->channelSelection); this->sampleRate = audioDriver->GetSampleRate(); - this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate)*OVERRUN_GRACE_PERIOD_S); + this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate) * OVERRUN_GRACE_PERIOD_S); this->vuSamplesPerUpdate = (size_t)(sampleRate * VU_UPDATE_RATE_S); - midiLv2Buffers.resize(audioDriver->MidiInputBufferCount()); for (size_t i = 0; i < audioDriver->MidiInputBufferCount(); ++i) { midiLv2Buffers[i] = AllocateRealtimeBuffer(MIDI_LV2_BUFFER_SIZE); } - active = true; audioDriver->Activate(); Lv2Log::info("Audio started."); - } catch (const std::exception &e) { @@ -1040,6 +1214,10 @@ public: } } + void OnMidiProgramRequest(RealtimeMidiProgramRequest &programRequest) + { + pNotifyCallbacks->OnNotifyMidiProgramChange(programRequest); + } void OnActivePedalBoardReleased(Lv2PedalBoard *pPedalBoard) { if (pPedalBoard) @@ -1107,7 +1285,7 @@ public: } } - void SetControlValue(uint64_t instanceId, const std::string &symbol, float value) + void SetControlValue(uint64_t instanceId, const std::string &symbol, float value) { std::lock_guard guard(mutex); if (active && this->currentPedalBoard) @@ -1207,7 +1385,7 @@ public: private: class RestartThread { - JackHostImpl *this_; + AudioHostImpl *this_; JackServerSettings jackServerSettings; std::function onComplete; std::atomic isComplete = false; @@ -1215,7 +1393,7 @@ private: public: RestartThread( - JackHostImpl *host, + AudioHostImpl *host, const JackServerSettings &jackServerSettings_, std::function onComplete_) : this_(host), @@ -1234,19 +1412,20 @@ private: { this_->restarting = true; // this_->Close(); (JackServerConfiguration now does a service restart.) - try { + try + { AdminClient client; client.SetJackServerConfiguration(jackServerSettings); - - //this_->Open(this_->channelSelection); + // this_->Open(this_->channelSelection); this_->restarting = false; onComplete(true, ""); isComplete = true; - } catch (const std::exception &e) + } + catch (const std::exception &e) { - onComplete(false,e.what()); + onComplete(false, e.what()); this_->restarting = false; isComplete = true; } @@ -1349,7 +1528,7 @@ public: { result.cpuUsage_ = audioDriver->CpuUse(); } - GetCpuFrequency(&result.cpuFreqMax_,&result.cpuFreqMin_); + GetCpuFrequency(&result.cpuFreqMax_, &result.cpuFreqMin_); result.governor_ = GetGovernor(); return result; @@ -1367,7 +1546,8 @@ public: } }; -static std::string UriToFieldName(const std::string&uri){ +static std::string UriToFieldName(const std::string &uri) +{ int pos; for (pos = uri.length(); pos >= 0; --pos) { @@ -1377,10 +1557,10 @@ static std::string UriToFieldName(const std::string&uri){ break; } } - return uri.substr(pos+1); + return uri.substr(pos + 1); } -void JackHostImpl::WriteAtom(json_writer &writer, LV2_Atom*pAtom) +void AudioHostImpl::WriteAtom(json_writer &writer, LV2_Atom *pAtom) { if (pAtom->type == uris.atom_Blank) { @@ -1389,86 +1569,95 @@ void JackHostImpl::WriteAtom(json_writer &writer, LV2_Atom*pAtom) else if (pAtom->type == uris.atom_float) { writer.write( - ((LV2_Atom_Float*)pAtom)->body - ); - - } else if (pAtom->type == uris.atom_Int) + ((LV2_Atom_Float *)pAtom)->body); + } + else if (pAtom->type == uris.atom_Int) { writer.write( - ((LV2_Atom_Int*)pAtom)->body - ); - } else if (pAtom->type == uris.atom_Long) + ((LV2_Atom_Int *)pAtom)->body); + } + else if (pAtom->type == uris.atom_Long) { writer.write( - ((LV2_Atom_Long*)pAtom)->body - ); - } else if (pAtom->type == uris.atom_Double) + ((LV2_Atom_Long *)pAtom)->body); + } + else if (pAtom->type == uris.atom_Double) { writer.write( - ((LV2_Atom_Double*)pAtom)->body - ); - } else if (pAtom->type == uris.atom_Bool) + ((LV2_Atom_Double *)pAtom)->body); + } + else if (pAtom->type == uris.atom_Bool) { writer.write( - ((LV2_Atom_Bool*)pAtom)->body - ); - } else if (pAtom->type == uris.atom_String) + ((LV2_Atom_Bool *)pAtom)->body); + } + else if (pAtom->type == uris.atom_String) { - const char *p = (((const char*) pAtom) + sizeof(LV2_Atom_String)); + const char *p = (((const char *)pAtom) + sizeof(LV2_Atom_String)); writer.write( - p - ); - } else if (pAtom->type == uris.atom_Vector) + p); + } + else if (pAtom->type == uris.atom_Vector) { - LV2_Atom_Vector *pVector = (LV2_Atom_Vector*)pAtom; + LV2_Atom_Vector *pVector = (LV2_Atom_Vector *)pAtom; writer.start_array(); { - size_t n = (pAtom->size-sizeof(pVector->body))/pVector->body.child_size; - char *pItems = ((char*)pAtom) + sizeof(LV2_Atom_Vector); + size_t n = (pAtom->size - sizeof(pVector->body)) / pVector->body.child_size; + char *pItems = ((char *)pAtom) + sizeof(LV2_Atom_Vector); if (pVector->body.child_type == uris.atom_float) { - float *p = (float*)pItems; + float *p = (float *)pItems; for (size_t i = 0; i < n; ++i) { - if (i != 0) writer.write_raw(","); + if (i != 0) + writer.write_raw(","); writer.write(*p++); } - } else if (pVector->body.child_type == uris.atom_Int) + } + else if (pVector->body.child_type == uris.atom_Int) { - int32_t *p = (int32_t*)pItems; + int32_t *p = (int32_t *)pItems; for (size_t i = 0; i < n; ++i) { - if (i != 0) writer.write_raw(","); + if (i != 0) + writer.write_raw(","); writer.write(*p++); } - } else if (pVector->body.child_type == uris.atom_Long) + } + else if (pVector->body.child_type == uris.atom_Long) { - int64_t *p = (int64_t*)pItems; + int64_t *p = (int64_t *)pItems; for (size_t i = 0; i < n; ++i) { - if (i != 0) writer.write_raw(","); + if (i != 0) + writer.write_raw(","); writer.write(*p++); } - } else if (pVector->body.child_type == uris.atom_Double) + } + else if (pVector->body.child_type == uris.atom_Double) { - double *p = (double*)pItems; + double *p = (double *)pItems; for (size_t i = 0; i < n; ++i) { - if (i != 0) writer.write_raw(","); + if (i != 0) + writer.write_raw(","); writer.write(*p++); } - } else if (pVector->body.child_type == uris.atom_Bool) + } + else if (pVector->body.child_type == uris.atom_Bool) { - bool *p = (bool*)pItems; + bool *p = (bool *)pItems; for (size_t i = 0; i < n; ++i) { - if (i != 0) writer.write_raw(","); + if (i != 0) + writer.write_raw(","); writer.write(*p++); } } } writer.end_array(); - } else if (pAtom->type == uris.atom_Object) + } + else if (pAtom->type == uris.atom_Object) { writer.start_object(); @@ -1477,14 +1666,14 @@ void JackHostImpl::WriteAtom(json_writer &writer, LV2_Atom*pAtom) if (obj->body.id != 0) { std::string id = pHost->Lv2UriudToString(obj->body.id); - writer.write_member("id",id.c_str()); + writer.write_member("id", id.c_str()); firstMember = false; } if (obj->body.otype != 0) { - std::string type = pHost->Lv2UriudToString(obj->body.otype); - writer.write_member("lv2Type",type.c_str()); + std::string type = pHost->Lv2UriudToString(obj->body.otype); + writer.write_member("lv2Type", type.c_str()); if (!firstMember) { writer.write_raw(","); @@ -1492,8 +1681,10 @@ void JackHostImpl::WriteAtom(json_writer &writer, LV2_Atom*pAtom) firstMember = false; } - LV2_ATOM_OBJECT_FOREACH (obj, prop) { - if (!firstMember) { + LV2_ATOM_OBJECT_FOREACH(obj, prop) + { + if (!firstMember) + { writer.write_raw(","); } firstMember = false; @@ -1502,17 +1693,30 @@ void JackHostImpl::WriteAtom(json_writer &writer, LV2_Atom*pAtom) writer.write(key); writer.write_raw(": "); LV2_Atom *value = &(prop->value); - WriteAtom(writer,value); + WriteAtom(writer, value); } writer.end_object(); - } - } -JackHost *JackHost::CreateInstance(IHost *pHost) +void AudioHostImpl::SetSystemMidiBindings(const std::vector&bindings) { - return new JackHostImpl(pHost); + for (auto i = bindings.begin(); i != bindings.end(); ++i) + { + if (i->symbol() == "nextProgram") + { + this->nextMidiBinding.SetBinding(*i); + } else if (i->symbol() == "prevProgram") + { + this->prevMidiBinding.SetBinding(*i); + } + } +} + + +AudioHost *AudioHost::CreateInstance(IHost *pHost) +{ + return new AudioHostImpl(pHost); } JSON_MAP_BEGIN(JackHostStatus) diff --git a/src/JackHost.hpp b/src/AudioHost.hpp similarity index 87% rename from src/JackHost.hpp rename to src/AudioHost.hpp index 689db04..616e1cd 100644 --- a/src/JackHost.hpp +++ b/src/AudioHost.hpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2022-2023 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 @@ -24,7 +24,7 @@ #include "Lv2PedalBoard.hpp" #include "VuUpdate.hpp" #include "json.hpp" -#include "JackHost.hpp" +#include "AudioHost.hpp" #include "JackServerSettings.hpp" #include #include "PiPedalAlsa.hpp" @@ -32,6 +32,8 @@ namespace pipedal { +struct RealtimeMidiProgramRequest; +struct RealtimeNextMidiProgramRequest; using PortMonitorCallback = std::function; @@ -91,7 +93,7 @@ public: }; -class IJackHostCallbacks { +class IAudioHostCallbacks { public: virtual void OnNotifyVusSubscription(const std::vector & updates) = 0; virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update) = 0; @@ -99,6 +101,8 @@ public: virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) = 0; virtual void OnNotifyAtomOutput(uint64_t instanceId, const std::string&atomType,const std::string&atomJson) = 0; + virtual void OnNotifyMidiProgramChange(RealtimeMidiProgramRequest&midiProgramRequest) = 0; + virtual void OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest&request) = 0; }; @@ -124,17 +128,17 @@ public: class IHost; -class JackHost { +class AudioHost { protected: - JackHost() { } + AudioHost() { } public: - static JackHost*CreateInstance(IHost *pHost); - virtual ~JackHost() { }; + static AudioHost*CreateInstance(IHost *pHost); + virtual ~AudioHost() { }; virtual void UpdateServerConfiguration(const JackServerSettings & jackServerSettings, std::function onComplete) = 0; - virtual void SetNotificationCallbacks(IJackHostCallbacks *pNotifyCallbacks) = 0; + virtual void SetNotificationCallbacks(IAudioHostCallbacks *pNotifyCallbacks) = 0; virtual void SetListenForMidiEvent(bool listen) = 0; virtual void SetListenForAtomOutput(bool listen) = 0; @@ -158,7 +162,10 @@ public: virtual void SetVuSubscriptions(const std::vector &instanceIds) = 0; virtual void SetMonitorPortSubscriptions(const std::vector &subscriptions) = 0; + virtual void SetSystemMidiBindings(const std::vector&bindings) = 0; + virtual void getRealtimeParameter(RealtimeParameterRequest*pParameterRequest) = 0; + virtual void AckMidiProgramRequest(uint64_t requestId) = 0; virtual JackHostStatus getJackStatus() = 0; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 46461d3..f91a996 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -149,7 +149,7 @@ set (PIPEDAL_SOURCES CpuGovernor.cpp CpuGovernor.hpp GovernorSettings.cpp GovernorSettings.hpp SysExec.cpp SysExec.hpp - BeastServer.cpp BeastServer.hpp HtmlHelper.cpp HtmlHelper.hpp pch.h Uri.cpp Uri.hpp + WebServer.cpp WebServer.hpp HtmlHelper.cpp HtmlHelper.hpp pch.h Uri.cpp Uri.hpp WifiConfigSettings.hpp WifiConfigSettings.cpp WifiDirectConfigSettings.hpp WifiDirectConfigSettings.cpp ConfigUtil.hpp ConfigUtil.cpp @@ -164,7 +164,7 @@ set (PIPEDAL_SOURCES Presets.hpp Presets.cpp Storage.hpp Storage.cpp Banks.hpp Banks.cpp - JackHost.hpp JackHost.cpp + AudioHost.hpp AudioHost.cpp JackConfiguration.hpp JackConfiguration.cpp defer.hpp Lv2Effect.cpp Lv2Effect.hpp @@ -264,7 +264,7 @@ add_executable(pipedaltest testMain.cpp SystemConfigFile.hpp SystemConfigFile.cpp SystemConfigFileTest.cpp - BeastServerTest.cpp + WebServerTest.cpp MemDebug.cpp MemDebug.hpp ) diff --git a/src/Lv2Effect.cpp b/src/Lv2Effect.cpp index eed9efb..f172e16 100644 --- a/src/Lv2Effect.cpp +++ b/src/Lv2Effect.cpp @@ -37,7 +37,7 @@ #include "lv2/parameters.lv2/parameters.h" #include "lv2/units.lv2/units.h" #include "lv2/atom.lv2/util.h" -#include "JackHost.hpp" +#include "AudioHost.hpp" #include #include "RingBufferReader.hpp" diff --git a/src/Lv2PedalBoard.cpp b/src/Lv2PedalBoard.cpp index 038ae00..eda156a 100644 --- a/src/Lv2PedalBoard.cpp +++ b/src/Lv2PedalBoard.cpp @@ -25,7 +25,7 @@ #include "SplitEffect.hpp" #include "RingBufferReader.hpp" #include "VuUpdate.hpp" -#include "JackHost.hpp" +#include "AudioHost.hpp" #include "Lv2EventBufferWriter.hpp" using namespace pipedal; diff --git a/src/MidiBinding.hpp b/src/MidiBinding.hpp index 6dac703..55f38f3 100644 --- a/src/MidiBinding.hpp +++ b/src/MidiBinding.hpp @@ -58,15 +58,21 @@ public: private: std::string symbol_; int channel_ = -1; - int bindingType_; + int bindingType_ = BINDING_TYPE_NONE; int note_ = 12*4+24; int control_ = 1; float minValue_ = 0; float maxValue_ = 1; float rotaryScale_ = 1; - int linearControlType_ = 0; - int switchControlType_ = 0; + int linearControlType_ = LINEAR_CONTROL_TYPE; + int switchControlType_ = LATCH_CONTROL_TYPE; public: + static MidiBinding SystemBinding(const std::string&symbol) + { + MidiBinding result; + result.symbol_ = symbol; + return result; + } GETTER_SETTER(channel); GETTER_SETTER_REF(symbol); GETTER_SETTER(bindingType); diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 71d9fba..075838e 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2022-2023 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 @@ -23,13 +23,14 @@ #include "ConfigUtil.hpp" #include #include "PiPedalModel.hpp" -#include "JackHost.hpp" +#include "AudioHost.hpp" #include "Lv2Log.hpp" #include #include "PiPedalConfiguration.hpp" #include "AdminClient.hpp" #include "SplitEffect.hpp" #include "CpuGovernor.hpp" +#include "RingBufferReader.hpp" #ifndef NO_MLOCK #include @@ -84,9 +85,9 @@ void PiPedalModel::Close() } delete[] t; - if (jackHost) + if (audioHost) { - jackHost->Close(); + audioHost->Close(); } } @@ -113,9 +114,9 @@ PiPedalModel::~PiPedalModel() try { - if (jackHost) + if (audioHost) { - jackHost->Close(); + audioHost->Close(); } } catch (...) @@ -133,6 +134,8 @@ void PiPedalModel::Init(const PiPedalConfiguration &configuration) storage.SetDataRoot(configuration.GetLocalStoragePath()); storage.Initialize(); + this->systemMidiBindings = storage.GetSystemMidiBindings(); + this->jackServerSettings.ReadJackConfiguration(); } @@ -191,10 +194,14 @@ void PiPedalModel::Load() } UpdateDefaults(&this->pedalBoard); - std::unique_ptr p{JackHost::CreateInstance(lv2Host.asIHost())}; - this->jackHost = std::move(p); + std::unique_ptr p{AudioHost::CreateInstance(lv2Host.asIHost())}; + this->audioHost = std::move(p); - this->jackHost->SetNotificationCallbacks(this); + this->audioHost->SetNotificationCallbacks(this); + + this->systemMidiBindings = storage.GetSystemMidiBindings(); + + this->audioHost->SetSystemMidiBindings(this->systemMidiBindings); if (configuration.GetMLock()) { @@ -226,11 +233,11 @@ void PiPedalModel::Load() this->lv2Host.OnConfigurationChanged(jackConfiguration, selection); try { - jackHost->Open(this->jackServerSettings, selection); + audioHost->Open(this->jackServerSettings, selection); try { std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; this->lv2PedalBoard = lv2PedalBoard; - jackHost->SetPedalBoard(lv2PedalBoard); + audioHost->SetPedalBoard(lv2PedalBoard); } catch (const std::exception &e) { @@ -305,7 +312,7 @@ void PiPedalModel::PreviewControl(int64_t clientId, int64_t pedalItemId, const s effect->SetControl(index,value);; } } else { - jackHost->SetControlValue(pedalItemId, symbol, value); + audioHost->SetControlValue(pedalItemId, symbol, value); } } @@ -387,11 +394,11 @@ void PiPedalModel::FirePedalBoardChanged(int64_t clientId) delete[] t; // notify the audio thread. - if (jackHost->IsOpen()) + if (audioHost->IsOpen()) { std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; this->lv2PedalBoard = lv2PedalBoard; - jackHost->SetPedalBoard(lv2PedalBoard); + audioHost->SetPedalBoard(lv2PedalBoard); UpdateRealtimeVuSubscriptions(); UpdateRealtimeMonitorPortSubscriptions(); } @@ -446,7 +453,7 @@ void PiPedalModel::SetPedalBoardItemEnable(int64_t clientId, int64_t pedalItemId this->SetPresetChanged(clientId, true); // Notify audo thread. - this->jackHost->SetBypass(pedalItemId, enabled); + this->audioHost->SetBypass(pedalItemId, enabled); } } @@ -621,6 +628,87 @@ int64_t PiPedalModel::UploadBank(BankFile &bankFile, int64_t uploadAfter) return newPreset; } +void PiPedalModel::OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest&request) +{ + std::lock_guard guard{mutex}; + try { + PresetIndex index; + storage.GetPresetIndex(&index); + auto currentPresetId = storage.GetCurrentPresetId(); + size_t currentPresetIndex = 0; + + for (size_t i = 0; i < index.presets().size(); ++i) + { + if (index.presets()[i].instanceId() == currentPresetId) + { + currentPresetIndex = i; + break; + } + } + if (index.presets().size() == 0) + { + throw PiPedalException("No presets loaded."); + } + if (request.direction < 0) + { + if (currentPresetIndex == 0) + { + currentPresetIndex = index.presets().size()-1; + } else { + --currentPresetIndex; + } + } else { + ++currentPresetIndex; + if (currentPresetIndex >= index.presets().size()) + { + currentPresetIndex = 0; + } + } + LoadPreset(-1,index.presets()[currentPresetIndex].instanceId()); + } catch (std::exception&e) + { + + Lv2Log::error(e.what()); + } + if (this->audioHost) + { + this->audioHost->AckMidiProgramRequest(request.requestId); + } +} + + +void PiPedalModel::OnNotifyMidiProgramChange(RealtimeMidiProgramRequest&midiProgramRequest) +{ + std::lock_guard guard{mutex}; + try { + if (midiProgramRequest.bank >= 0) + { + int64_t bankId = storage.GetBankByMidiBankNumber(midiProgramRequest.bank); + if (bankId == -1) throw PiPedalException("Bank not found."); + if (bankId != this->storage.GetBanks().selectedBank()) + { + storage.LoadBank(bankId); + + FireBanksChanged(-1); + FirePresetsChanged(-1); + } + } + int64_t presetId = storage.GetPresetByProgramNumber(midiProgramRequest.program); + if (presetId == -1) throw PiPedalException("No valid preset."); + LoadPreset(-1,presetId); + } catch (std::exception&e) + { + + Lv2Log::error(e.what()); + } + if (this->audioHost) + { + this->audioHost->AckMidiProgramRequest(midiProgramRequest.requestId); + } +} + + + void PiPedalModel::LoadPreset(int64_t clientId, int64_t instanceId) { std::lock_guard guard{mutex}; @@ -883,10 +971,10 @@ JackConfiguration PiPedalModel::GetJackConfiguration() void PiPedalModel::RestartAudio() { - if (this->jackHost->IsOpen()) + if (this->audioHost->IsOpen()) { - this->jackHost->Close(); + this->audioHost->Close(); } // restarting is a bit dodgy. It was impossible with Jack, but // now very plausible with the ALSA audio stack. @@ -899,7 +987,7 @@ void PiPedalModel::RestartAudio() // do a complete reload. - this->jackHost->SetPedalBoard(nullptr); + this->audioHost->SetPedalBoard(nullptr); this->jackConfiguration.AlsaInitialize(this->jackServerSettings); @@ -928,13 +1016,13 @@ void PiPedalModel::RestartAudio() Lv2Log::error("Audio configuration not valid."); return; } - this->jackHost->Open(this->jackServerSettings, channelSelection); + this->audioHost->Open(this->jackServerSettings, channelSelection); this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection); std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; this->lv2PedalBoard = lv2PedalBoard; - jackHost->SetPedalBoard(lv2PedalBoard); + audioHost->SetPedalBoard(lv2PedalBoard); this->UpdateRealtimeVuSubscriptions(); UpdateRealtimeMonitorPortSubscriptions(); } @@ -1121,12 +1209,12 @@ void PiPedalModel::UpdateRealtimeVuSubscriptions() } } std::vector instanceids(addedInstances.begin(), addedInstances.end()); - jackHost->SetVuSubscriptions(instanceids); + audioHost->SetVuSubscriptions(instanceids); } void PiPedalModel::UpdateRealtimeMonitorPortSubscriptions() { - jackHost->SetMonitorPortSubscriptions(this->activeMonitorPortSubscriptions); + audioHost->SetMonitorPortSubscriptions(this->activeMonitorPortSubscriptions); } int64_t PiPedalModel::MonitorPort(int64_t instanceId, const std::string &key, float updateInterval, PortMonitorCallback onUpdate) @@ -1220,7 +1308,7 @@ void PiPedalModel::GetLv2Parameter( std::lock_guard guard(mutex); outstandingParameterRequests.push_back(request); - this->jackHost->getRealtimeParameter(request); + this->audioHost->getRealtimeParameter(request); } BankIndex PiPedalModel::GetBankIndex() const @@ -1254,6 +1342,7 @@ void PiPedalModel::OpenBank(int64_t clientId, int64_t bankId) FirePresetsChanged(clientId); this->pedalBoard = storage.GetCurrentPreset(); + UpdateDefaults(&this->pedalBoard); this->hasPresetChanged = false; this->FirePedalBoardChanged(clientId); @@ -1311,7 +1400,7 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet this->jackConfiguration.SetIsRestarting(true); FireJackConfigurationChanged(this->jackConfiguration); - this->jackHost->UpdateServerConfiguration( + this->audioHost->UpdateServerConfiguration( jackServerSettings, [this](bool success, const std::string &errorMessage) { @@ -1341,7 +1430,7 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; this->lv2PedalBoard = lv2PedalBoard; - jackHost->SetPedalBoard(lv2PedalBoard); + audioHost->SetPedalBoard(lv2PedalBoard); UpdateRealtimeVuSubscriptions(); UpdateRealtimeMonitorPortSubscriptions(); #endif @@ -1418,7 +1507,7 @@ void PiPedalModel::LoadPluginPreset(int64_t pluginInstanceId, uint64_t presetIns if (pedalBoardItem != nullptr) { std::vector controlValues = storage.GetPluginPresetValues(pedalBoardItem->uri(), presetInstanceId); - jackHost->SetPluginPreset(pluginInstanceId, controlValues); + audioHost->SetPluginPreset(pluginInstanceId, controlValues); for (size_t i = 0; i < controlValues.size(); ++i) { @@ -1451,7 +1540,7 @@ void PiPedalModel::DeleteAtomOutputListeners(int64_t clientId) --i; } } - jackHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); + audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); } void PiPedalModel::DeleteMidiListeners(int64_t clientId) @@ -1465,7 +1554,7 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId) --i; } } - jackHost->SetListenForMidiEvent(midiEventListeners.size() != 0); + audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); } void PiPedalModel::OnNotifyAtomOutput(uint64_t instanceId, const std::string &atomType, const std::string &atomJson) @@ -1489,7 +1578,7 @@ void PiPedalModel::OnNotifyAtomOutput(uint64_t instanceId, const std::string &at } } } - jackHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); + audioHost->SetListenForAtomOutput(atomOutputListeners.size() != 0); } void PiPedalModel::OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) @@ -1513,7 +1602,7 @@ void PiPedalModel::OnNotifyMidiListen(bool isNote, uint8_t noteOrControl) } } } - jackHost->SetListenForMidiEvent(midiEventListeners.size() != 0); + audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0); } void PiPedalModel::ListenForMidiEvent(int64_t clientId, int64_t clientHandle, bool listenForControlsOnly) @@ -1521,7 +1610,7 @@ void PiPedalModel::ListenForMidiEvent(int64_t clientId, int64_t clientHandle, bo std::lock_guard guard(mutex); MidiListener listener{clientId, clientHandle, listenForControlsOnly}; midiEventListeners.push_back(listener); - jackHost->SetListenForMidiEvent(true); + audioHost->SetListenForMidiEvent(true); } void PiPedalModel::ListenForAtomOutputs(int64_t clientId, int64_t clientHandle, uint64_t instanceId) @@ -1529,7 +1618,7 @@ void PiPedalModel::ListenForAtomOutputs(int64_t clientId, int64_t clientHandle, std::lock_guard guard(mutex); AtomOutputListener listener{clientId, clientHandle, instanceId}; atomOutputListeners.push_back(listener); - jackHost->SetListenForAtomOutput(true); + audioHost->SetListenForAtomOutput(true); } void PiPedalModel::CancelListenForMidiEvent(int64_t clientId, int64_t clientHandle) @@ -1546,7 +1635,7 @@ void PiPedalModel::CancelListenForMidiEvent(int64_t clientId, int64_t clientHand } if (midiEventListeners.size() == 0) { - jackHost->SetListenForMidiEvent(false); + audioHost->SetListenForMidiEvent(false); } } void PiPedalModel::CancelListenForAtomOutputs(int64_t clientId, int64_t clientHandle) @@ -1563,7 +1652,7 @@ void PiPedalModel::CancelListenForAtomOutputs(int64_t clientId, int64_t clientHa } if (midiEventListeners.size() == 0) { - jackHost->SetListenForMidiEvent(false); + audioHost->SetListenForMidiEvent(false); } } std::vector PiPedalModel::GetAlsaDevices() @@ -1600,3 +1689,31 @@ void PiPedalModel::SetFavorites(const std::map &favorites) t[i]->OnFavoritesChanged(favorites); } } +std::vector PiPedalModel::GetSystemMidiBidings() { + std::lock_guard guard(mutex); + return this->systemMidiBindings; +} +void PiPedalModel::SetSystemMidiBindings(std::vector &bindings) +{ + std::lock_guard guard(mutex); + this->systemMidiBindings = bindings; + storage.SetSystemMidiBindings(bindings); + if (this->audioHost) + { + this->audioHost->SetSystemMidiBindings(bindings); + } + + IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; + for (size_t i = 0; i < subscribers.size(); ++i) + { + t[i] = this->subscribers[i]; + } + size_t n = this->subscribers.size(); + for (size_t i = 0; i < n; ++i) + { + t[i]->OnSystemMidiBindingsChanged(bindings); + } + delete[] t; + + +} \ No newline at end of file diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 3e1b3ae..787fed8 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -25,7 +25,7 @@ #include "Storage.hpp" #include "Banks.hpp" #include "JackConfiguration.hpp" -#include "JackHost.hpp" +#include "AudioHost.hpp" #include "VuUpdate.hpp" #include #include @@ -38,245 +38,249 @@ #include "AvahiService.hpp" #include +namespace pipedal +{ + struct RealtimeMidiProgramRequest; + struct RealtimeNextMidiProgramRequest; - - -namespace pipedal { - - - - -class IPiPedalModelSubscriber { -public: - virtual int64_t GetClientId() = 0; - virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0; - virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0; - virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string&state) = 0; - virtual void OnPedalBoardChanged(int64_t clientId, const PedalBoard &pedalBoard) = 0; - virtual void OnPresetsChanged(int64_t clientId, const PresetIndex&presets) = 0; - virtual void OnPluginPresetsChanged(const std::string&pluginUri) = 0; - virtual void OnChannelSelectionChanged(int64_t clientId, const JackChannelSelection&channelSelection) = 0; - virtual void OnVuMeterUpdate(const std::vector& updates) = 0; - virtual void OnBankIndexChanged(const BankIndex& bankIndex) = 0; - virtual void OnJackServerSettingsChanged(const JackServerSettings& jackServerSettings) = 0; - virtual void OnJackConfigurationChanged(const JackConfiguration& jackServerConfiguration) = 0; - virtual void OnLoadPluginPreset(int64_t instanceId,const std::vector&controlValues) = 0; - virtual void OnMidiValueChanged(int64_t instanceId, const std::string&symbol, float value) = 0; - virtual void OnNotifyMidiListener(int64_t clientHandle, bool isNote, uint8_t noteOrControl) = 0; - virtual void OnNotifyAtomOutput(int64_t clientModel,uint64_t instanceId,const std::string&atomType, const std::string&atomJson) = 0; - virtual void OnWifiConfigSettingsChanged(const WifiConfigSettings&wifiConfigSettings) = 0; - virtual void OnWifiDirectConfigSettingsChanged(const WifiDirectConfigSettings&wifiDirectConfigSettings) = 0; - virtual void OnGovernorSettingsChanged(const std::string &governor) = 0; - virtual void OnFavoritesChanged(const std::map &favorites) = 0; - virtual void OnShowStatusMonitorChanged(bool show) = 0; - virtual void Close() = 0; -}; - -class PiPedalModel: private IJackHostCallbacks { -private: - std::unique_ptr pingThread; - - AvahiService avahiService; - uint16_t webPort; - - PiPedalAlsaDevices alsaDevices; - std::recursive_mutex mutex; - - AdminClient adminClient; - - - class MidiListener { + class IPiPedalModelSubscriber + { public: - int64_t clientId; - int64_t clientHandle; - bool listenForControlsOnly; + virtual int64_t GetClientId() = 0; + virtual void OnItemEnabledChanged(int64_t clientId, int64_t pedalItemId, bool enabled) = 0; + virtual void OnControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value) = 0; + virtual void OnVst3ControlChanged(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value, const std::string &state) = 0; + virtual void OnPedalBoardChanged(int64_t clientId, const PedalBoard &pedalBoard) = 0; + virtual void OnPresetsChanged(int64_t clientId, const PresetIndex &presets) = 0; + virtual void OnPluginPresetsChanged(const std::string &pluginUri) = 0; + virtual void OnChannelSelectionChanged(int64_t clientId, const JackChannelSelection &channelSelection) = 0; + virtual void OnVuMeterUpdate(const std::vector &updates) = 0; + virtual void OnBankIndexChanged(const BankIndex &bankIndex) = 0; + virtual void OnJackServerSettingsChanged(const JackServerSettings &jackServerSettings) = 0; + virtual void OnJackConfigurationChanged(const JackConfiguration &jackServerConfiguration) = 0; + virtual void OnLoadPluginPreset(int64_t instanceId, const std::vector &controlValues) = 0; + virtual void OnMidiValueChanged(int64_t instanceId, const std::string &symbol, float value) = 0; + virtual void OnNotifyMidiListener(int64_t clientHandle, bool isNote, uint8_t noteOrControl) = 0; + virtual void OnNotifyAtomOutput(int64_t clientModel, uint64_t instanceId, const std::string &atomType, const std::string &atomJson) = 0; + virtual void OnWifiConfigSettingsChanged(const WifiConfigSettings &wifiConfigSettings) = 0; + virtual void OnWifiDirectConfigSettingsChanged(const WifiDirectConfigSettings &wifiDirectConfigSettings) = 0; + virtual void OnGovernorSettingsChanged(const std::string &governor) = 0; + virtual void OnFavoritesChanged(const std::map &favorites) = 0; + virtual void OnShowStatusMonitorChanged(bool show) = 0; + virtual void OnSystemMidiBindingsChanged(const std::vector&bindings) = 0; + virtual void Close() = 0; }; - class AtomOutputListener { + + class PiPedalModel : private IAudioHostCallbacks + { + private: + std::unique_ptr pingThread; + + std::vector systemMidiBindings; + + AvahiService avahiService; + uint16_t webPort; + + PiPedalAlsaDevices alsaDevices; + std::recursive_mutex mutex; + + AdminClient adminClient; + + class MidiListener + { + public: + int64_t clientId; + int64_t clientHandle; + bool listenForControlsOnly; + }; + class AtomOutputListener + { + public: + int64_t clientId; + int64_t clientHandle; + uint64_t instanceId; + }; + void DeleteMidiListeners(int64_t clientId); + void DeleteAtomOutputListeners(int64_t clientId); + + std::vector midiEventListeners; + std::vector atomOutputListeners; + + JackServerSettings jackServerSettings; + PiPedalHost lv2Host; + PedalBoard pedalBoard; + Storage storage; + bool hasPresetChanged = false; + + std::unique_ptr audioHost; + JackConfiguration jackConfiguration; + std::shared_ptr lv2PedalBoard; + std::filesystem::path webRoot; + + std::vector subscribers; + void SetPresetChanged(int64_t clientId, bool value); + void FirePresetsChanged(int64_t clientId); + void FirePluginPresetsChanged(const std::string &pluginUri); + void FirePedalBoardChanged(int64_t clientId); + void FireChannelSelectionChanged(int64_t clientId); + void FireBanksChanged(int64_t clientId); + void FireJackConfigurationChanged(const JackConfiguration &jackConfiguration); + + void UpdateDefaults(PedalBoardItem *pedalBoardItem); + void UpdateDefaults(PedalBoard *pedalBoard); + + class VuSubscription + { + public: + int64_t subscriptionHandle; + int64_t instanceid; + }; + int64_t nextSubscriptionId = 1; + std::vector activeVuSubscriptions; + + std::vector activeMonitorPortSubscriptions; + + void UpdateRealtimeVuSubscriptions(); + void UpdateRealtimeMonitorPortSubscriptions(); + void OnVuUpdate(const std::vector &updates); + + void RestartAudio(); + + std::vector outstandingParameterRequests; + + IPiPedalModelSubscriber *GetNotificationSubscriber(int64_t clientId); + + private: // IAudioHostCallbacks + virtual void OnNotifyVusSubscription(const std::vector &updates); + virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update); + virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value); + virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl); + virtual void OnNotifyAtomOutput(uint64_t instanceId, const std::string &atomType, const std::string &atomJson); + virtual void OnNotifyMidiProgramChange(RealtimeMidiProgramRequest &midiProgramRequest); + virtual void OnNotifyNextMidiProgram(const RealtimeNextMidiProgramRequest&request); + + void UpdateVst3Settings(PedalBoard &pedalBoard); + + PiPedalConfiguration configuration; + public: - int64_t clientId; - int64_t clientHandle; - uint64_t instanceId; + PiPedalModel(); + virtual ~PiPedalModel(); + + uint16_t GetWebPort() const { return webPort; } + void Close(); + + void UpdateDnsSd(); + + AdminClient &GetAdminClient() { return adminClient; } + + void Init(const PiPedalConfiguration &configuration); + + void LoadLv2PluginInfo(); + void Load(); + + const PiPedalHost &GetLv2Host() const { return lv2Host; } + PedalBoard GetCurrentPedalBoardCopy() + { + std::lock_guard guard(mutex); + return pedalBoard; // can return a referece because we'd lose mutex protection + } + PluginUiPresets GetPluginUiPresets(const std::string &pluginUri); + PluginPresets GetPluginPresets(const std::string &pluginUri); + + void LoadPluginPreset(int64_t pluginInstanceId, uint64_t presetInstanceId); + + void AddNotificationSubscription(IPiPedalModelSubscriber *pSubscriber); + void RemoveNotificationSubsription(IPiPedalModelSubscriber *pSubscriber); + + void SetPedalBoardItemEnable(int64_t clientId, int64_t instanceId, bool enabled); + void SetControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value); + void PreviewControl(int64_t clientId, int64_t pedalItemId, const std::string &symbol, float value); + void SetPedalBoard(int64_t clientId, PedalBoard &pedalBoard); + void UpdateCurrentPedalBoard(int64_t clientId, PedalBoard &pedalBoard); + + void GetPresets(PresetIndex *pResult); + + PedalBoard GetPreset(int64_t instanceId); + void GetBank(int64_t instanceId, BankFile *pBank); + + int64_t UploadBank(BankFile &bankFile, int64_t uploadAfter = -1); + int64_t UploadPreset(const BankFile &bankFile, int64_t uploadAfter = -1); + void UploadPluginPresets(const PluginPresets &pluginPresets); + void SaveCurrentPreset(int64_t clientId); + int64_t SaveCurrentPresetAs(int64_t clientId, const std::string &name, int64_t saveAfterInstanceId = -1); + int64_t SavePluginPresetAs(int64_t instanceId, const std::string &name); + + void LoadPreset(int64_t clientId, int64_t instanceId); + bool UpdatePresets(int64_t clientId, const PresetIndex &presets); + void UpdatePluginPresets(const PluginUiPresets &pluginPresets); + int64_t DeletePreset(int64_t clientId, int64_t instanceId); + bool RenamePreset(int64_t clientId, int64_t instanceId, const std::string &name); + int64_t CopyPreset(int64_t clientId, int64_t fromIndex, int64_t toIndex); + uint64_t CopyPluginPreset(const std::string &pluginUri, uint64_t presetId); + + void MoveBank(int64_t clientId, int from, int to); + int64_t DeleteBank(int64_t clientId, int64_t instanceId); + + int64_t DuplicatePreset(int64_t clientId, int64_t instanceId) { return CopyPreset(clientId, instanceId, -1); } + + JackConfiguration GetJackConfiguration(); + + void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection); + JackChannelSelection GetJackChannelSelection(); + + void SetShowStatusMonitor(bool show); + bool GetShowStatusMonitor(); + + void SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings); + WifiConfigSettings GetWifiConfigSettings(); + + void SetWifiDirectConfigSettings(const WifiDirectConfigSettings &wifiConfigSettings); + WifiDirectConfigSettings GetWifiDirectConfigSettings(); + + void SetGovernorSettings(const std::string &governor); + GovernorSettings GetGovernorSettings(); + + int64_t AddVuSubscription(int64_t instanceId); + void RemoveVuSubscription(int64_t subscriptionHandle); + + void SetSystemMidiBindings(std::vector &bindings); + std::vector GetSystemMidiBidings(); + + int64_t MonitorPort(int64_t instanceId, const std::string &key, float updateInterval, PortMonitorCallback onUpdate); + void UnmonitorPort(int64_t subscriptionHandle); + + void GetLv2Parameter( + int64_t clientId, + int64_t instanceId, + const std::string uri, + std::function onSuccess, + std::function onError); + + BankIndex GetBankIndex() const; + void RenameBank(int64_t clientId, int64_t bankId, const std::string &newName); + int64_t SaveBankAs(int64_t clientId, int64_t bankId, const std::string &newName); + void OpenBank(int64_t clientId, int64_t bankId); + + JackHostStatus GetJackStatus() + { + return this->audioHost->getJackStatus(); + } + JackServerSettings GetJackServerSettings(); + void SetJackServerSettings(const JackServerSettings &jackServerSettings); + + void ListenForMidiEvent(int64_t clientId, int64_t clientHandle, bool listenForControlsOnly); + void CancelListenForMidiEvent(int64_t clientId, int64_t clientHandle); + + void ListenForAtomOutputs(int64_t clientId, int64_t clientHandle, uint64_t instanceId); + void CancelListenForAtomOutputs(int64_t clientId, int64_t clientHandle); + + std::vector GetAlsaDevices(); + const std::filesystem::path &GetWebRoot() const; + + std::map GetFavorites() const; + void SetFavorites(const std::map &favorites); }; - void DeleteMidiListeners(int64_t clientId); - void DeleteAtomOutputListeners(int64_t clientId); - - std::vector midiEventListeners; - std::vector atomOutputListeners; - - JackServerSettings jackServerSettings; - PiPedalHost lv2Host; - PedalBoard pedalBoard; - Storage storage; - bool hasPresetChanged = false; - - std::unique_ptr jackHost; - JackConfiguration jackConfiguration; - std::shared_ptr lv2PedalBoard; - std::filesystem::path webRoot; - - - std::vector subscribers; - void SetPresetChanged(int64_t clientId,bool value); - void FirePresetsChanged(int64_t clientId); - void FirePluginPresetsChanged(const std::string &pluginUri); - void FirePedalBoardChanged(int64_t clientId); - void FireChannelSelectionChanged(int64_t clientId); - void FireBanksChanged(int64_t clientId); - void FireJackConfigurationChanged(const JackConfiguration&jackConfiguration); - - void UpdateDefaults(PedalBoardItem*pedalBoardItem); - void UpdateDefaults(PedalBoard*pedalBoard); - - - class VuSubscription { - public: - int64_t subscriptionHandle; - int64_t instanceid; - }; - int64_t nextSubscriptionId = 1; - std::vector activeVuSubscriptions; - - std::vector activeMonitorPortSubscriptions; - - void UpdateRealtimeVuSubscriptions(); - void UpdateRealtimeMonitorPortSubscriptions(); - void OnVuUpdate(const std::vector& updates); - - void RestartAudio(); - - std::vector outstandingParameterRequests; - - IPiPedalModelSubscriber*GetNotificationSubscriber(int64_t clientId); - -private: // IJackHostCallbacks - virtual void OnNotifyVusSubscription(const std::vector & updates); - virtual void OnNotifyMonitorPort(const MonitorPortUpdate &update); - virtual void OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, float value); - virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl); - virtual void OnNotifyAtomOutput(uint64_t instanceId, const std::string&atomType,const std::string&atomJson); - - void UpdateVst3Settings(PedalBoard&pedalBoard); - - PiPedalConfiguration configuration; -public: - PiPedalModel(); - virtual ~PiPedalModel(); - - uint16_t GetWebPort() const { return webPort; } - void Close(); - - void UpdateDnsSd(); - - AdminClient&GetAdminClient() { return adminClient; } - - void Init(const PiPedalConfiguration&configuration); - - void LoadLv2PluginInfo(); - void Load(); - - const PiPedalHost& GetLv2Host() const { return lv2Host; } - PedalBoard GetCurrentPedalBoardCopy() - { - std::lock_guard guard(mutex); - return pedalBoard; // can return a referece because we'd lose mutex protection - } - PluginUiPresets GetPluginUiPresets(const std::string& pluginUri); - PluginPresets GetPluginPresets(const std::string&pluginUri); - - void LoadPluginPreset(int64_t pluginInstanceId,uint64_t presetInstanceId); - - void AddNotificationSubscription(IPiPedalModelSubscriber* pSubscriber); - void RemoveNotificationSubsription(IPiPedalModelSubscriber* pSubscriber); - - void SetPedalBoardItemEnable(int64_t clientId, int64_t instanceId, bool enabled); - void SetControl(int64_t clientId,int64_t pedalItemId, const std::string&symbol,float value); - void PreviewControl(int64_t clientId,int64_t pedalItemId, const std::string&symbol,float value); - void SetPedalBoard(int64_t clientId,PedalBoard &pedalBoard); - void UpdateCurrentPedalBoard(int64_t clientId, PedalBoard &pedalBoard); - - - void GetPresets(PresetIndex*pResult); - PedalBoard GetPreset(int64_t instanceId); - void GetBank(int64_t instanceId, BankFile*pBank); - - int64_t UploadBank(BankFile&bankFile, int64_t uploadAfter = -1); - int64_t UploadPreset(const BankFile&bankFile, int64_t uploadAfter = -1); - void UploadPluginPresets(const PluginPresets&pluginPresets); - void SaveCurrentPreset(int64_t clientId); - int64_t SaveCurrentPresetAs(int64_t clientId, const std::string& name,int64_t saveAfterInstanceId = -1); - int64_t SavePluginPresetAs(int64_t instanceId, const std::string&name); - - void LoadPreset(int64_t clientId, int64_t instanceId); - bool UpdatePresets(int64_t clientId,const PresetIndex&presets); - void UpdatePluginPresets(const PluginUiPresets&pluginPresets); - int64_t DeletePreset(int64_t clientId,int64_t instanceId); - bool RenamePreset(int64_t clientId,int64_t instanceId,const std::string&name); - int64_t CopyPreset(int64_t clientId, int64_t fromIndex, int64_t toIndex); - uint64_t CopyPluginPreset(const std::string&pluginUri,uint64_t presetId); - - void MoveBank(int64_t clientId,int from, int to); - int64_t DeleteBank(int64_t clientId, int64_t instanceId); - - - int64_t DuplicatePreset(int64_t clientId, int64_t instanceId) { return CopyPreset(clientId,instanceId,-1); } - - JackConfiguration GetJackConfiguration(); - - void SetJackChannelSelection(int64_t clientId,const JackChannelSelection &channelSelection); - JackChannelSelection GetJackChannelSelection(); - - void SetShowStatusMonitor(bool show); - bool GetShowStatusMonitor(); - - void SetWifiConfigSettings(const WifiConfigSettings&wifiConfigSettings); - WifiConfigSettings GetWifiConfigSettings(); - - void SetWifiDirectConfigSettings(const WifiDirectConfigSettings&wifiConfigSettings); - WifiDirectConfigSettings GetWifiDirectConfigSettings(); - - void SetGovernorSettings(const std::string& governor); - GovernorSettings GetGovernorSettings(); - - - - int64_t AddVuSubscription(int64_t instanceId); - void RemoveVuSubscription(int64_t subscriptionHandle); - - - int64_t MonitorPort(int64_t instanceId,const std::string &key,float updateInterval, PortMonitorCallback onUpdate); - void UnmonitorPort(int64_t subscriptionHandle); - - void GetLv2Parameter( - int64_t clientId, - int64_t instanceId, - const std::string uri, - std::function onSuccess, - std::function onError - ); - - BankIndex GetBankIndex() const; - void RenameBank(int64_t clientId,int64_t bankId, const std::string& newName); - int64_t SaveBankAs(int64_t clientId,int64_t bankId, const std::string& newName); - void OpenBank(int64_t clientId,int64_t bankId); - - JackHostStatus GetJackStatus() { - return this->jackHost->getJackStatus(); - } - JackServerSettings GetJackServerSettings(); - void SetJackServerSettings(const JackServerSettings& jackServerSettings); - - void ListenForMidiEvent(int64_t clientId, int64_t clientHandle, bool listenForControlsOnly); - void CancelListenForMidiEvent(int64_t clientId, int64_t clientHandle); - - void ListenForAtomOutputs(int64_t clientId, int64_t clientHandle, uint64_t instanceId); - void CancelListenForAtomOutputs(int64_t clientId, int64_t clientHandle); - - std::vector GetAlsaDevices(); - const std::filesystem::path& GetWebRoot() const; - - std::map GetFavorites() const; - void SetFavorites(const std::map &favorites); -}; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index d058eea..5f06e58 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2022-2023 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 @@ -1235,6 +1235,17 @@ public: pReader->read(&favorites); this->model.SetFavorites(favorites); } + else if (message == "setSystemMidiBindings") + { + std::vector bindings; + pReader->read(&bindings); + this->model.SetSystemMidiBindings(bindings); + } + else if (message == "getSystemMidiBindings") + { + std::vector bindings = this->model.GetSystemMidiBidings(); + this->Reply(replyTo,"getSystemMidiBindings",bindings); + } else { Lv2Log::error("Unknown message received: %s", message.c_str()); @@ -1308,6 +1319,10 @@ protected: } public: + virtual void OnSystemMidiBindingsChanged(const std::vector&bindings) { + Send("onSystemMidiBindingsChanged",bindings); + } + virtual void OnFavoritesChanged(const std::map &favorites) { Send("onFavoritesChanged", favorites); diff --git a/src/PiPedalSocket.hpp b/src/PiPedalSocket.hpp index 8a59b07..3a14026 100644 --- a/src/PiPedalSocket.hpp +++ b/src/PiPedalSocket.hpp @@ -19,7 +19,7 @@ #pragma once -#include "BeastServer.hpp" +#include "WebServer.hpp" #include "PiPedalModel.hpp" diff --git a/src/RingBufferReader.hpp b/src/RingBufferReader.hpp index 570dfc0..a01f495 100644 --- a/src/RingBufferReader.hpp +++ b/src/RingBufferReader.hpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2022-2023 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 @@ -22,7 +22,7 @@ #include "PiPedalException.hpp" #include "Lv2Log.hpp" #include "VuUpdate.hpp" -#include "JackHost.hpp" +#include "AudioHost.hpp" namespace pipedal { @@ -51,8 +51,25 @@ namespace pipedal AtomOutput = 17, + MidiProgramChange = 18, // program change requested via midi. + AckMidiProgramChange = 19, + + NextMidiProgram = 20, + }; + struct RealtimeNextMidiProgramRequest { + int64_t requestId; + int32_t direction; + }; + + struct RealtimeMidiProgramRequest { + int64_t requestId; + int8_t bank; + uint8_t program; + }; + + class RealtimeMonitorPortSubscription { public: @@ -304,6 +321,25 @@ namespace pipedal write(RingBufferCommand::OnMidiListen, msg); } + /** + * @brief Notify host of a midi program change request. + * + * @param bank MIDI bank number, or -1 for current bank. + * @param program MIDI program number. + */ + void OnMidiProgramChange(int64_t _requestId,int8_t _bank,uint8_t _program) + { + RealtimeMidiProgramRequest msg { requestId:_requestId, bank: _bank,program: _program}; + + + write(RingBufferCommand::MidiProgramChange,msg); + } + void OnNextMidiProgram(int64_t requestId,int32_t direction) + { + RealtimeNextMidiProgramRequest msg { requestId: requestId, direction:direction}; + write(RingBufferCommand::NextMidiProgram,msg); + } + void SetControlValue(int effectIndex, int controlIndex, float value) { SetControlValueBody body; @@ -351,6 +387,10 @@ namespace pipedal { write(RingBufferCommand::SetVuSubscriptions, configuration); } + void AckMidiProgramRequest(int64_t requestId) + { + write(RingBufferCommand::AckMidiProgramChange,requestId); + } void SetMonitorPortSubscriptions(RealtimeMonitorPortSubscriptions *subscriptions) { diff --git a/src/Storage.cpp b/src/Storage.cpp index 3eb9dd0..417c7d3 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022 Robin Davies +// Copyright (c) 2022-2023 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 @@ -511,6 +511,7 @@ void Storage::SetPresetIndex(const PresetIndex &presets) this->currentBank = std::move(bankFile); // deleting any stray presets while we're at it. this->SaveCurrentBank(); } + void Storage::GetPresetIndex(PresetIndex *pResult) { *pResult = PresetIndex(); @@ -523,6 +524,15 @@ void Storage::GetPresetIndex(PresetIndex *pResult) pResult->presets().push_back(entry); } } +int64_t Storage::GetPresetByProgramNumber(uint8_t program)const +{ + if (program >= currentBank.presets().size()) + { + if (currentBank.presets().size() == 0) return -1; + program = (uint8_t)currentBank.presets().size()-1; + } + return currentBank.presets()[program]->instanceId(); +} PedalBoard Storage::GetPreset(int64_t instanceId) const { @@ -736,6 +746,17 @@ void Storage::MoveBank(int from, int to) this->bankIndex.move(from, to); this->SaveBankIndex(); } + + +int64_t Storage::GetBankByMidiBankNumber(uint8_t bankNumber) { + auto &entries = this->bankIndex.entries(); + if (bankNumber >= entries.size()) + { + if (entries.size() == 0) return -1; + bankNumber = (uint8_t)(entries.size()-1); + } + return entries[bankNumber].instanceId(); +} int64_t Storage::DeleteBank(int64_t bankId) { auto &entries = this->bankIndex.entries(); @@ -1333,6 +1354,36 @@ void Storage::SetJackServerSettings(const pipedal::JackServerSettings &jackConfi #endif } +void Storage::SetSystemMidiBindings(const std::vector&bindings) +{ + std::filesystem::path fileName = this->dataRoot / "SystemMidiBindings.json"; + std::ofstream f; + f.open(fileName); + if (f.is_open()) + { + json_writer writer(f); + writer.write(bindings); + } +} +std::vector Storage::GetSystemMidiBindings() +{ + std::vector result; + + std::filesystem::path fileName = this->dataRoot / "SystemMidiBindings.json"; + std::ifstream f; + f.open(fileName); + if (f.is_open()) + { + json_reader reader(f); + reader.read(&result); + } else { + result.push_back(MidiBinding::SystemBinding("prevProgram")); + result.push_back(MidiBinding::SystemBinding("nextProgram")); + } + return result; +} + + JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) diff --git a/src/Storage.hpp b/src/Storage.hpp index af30a65..2e287e8 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -109,6 +109,7 @@ public: void LoadUserSettings(); void SaveUserSettings(); void LoadBank(int64_t instanceId); + int64_t GetBankByMidiBankNumber(uint8_t bankNumber); const PedalBoard& GetCurrentPreset(); void SaveCurrentPreset(const PedalBoard&pedalBoard); int64_t SaveCurrentPresetAs(const PedalBoard&pedalBoard, const std::string&namne,int64_t saveAfterInstanceId = -1); @@ -116,11 +117,12 @@ public: void GetPresetIndex(PresetIndex*pResult); void SetPresetIndex(const PresetIndex &presetIndex); PedalBoard GetPreset(int64_t instanceId) const; + int64_t GetPresetByProgramNumber(uint8_t program) const; void GetBankFile(int64_t instanceId,BankFile*pResult) const; int64_t UploadPreset(const BankFile&bankFile, int64_t uploadAfter); int64_t UploadBank(BankFile&bankFile, int64_t uploadAfter); - + bool LoadPreset(int64_t presetId); int64_t DeletePreset(int64_t presetId); bool RenamePreset(int64_t presetId, const std::string&name); @@ -170,6 +172,8 @@ public: void SetShowStatusMonitor(bool show); bool GetShowStatusMonitor() const; + void SetSystemMidiBindings(const std::vector&bindings); + std::vector GetSystemMidiBindings(); }; diff --git a/src/BeastServer.cpp b/src/WebServer.cpp similarity index 96% rename from src/BeastServer.cpp rename to src/WebServer.cpp index 34f3a4d..2e7dbc5 100644 --- a/src/BeastServer.cpp +++ b/src/WebServer.cpp @@ -18,7 +18,7 @@ #include #include "Ipv6Helpers.hpp" -#include "BeastServer.hpp" +#include "WebServer.hpp" #include "Uri.hpp" @@ -133,7 +133,7 @@ std::string GetFromAddress(const tcp::socket &socket) namespace pipedal { - class BeastServerImpl : public BeastServer + class WebServerImpl : public WebServer { private: int signalOnDone = -1; @@ -200,7 +200,7 @@ namespace pipedal class WebSocketSession : public std::enable_shared_from_this, public SocketHandler::IWriteCallback { - BeastServerImpl *pServer; + WebServerImpl *pServer; server::connection_ptr webSocket; std::string fromAddress; std::shared_ptr socketHandler; @@ -224,7 +224,7 @@ namespace pipedal Lv2Log::info("WebSocketSession closed."); } using ptr = std::shared_ptr; - WebSocketSession(BeastServerImpl *pServer, server::connection_ptr &webSocket) + WebSocketSession(WebServerImpl *pServer, server::connection_ptr &webSocket) : pServer(pServer), webSocket(webSocket) { @@ -599,9 +599,9 @@ namespace pipedal using websocketpp::lib::placeholders::_1; using websocketpp::lib::placeholders::_2; - m_endpoint.set_open_handler(bind(&BeastServerImpl::on_open, this, _1)); - m_endpoint.set_close_handler(bind(&BeastServerImpl::on_close, this, _1)); - m_endpoint.set_http_handler(bind(&BeastServerImpl::on_http, this, _1)); + m_endpoint.set_open_handler(bind(&WebServerImpl::on_open, this, _1)); + m_endpoint.set_close_handler(bind(&WebServerImpl::on_close, this, _1)); + m_endpoint.set_http_handler(bind(&WebServerImpl::on_http, this, _1)); std::string hostName = getHostName(); if (hostName.length() != 0) @@ -660,7 +660,7 @@ namespace pipedal } } - static void ThreadProc(BeastServerImpl *server) + static void ThreadProc(WebServerImpl *server) { server->Run(); } @@ -717,7 +717,7 @@ namespace pipedal this->pBgThread = new std::thread(ThreadProc, this); } - BeastServerImpl(const std::string &address, int port, const char *rootPath, int threads) + WebServerImpl(const std::string &address, int port, const char *rootPath, int threads) : address(address), rootPath(rootPath), port(port), @@ -727,7 +727,7 @@ namespace pipedal }; } // namespace pipedal -std::shared_ptr BeastServerImpl::GetSocketFactory(const uri &requestUri) +std::shared_ptr WebServerImpl::GetSocketFactory(const uri &requestUri) { for (auto factory : this->socket_factories) @@ -740,7 +740,7 @@ std::shared_ptr BeastServerImpl::GetSocketFactory(const uri &req return nullptr; } -std::shared_ptr pipedal::createBeastServer(const boost::asio::ip::address &address, int port, const char *rootPath, int threads) +std::shared_ptr pipedal::WebServer::create(const boost::asio::ip::address &address, int port, const char *rootPath, int threads) { - return std::shared_ptr(new BeastServerImpl(address.to_string(), port, rootPath, threads)); + return std::shared_ptr(new WebServerImpl(address.to_string(), port, rootPath, threads)); } diff --git a/src/BeastServer.hpp b/src/WebServer.hpp similarity index 95% rename from src/BeastServer.hpp rename to src/WebServer.hpp index 3f3602b..19a5087 100644 --- a/src/BeastServer.hpp +++ b/src/WebServer.hpp @@ -68,10 +68,10 @@ public: //xxx move this to HtmlHelpers. std::string last_modified(const std::filesystem::path& path); -class BeastServerImpl; +class WebServerImpl; class SocketHandler { - friend class BeastServerImpl; + friend class WebServerImpl; public: class IWriteCallback { @@ -213,9 +213,9 @@ public: }; -class BeastServer { +class WebServer { public: - virtual ~BeastServer() { } + virtual ~WebServer() { } virtual void SetLogHttpRequests(bool enableLogging) = 0; @@ -227,10 +227,17 @@ public: // signalOnDone: fire the specified POSIX signal when the service thread terminates. -1 for no signal. virtual void RunInBackground(int signalOnDone = -1) = 0; + + static std::shared_ptr create( + const boost::asio::ip::address &address, + int port, + const char *rootPath, + int threads); + + }; -std::shared_ptr createBeastServer(const boost::asio::ip::address &address, int port, const char *rootPath, int threads); diff --git a/src/BeastServerTest.cpp b/src/WebServerTest.cpp similarity index 93% rename from src/BeastServerTest.cpp rename to src/WebServerTest.cpp index f6660e1..eb0dd00 100644 --- a/src/BeastServerTest.cpp +++ b/src/WebServerTest.cpp @@ -19,13 +19,13 @@ #include "pch.h" #include "catch.hpp" -#include "BeastServer.hpp" +#include "WebServer.hpp" #include "MemDebug.hpp" #include using namespace pipedal; -TEST_CASE("BeastServer shutdown", "[beastServerShutdown][Build][Dev]") +TEST_CASE("WebServer shutdown", "[webServerShutdown][Build][Dev]") { MemStats initialMemory = GetMemStats(); { @@ -34,7 +34,7 @@ TEST_CASE("BeastServer shutdown", "[beastServerShutdown][Build][Dev]") std::string doc_root = "."; auto const threads = 3; - auto server = createBeastServer( + auto server = WebServer::create( address, port, doc_root.c_str(), threads); server->RunInBackground(); sleep(5); diff --git a/src/main.cpp b/src/main.cpp index 84b27f8..5162c8b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,7 +20,7 @@ #include "pch.h" #include "AudioConfig.hpp" -#include "BeastServer.hpp" +#include "WebServer.hpp" #include #include "Lv2Log.hpp" #include "ServiceConfiguration.hpp" @@ -601,7 +601,7 @@ int main(int argc, char *argv[]) } uint16_t port; - std::shared_ptr server; + std::shared_ptr server; try { auto const address = boost::asio::ip::make_address(configuration.GetSocketServerAddress()); @@ -609,7 +609,7 @@ int main(int argc, char *argv[]) auto const threads = std::max(1, configuration.GetThreads()); - server = createBeastServer( + server = WebServer::create( address, port, web_root.c_str(), threads); Lv2Log::info("Document root: %s Threads: %d", doc_root.c_str(), (int)threads); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 119c11d..b3e9e4a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -42,8 +42,8 @@ add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY) add_executable(piddletest main.cpp pch.h UriTest.cpp JsonTest.cpp - ../src/Uri.cpp ../src/Uri.hpp ../src/HtmlHelper.cpp ../src/HtmlHelper.hpp BeastServerTest.cpp - ../src/BeastServer.cpp ../src/BeastServer.hpp ../src/json.hpp ../src/json.cpp + ../src/Uri.cpp ../src/Uri.hpp ../src/HtmlHelper.cpp ../src/HtmlHelper.hpp WebServerTest.cpp + ../src/WebServer.cpp ../src/WebServer.hpp ../src/json.hpp ../src/json.cpp ../src/Lv2Host.cpp ../src/Lv2Host.hpp ../src/Lv2Log.hpp ../src/Lv2Log.cpp ../src/PluginType.hpp ../src/PluginType.cpp