Merge pull request #173 from rerdavies/dev

v1.2.40
This commit is contained in:
Robin Davies
2024-08-20 21:12:06 -04:00
committed by GitHub
35 changed files with 954 additions and 796 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
cmake_minimum_required(VERSION 3.16.0)
project(pipedal
VERSION 1.2.38
VERSION 1.2.40
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
)
set (DISPLAY_VERSION "v1.2.38beta5")
set (DISPLAY_VERSION "v1.2.40Beta6")
set (CMAKE_INSTALL_PREFIX "/usr/")
+6 -4
View File
@@ -6,16 +6,18 @@
<a href="https://rerdavies.github.io/pipedal/LicensePiPedal.html"><img src="https://img.shields.io/badge/MIT-MIT?label=license&color=%23808080"/></a>
<a href="https://github.com/rerdavies/pipedal/actions"><img src="https://img.shields.io/github/actions/workflow/status/rerdavies/pipedal/cmake.yml?branch=main"/></a>
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.2.38 Beta5</a>&nbsp;&nbsp;&nbsp;&nbsp; Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Download:&nbsp;<a href='https://rerdavies.github.io/pipedal/download.html'>v1.2.40 Beta6</a>
Website:&nbsp;[https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Documentation:&nbsp;[https://rerdavies.github.io/pipedal/Documentation.html](https://rerdavies.github.io/pipedal/Documentation.html).
&nbsp;
#### NEW version 1.2.38 Beta5 release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
#### NEW version 1.2.40 Beta6 release, providing support for Raspberry Pi OS Bookworm. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
&nbsp;
Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet.
PiPedal running on a Raspberry Pi 4 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats. 
PiPedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats. 
PiPedal's user interface has been specifically designed to work well on small form-factor touch devices like phones or tablets. Clip a phone or tablet on your microphone stand on stage, and you're ready to play! Or connect via a desktop browser, for a slightly more luxurious experience. The PiPedal user-interface adapts to the screen size and orientation of your device, providing easy control of your guitar effects across a broad variety devices and screen sizes.
@@ -43,7 +45,7 @@ If your USB audio adapter has MIDI connectors, you can use MIDI devices (keyboar
### [Using LV2 Audio Plugins](https://rerdavies.github.io/pipedal/UsingLv2Plugins.md)
### [Which LV2 Plugins does PiPedal support?](https://rerdavies.github.io/pipedal/WhichLv2PluginsAreSupported.html)
 
 &nbsp;
### [Building PiPedal from Source](https://rerdavies.github.io/pipedal/BuildingPiPedalFromSource.html)
### [Build Prerequisites](https://rerdavies.github.io/pipedal/BuildPrerequisites.html)
+3 -3
View File
@@ -13,17 +13,17 @@ page_icon: img/Install4.jpg
Download the most recent Debian (.deb) package for your platform:
- [Raspberry Pi OS bookworm (64-bit) v1.2.38 Beta5](https://github.com/rerdavies/pipedal/releases/download/)
- [Raspberry Pi OS bookworm (64-bit) v1.2.40 Beta6](https://github.com/rerdavies/pipedal/releases/download/)
- [Ubuntu/Raspberry Pi OS bullseyeye (64-bit) v1.2.31](https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb)
Version 1.2.38 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31.
Version 1.2.40 has not yet been tested on Ubuntu or Raspberry Pi OS bullseye. On these platforms, we recommend that you use version 1.1.31.
Install the package by running
```
sudo apt update
cd ~/Downloads
sudo apt-get install pipedal_1.2.38_arm64.deb
sudo apt-get install pipedal_1.2.40_arm64.deb
```
Adjust accordingly if you have downloaded v1.1.31.
+5 -1
View File
@@ -1,5 +1,9 @@
# Release Notes
## Pipedal 1.2.38 Beta5
## Pipedal 1.2.40 Beta6
- PiPedal now monitors lv2 directories in order to automatically add newly-installed LV2 plugins.
- Toggle and on/off switch colors changed from red/pink to purple.
## Pipedal 1.2.38 Beta6
Fixes:
- PiPedal won't run on non-en-US locales.
+2 -2
View File
@@ -201,8 +201,8 @@
+ "px; left: 0px; top: 0px; opacity: 0.0;"
+ " display: flex; flex-flow: row none; justify-content: space-between;align-items: end; padding:20; opacity: 0.8;'>");
{
document.writeln("<div><span style='cursor: default;font-size: 52px;color: #444;'></span></div>");
document.writeln("<div><span style='cursor: default;font-size: 52px;color: #444;'></span></div>");
document.writeln("<div><span style='cursor: default;font-size: 52px;color: #444;'><img src="img/arrow_back.svg"/></span></div>");
document.writeln("<div><span style='cursor: default;font-size: 52px;color: #444;'><img src="img/arrow_forward.svg"/></span></div>");
}
document.writeln("</div>");
+3 -3
View File
@@ -4,17 +4,17 @@
Download the most recent Debian (.deb) package for your platform:
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.2.38/pipedal_1.2.38_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.2.38 Beta5</a>
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.2.40/pipedal_1.2.40_arm64.deb">Raspberry Pi OS Bookworm (64-bit) v1.2.40 Beta6</a>
- <a href="https://github.com/rerdavies/pipedal/releases/download/v1.1.31/pipedal_1.1.31_arm64.deb">Ubuntu 21.04 or Raspberry Pi OS bullseyeyeye (64-bit) v1.1.31</a>
v1.2.38 Beta5 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS.
v1.2.40 Beta6 is does not currently support Ubuntu 21.04, or older versions of Raspberry Pi OS.
Install the package by running
```
sudo apt update
cd ~/Downloads
sudo apt-get install ./pipedal_1.2.38_arm64.deb
sudo apt-get install ./pipedal_1.2.40_arm64.deb
```
Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M400-80 0-480l400-400 71 71-329 329 329 329-71 71Z"/></svg>

After

Width:  |  Height:  |  Size: 175 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m321-80-71-71 329-329-329-329 71-71 400 400L321-80Z"/></svg>

After

Width:  |  Height:  |  Size: 176 B

+6 -4
View File
@@ -1,17 +1,19 @@
<img src="GithubBanner.png" width="100%"/>
<a href="Installing.html"><i>v1.2.38</i></a>
<a href="Installing.html"><i>v1.2.40</i></a>
&nbsp;
To download PiPedal, click [here](download.md).
To download PiPedal, click [here](download.md).
To view PiPedal documentation, click [here](Documentation.md).
> NEW version 1.2.38 Beta5 release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
> NEW version 1.2.40 Beta6 release. See the [release notes](https://rerdavies.github.io/pipedal/ReleaseNotes) for details.
Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet.
PiPedal running on a Raspberry Pi 4 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats.
PiPedal running on a Raspberry Pi 4 or Pi 5 provides stable super-low-latency audio via external USB audio devices, or internal Raspberry Pi audio hats.
{% include gallery.html %}
+54 -137
View File
@@ -21,9 +21,57 @@ add_custom_command(
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react
DEPENDS
src/SelectThemeDialog.tsx
src/OkCancelDialog.tsx
src/PluginControl.tsx
src/svg/ic_save_bank_as.svg
src/svg/fx_constant.svg
src/svg/fx_distortion.svg
src/svg/ic_help_outline.svg
src/svg/fx_oscillator.svg
src/svg/fx_filter.svg
src/svg/fx_function.svg
src/svg/fx_generator.svg
src/svg/fx_compressor.svg
src/svg/file_download_black_24dp.svg
src/svg/fx_filter_hp.svg
src/svg/fx_plugin.svg
src/svg/fx_error.svg
src/svg/fx_gate.svg
src/svg/ic_midi.svg
src/svg/ic_pluginpreset.svg
src/svg/fx_split_a.svg
src/svg/fx_utility.svg
src/svg/ic_pluginpreset2.svg
src/svg/fx_parametric_eq.svg
src/svg/fx_terminal.svg
src/svg/ic_presets.svg
src/svg/fx_flanger.svg
src/svg/fx_analyzer.svg
src/svg/fx_delay.svg
src/svg/fx_spectral.svg
src/svg/fx_chorus.svg
src/svg/fx_simulator.svg
src/svg/drive_file_rename_outline_black_24dp.svg
src/svg/fx_mixer.svg
src/svg/fx_eq.svg
src/svg/fx_modulator.svg
src/svg/file_upload_black_24dp.svg
src/svg/fx_pitch.svg
src/svg/fx_phaser.svg
src/svg/fx_amplifier.svg
src/svg/fx_lr.svg
src/svg/fx_instrument.svg
src/svg/fx_reverb.svg
src/svg/ic_edit_banks.svg
src/svg/fx_limiter.svg
src/svg/fx_empty.svg
src/svg/fx_dial.svg
src/svg/fx_flanger2.svg
src/svg/fx_split_b.svg
src/svg/fx_spatial.svg
src/svg/fx_converter.svg
src/svg/old_delete_outline_24dp.svg
src/svg/ic_settings.svg
src/reportWebVitals.ts
src/JackHostStatus.tsx
src/PluginPresetsDialog.tsx
@@ -39,6 +87,7 @@ add_custom_command(
src/StringBuilder.tsx
src/FilePropertyDialog.tsx
src/GxTunerControl.tsx
src/DarkMode.tsx
src/App.tsx
src/GxTunerView.tsx
src/Rect.tsx
@@ -66,12 +115,14 @@ add_custom_command(
src/ToobPowerStage2View.tsx
src/ListSelectDialog.tsx
src/ToobWaveShapeView.tsx
src/SelectThemeDialog.tsx
src/PluginInfoDialog.tsx
src/SearchFilter.tsx
src/SearchControl.tsx
src/JackServerSettings.tsx
src/WifiChannel.tsx
src/WifiConfigDialog.tsx
src/FilePropertyDirectoryTree.tsx
src/App.test.tsx
src/PluginPresetSelector.tsx
src/ObservableProperty.tsx
@@ -97,6 +148,7 @@ add_custom_command(
src/AlsaMidiDeviceInfo.tsx
src/ResizeResponsiveComponent.tsx
src/RenameDialog.tsx
src/FilePropertyDirectorySelectDialog.tsx
src/SystemMidiBindingView.tsx
src/OldDeleteIcon.tsx
src/ToobInputStageView.tsx
@@ -122,142 +174,7 @@ add_custom_command(
src/MidiBindingView.tsx
src/PluginIcon.tsx
src/UploadFileDialog.tsx
public/css/roboto.css
public/logo192x.png
public/robots.txt
public/img/ic_drawer_2.png
public/img/fx_constant.svg
public/img/ic_presets_white.svg
public/img/cx_mono.svg
public/img/cx_stereo.svg
public/img/fx_distortion.svg
public/img/Pi-Logo-3.png
public/img/fx_oscillator.svg
src/svg/ic_save_bank_as.svg
src/svg/ic_edit_banks.svg
src/svg/ic_settings.svg
public/img/fx_filter.svg
public/img/delete_outline_black_24dp.svg
public/img/fx_function.svg
public/img/fx_generator.svg
public/img/old_delete_outline_black_24dp.svg
public/img/ic_bank.svg
public/img/fx_compressor.svg
public/img/fx_filter_hp.svg
public/img/fx_plugin.svg
public/img/fx_error.svg
public/img/fx_gate.svg
public/img/cc-by.png
public/img/fx_split_a.svg
public/img/fx_utility.svg
src/svg/ic_help_outline.svg
public/img/ic_navigate_before.svg
public/img/ic_drawer_2.svg
public/img/fx_parametric_eq.svg
public/img/fx_terminal.svg
public/img/ic_presets.svg
public/img/VST_Logo_Steinberg.ico
public/img/fx_flanger.svg
public/img/fx_analyzer.svg
public/img/fx_delay.svg
public/img/fx_spectral.svg
public/img/fx_chorus.svg
public/img/fx_simulator.svg
public/img/fx_mixer.svg
public/img/fx_eq.svg
public/img/fx_modulator.svg
public/img/fx_pitch.svg
public/img/ic_navigate_next.svg
public/img/fx_phaser.svg
public/img/ic_drawer.svg
public/img/fx_amplifier.svg
public/img/fx_lr.svg
public/img/fx_instrument.svg
public/img/Pi-Logo-3.svg
public/img/fx_reverb.svg
public/img/fx_limiter.svg
public/img/VST_Logo_Steinberg.png
public/img/fx_empty.svg
public/img/fx_dial.svg
public/img/ic_logo.svg
public/img/fx_flanger2.svg
public/img/old_delete_outline_white_24dp.svg
public/img/fx_split_b.svg
public/img/fx_spatial.svg
public/img/fx_converter.svg
public/img/vst.svg
public/img/vst.png
public/sample_lv2_plugins.json
public/serve.json
public/fonts/Roboto-LightItalic.woff2
public/fonts/Roboto-Light.woff2
public/fonts/Roboto-BlackItalic.woff2
public/fonts/Roboto-ThinItalic.woff2
public/fonts/Roboto-Bold.woff2
public/fonts/Roboto-Italic.woff2
public/fonts/Roboto-Black.woff2
public/fonts/Roboto-BoldItalic.woff2
public/fonts/Roboto-Regular.woff2
public/fonts/Roboto-Medium.woff2
public/fonts/Roboto-Thin.woff2
public/fonts/LICENSE.txt
public/fonts/Roboto-MediumItalic.woff2
public/favicon.ico
public/iso_codes.json
public/logo192.png
public/logo512.png
public/index.html
public/manifest.json
public/var/config.json
public/var/current_pedalboard.json
public/var/uiplugins.json
public/var/plugin_classes.json
src/svg/fx_constant.svg
src/svg/fx_distortion.svg
src/svg/fx_oscillator.svg
src/svg/fx_filter.svg
src/svg/fx_function.svg
src/svg/fx_generator.svg
src/svg/fx_compressor.svg
src/svg/file_download_black_24dp.svg
src/svg/fx_filter_hp.svg
src/svg/fx_plugin.svg
src/svg/fx_error.svg
src/svg/fx_gate.svg
src/svg/ic_midi.svg
src/svg/ic_pluginpreset.svg
src/svg/fx_split_a.svg
src/svg/fx_utility.svg
src/svg/ic_pluginpreset2.svg
src/svg/fx_parametric_eq.svg
src/svg/fx_terminal.svg
src/svg/ic_presets.svg
src/svg/fx_flanger.svg
src/svg/fx_analyzer.svg
src/svg/fx_delay.svg
src/svg/fx_spectral.svg
src/svg/fx_chorus.svg
src/svg/fx_simulator.svg
src/svg/fx_mixer.svg
src/svg/fx_eq.svg
src/svg/fx_modulator.svg
src/svg/file_upload_black_24dp.svg
src/svg/fx_pitch.svg
src/svg/fx_phaser.svg
src/svg/fx_amplifier.svg
src/svg/fx_lr.svg
src/svg/fx_instrument.svg
src/svg/fx_reverb.svg
src/svg/fx_limiter.svg
src/svg/fx_empty.svg
src/svg/fx_dial.svg
src/svg/fx_flanger2.svg
src/svg/fx_split_b.svg
src/svg/fx_spatial.svg
src/svg/fx_converter.svg
src/svg/old_delete_outline_24dp.svg
)
add_custom_target (
+1 -1
View File
@@ -1,5 +1,5 @@
{
"socket_server_port": 8080,
"socket_server_port": 80,
"socket_server_address": "*",
"debug": true,
"max_upload_size": 536870912,
+384 -372
View File
@@ -59,19 +59,19 @@ import { BankIndex, BankIndexEntry } from './Banks';
import RenameDialog from './RenameDialog';
import JackStatusView from './JackStatusView';
import { Theme } from '@mui/material/styles';
import {isDarkMode} from './DarkMode';
import { isDarkMode } from './DarkMode';
import {ReactComponent as RenameOutlineIcon} from './svg/drive_file_rename_outline_black_24dp.svg';
import {ReactComponent as SaveBankAsIcon} from './svg/ic_save_bank_as.svg';
import {ReactComponent as EditBanksIcon} from './svg/ic_edit_banks.svg';
import {ReactComponent as SettingsIcon} from './svg/ic_settings.svg';
import {ReactComponent as HelpOutlineIcon} from './svg/ic_help_outline.svg';
import { ReactComponent as RenameOutlineIcon } from './svg/drive_file_rename_outline_black_24dp.svg';
import { ReactComponent as SaveBankAsIcon } from './svg/ic_save_bank_as.svg';
import { ReactComponent as EditBanksIcon } from './svg/ic_edit_banks.svg';
import { ReactComponent as SettingsIcon } from './svg/ic_settings.svg';
import { ReactComponent as HelpOutlineIcon } from './svg/ic_help_outline.svg';
import DialogEx from './DialogEx';
const appStyles = (theme: Theme) => createStyles({
"&": { // :root
colorScheme: (isDarkMode()? "dark": "light")
colorScheme: (isDarkMode() ? "dark" : "light")
},
menuListItem: {
color: "#FE8!important" as any, //theme.palette.text.primary,
@@ -95,7 +95,7 @@ const appStyles = (theme: Theme) => createStyles({
top: "0px",
width: "100%",
height: "100%",
background: isDarkMode() ? "#222": "#DDD",
background: isDarkMode() ? "#222" : "#DDD",
opacity: "0.95",
justifyContent: "center",
textAlign: "center",
@@ -114,13 +114,13 @@ const appStyles = (theme: Theme) => createStyles({
top: "0px",
width: "100%",
height: "100%",
color: isDarkMode() ? "#CCC": "#444",
color: isDarkMode() ? "#CCC" : "#444",
zIndex: 2000
},
errorContentMask: {
position: "absolute", left: 0, top: 0,
width: "100%", height: "100%",
background: isDarkMode()? "#121212": "#BBB",
background: isDarkMode() ? "#121212" : "#BBB",
opacity: 0.95,
zIndex: 1999
},
@@ -511,7 +511,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
}
private unmountListener(e: Event) {
if ((!this.model_.reloadRequested) && this.model_.state.get() === State.Ready && !this.model_.isAndroidHosted()) {
if (this.model_.state.get() === State.Ready && !this.model_.isAndroidHosted()) {
e.preventDefault();
(e as any).returnValue = "Are you sure you want to leave this page?";
return "Are you sure you want to leave this page?";
@@ -521,7 +521,7 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
componentDidMount() {
super.componentDidMount();
window.addEventListener("beforeunload",this.unmountListener);
window.addEventListener("beforeunload", this.unmountListener);
this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_);
this.model_.state.addOnChangedHandler(this.stateChangeHandler_);
@@ -533,417 +533,429 @@ const AppThemed = withStyles(appStyles)(class extends ResizeResponsiveComponent<
}
updateOverscroll(): void {
if(this.model_.serverVersion) {
// no pull-down refresh on android devices once we're ready (unless we're debug)
let preventOverscroll =
this.model_.state.get() === State.Ready
&& !this.model_.debug;
updateOverscroll(): void {
if (this.model_.serverVersion) {
// no pull-down refresh on android devices once we're ready (unless we're debug)
let preventOverscroll =
this.model_.state.get() === State.Ready
&& !this.model_.debug;
let overscrollBehavior = preventOverscroll ? "none" : "auto";
document.body.style.overscrollBehavior = overscrollBehavior;
}
let overscrollBehavior = preventOverscroll ? "none" : "auto";
document.body.style.overscrollBehavior = overscrollBehavior;
}
}
componentDidUpdate() {
}
componentDidUpdate() {
}
componentWillUnmount() {
super.componentWillUnmount();
window.removeEventListener("beforeunload",this.unmountListener);
componentWillUnmount() {
super.componentWillUnmount();
window.removeEventListener("beforeunload", this.unmountListener);
this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_);
this.model_.state.removeOnChangedHandler(this.stateChangeHandler_);
this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler);
this.model_.banks.removeOnChangedHandler(this.banksChangedHandler);
this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
this.model_.errorMessage.removeOnChangedHandler(this.errorChangeHandler_);
this.model_.state.removeOnChangedHandler(this.stateChangeHandler_);
this.model_.pedalboard.removeOnChangedHandler(this.presetChangedHandler);
this.model_.banks.removeOnChangedHandler(this.banksChangedHandler);
this.model_.banks.removeOnChangedHandler(this.showStatusMonitorHandler);
}
}
alertMessageChangedHandler() {
let message = this.model_.alertMessage.get();
if (message === "") {
this.setState({ alertDialogOpen: false });
// leave the message intact so the dialog can fade.
} else {
this.setState({
alertDialogOpen: true,
alertDialogMessage: message
});
}
}
updateResponsive() {
// functional, but disabled.
// let tinyToolBar_ = this.windowSize.height < 600;
// this.setState({ tinyToolBar: tinyToolBar_ });
let height = this.windowSize.height;
const ENTRY_HEIGHT = 48;
// ENTRY_HEIGHT*6 +K = 727 from observation.
const K = 450;
let bankEntries = Math.floor((height - K) / ENTRY_HEIGHT);
if (bankEntries < 1) bankEntries = 1;
if (bankEntries > 7) bankEntries = 7;
this.setState({ bankDisplayItems: bankEntries });
}
onWindowSizeChanged(width: number, height: number): void {
super.onWindowSizeChanged(width, height);
this.updateResponsive();
}
setErrorMessage(message: string): void {
this.setState({ errorMessage: message });
}
setDisplayState(newState: State): void {
this.updateOverscroll();
alertMessageChangedHandler() {
let message = this.model_.alertMessage.get();
if (message === "") {
this.setState({ alertDialogOpen: false });
// leave the message intact so the dialog can fade.
} else {
this.setState({
alertDialogOpen: true,
alertDialogMessage: message
displayState: newState,
canFullScreen: supportsFullScreen() && !this.model_.isAndroidHosted()
});
}
}
updateResponsive() {
// functional, but disabled.
// let tinyToolBar_ = this.windowSize.height < 600;
// this.setState({ tinyToolBar: tinyToolBar_ });
let height = this.windowSize.height;
const ENTRY_HEIGHT = 48;
// ENTRY_HEIGHT*6 +K = 727 from observation.
const K = 450;
let bankEntries = Math.floor((height - K) / ENTRY_HEIGHT);
if (bankEntries < 1) bankEntries = 1;
if (bankEntries > 7) bankEntries = 7;
this.setState({ bankDisplayItems: bankEntries });
}
onWindowSizeChanged(width: number, height: number): void {
super.onWindowSizeChanged(width, height);
this.updateResponsive();
}
setErrorMessage(message: string): void {
this.setState({ errorMessage: message });
}
setDisplayState(newState: State): void {
this.updateOverscroll();
this.setState({
displayState: newState,
canFullScreen: supportsFullScreen() && !this.model_.isAndroidHosted()
});
if(newState === State.Ready)
{
if (this.model_.isOnboarding()) {
this.handleDisplayOnboarding();
}
}
}
showDrawer() {
this.setState({ isDrawerOpen: true })
}
hideDrawer() {
this.setState({ isDrawerOpen: false })
}
shortBankList(banks: BankIndex): BankIndexEntry[] {
let n = this.state.bankDisplayItems;
let entries = banks.entries;
if (entries.length < n + 1) { // +1 for the .... entry.
return entries;
}
let result: BankIndexEntry[] = [];
let selectedIndex = -1;
for (let i = 0; i < entries.length; ++i) {
if (entries[i].instanceId === banks.selectedBank) {
selectedIndex = i;
break;
if (newState === State.Ready) {
if (this.model_.isOnboarding()) {
this.handleDisplayOnboarding();
}
}
}
if (n > entries.length) n = entries.length;
if (selectedIndex > n) {
for (let i = 0; i < n - 1; ++i) {
result.push(entries[i]);
showDrawer() {
this.setState({ isDrawerOpen: true })
}
hideDrawer() {
this.setState({ isDrawerOpen: false })
}
shortBankList(banks: BankIndex): BankIndexEntry[] {
let n = this.state.bankDisplayItems;
let entries = banks.entries;
if (entries.length < n + 1) { // +1 for the .... entry.
return entries;
}
result.push(entries[selectedIndex]);
} else {
for (let i = 0; i < n; ++i) {
result.push(entries[i]);
let result: BankIndexEntry[] = [];
let selectedIndex = -1;
for (let i = 0; i < entries.length; ++i) {
if (entries[i].instanceId === banks.selectedBank) {
selectedIndex = i;
break;
}
}
if (n > entries.length) n = entries.length;
if (selectedIndex > n) {
for (let i = 0; i < n - 1; ++i) {
result.push(entries[i]);
}
result.push(entries[selectedIndex]);
} else {
for (let i = 0; i < n; ++i) {
result.push(entries[i]);
}
}
return result;
}
handleReload() {
if (this.model_.isAndroidHosted()) {
this.model_.chooseNewDevice();
} else {
window.location.reload();
}
}
return result;
}
handleReload() {
if (this.model_.isAndroidHosted()) {
this.model_.chooseNewDevice();
} else {
window.location.reload();
getReloadingMessage(): string {
switch (this.state.displayState) {
case State.ApplyingChanges:
return "Applying\u00A0changes...";
case State.ReloadingPlugins:
return "Reloading\u00A0plugins...";
default:
return "Reconnecting...";
}
}
}
render() {
render() {
const { classes } = this.props;
const { classes } = this.props;
let shortBankList = this.shortBankList(this.state.banks);
let showBankSelectDialog = shortBankList.length !== this.state.banks.entries.length;
let shortBankList = this.shortBankList(this.state.banks);
let showBankSelectDialog = shortBankList.length !== this.state.banks.entries.length;
return (
<div style={{
colorScheme: isDarkMode() ? "dark": "light", // affects scrollbar color
minHeight: 345, minWidth: 390,
position: "absolute", width: "100%", height: "100%", background: "#F88", userSelect: "none",
display: "flex", flexDirection: "column", flexWrap: "nowrap",
overscrollBehavior: this.state.isDebug ? "auto" : "none"
}}
onContextMenu={(e) => {
if (!this.model_.debug) {
e.preventDefault(); e.stopPropagation();
}
return (
<div style={{
colorScheme: isDarkMode() ? "dark" : "light", // affects scrollbar color
minHeight: 345, minWidth: 390,
position: "absolute", width: "100%", height: "100%", background: "#F88", userSelect: "none",
display: "flex", flexDirection: "column", flexWrap: "nowrap",
overscrollBehavior: this.state.isDebug ? "auto" : "none"
}}
>
<CssBaseline />
{(!this.state.tinyToolBar) ?
(
<AppBar position="absolute" >
<Toolbar variant="dense" className={classes.toolBar} >
<IconButton
edge="start"
aria-label="menu"
color="inherit"
onClick={() => { this.showDrawer() }}
size="large">
<MenuButton style={{opacity: 0.75}} />
</IconButton>
<div style={{ flex: "0 1 400px", minWidth: 100 }}>
<PresetSelector />
</div>
<div style={{ flex: "2 2 30px" }} />
{this.state.canFullScreen &&
onContextMenu={(e) => {
if (!this.model_.debug) {
e.preventDefault(); e.stopPropagation();
}
}}
>
<CssBaseline />
{(!this.state.tinyToolBar) ?
(
<AppBar position="absolute" >
<Toolbar variant="dense" className={classes.toolBar} >
<IconButton
edge="start"
aria-label="menu"
onClick={() => { this.toggleFullScreen(); }}
color="inherit"
onClick={() => { this.showDrawer() }}
size="large">
<MenuButton style={{ opacity: 0.75 }} />
</IconButton>
<div style={{ flex: "0 1 400px", minWidth: 100 }}>
<PresetSelector />
</div>
<div style={{ flex: "2 2 30px" }} />
{this.state.canFullScreen &&
<IconButton
aria-label="menu"
onClick={() => { this.toggleFullScreen(); }}
color="inherit"
size="large">
{this.state.isFullScreen ? (
<FullscreenExitIcon style={{ opacity: 0.75 }} />
) : (
<FullscreenIcon style={{ opacity: 0.75 }} />
)}
</IconButton>
}
</Toolbar>
</AppBar>
) : (
<div className={classes.toolBarContent} >
<IconButton
style={{ position: "absolute", left: 12, top: 8, zIndex: 2 }}
aria-label="menu"
onClick={() => { this.showDrawer() }}
color="inherit"
size="large">
<MenuButton />
</IconButton>
{this.state.canFullScreen && (
<IconButton
style={{ position: "absolute", right: 8, top: 8, zIndex: 2 }}
aria-label="menu"
color="inherit"
onClick={() => { this.toggleFullScreen(); }}
size="large">
{this.state.isFullScreen ? (
<FullscreenExitIcon style={{opacity: 0.75}} />
<FullscreenExitIcon />
) : (
<FullscreenIcon style={{opacity: 0.75}} />
<FullscreenIcon />
)}
</IconButton>
}
</Toolbar>
</AppBar>
) : (
<div className={classes.toolBarContent} >
<IconButton
style={{ position: "absolute", left: 12, top: 8, zIndex: 2 }}
aria-label="menu"
onClick={() => { this.showDrawer() }}
color="inherit"
size="large">
<MenuButton />
</IconButton>
{this.state.canFullScreen && (
<IconButton
style={{ position: "absolute", right: 8, top: 8, zIndex: 2 }}
aria-label="menu"
color="inherit"
onClick={() => { this.toggleFullScreen(); }}
size="large">
{this.state.isFullScreen ? (
<FullscreenExitIcon />
) : (
<FullscreenIcon />
)}
</div>
)}
<TemporaryDrawer position='left' title="PiPedal"
is_open={this.state.isDrawerOpen} onClose={() => { this.hideDrawer(); }} >
<ListSubheader className="listSubheader" component="div" id="xnested-list-subheader" style={{ background: "rgba(12,12,12,0.0)" }}>
<Typography variant="caption" style={{ position: "relative", top: 15 }}>Banks</Typography></ListSubheader>
)}
<List >
{
shortBankList.map((bank) => {
return (
<ListItem button key={'bank' + bank.instanceId} selected={bank.instanceId === this.state.banks.selectedBank}
onClick={() => this.onOpenBank(bank.instanceId)}
>
</IconButton>
)}
</div>
)}
<TemporaryDrawer position='left' title="PiPedal"
is_open={this.state.isDrawerOpen} onClose={() => { this.hideDrawer(); }} >
<ListSubheader className="listSubheader" component="div" id="xnested-list-subheader" style={{background: "rgba(12,12,12,0.0)"}}>
<Typography variant="caption" style={{position: "relative", top: 15}}>Banks</Typography></ListSubheader>
<ListItemText primary={bank.name} />
</ListItem>
<List >
{
shortBankList.map((bank) => {
return (
<ListItem button key={'bank' + bank.instanceId} selected={bank.instanceId === this.state.banks.selectedBank}
onClick={() => this.onOpenBank(bank.instanceId)}
);
})
}
{
showBankSelectDialog && (
<ListItem button key={'bankDOTDOTDOT'} selected={false}
onClick={() => this.handleDrawerSelectBank()}
>
<ListItemText primary={bank.name} />
<ListItemText primary={"..."} />
</ListItem>
);
)
}
</List>
<Divider />
<List>
<ListItem button key='RenameBank' onClick={() => { this.handleDrawerRenameBank() }}>
<ListItemIcon >
<RenameOutlineIcon color='inherit' className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='Rename Bank' />
</ListItem>
<ListItem button key='SaveBank' onClick={() => { this.handleDrawerSaveBankAs() }} >
<ListItemIcon>
<SaveBankAsIcon color="inherit" className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='Save As New Bank' />
</ListItem>
<ListItem button key='EditBanks' onClick={() => { this.handleDrawerManageBanks(); }}>
<ListItemIcon>
<EditBanksIcon color="inherit" className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='Manage Banks...' />
</ListItem>
</List>
<Divider />
<List>
<ListItem button key='Settings' onClick={() => { this.handleDrawerSettingsClick() }}>
<ListItemIcon>
<SettingsIcon color="inherit" className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='Settings' />
</ListItem>
<ListItem button key='About' onClick={() => { this.handleDrawerAboutClick() }}>
<ListItemIcon>
<HelpOutlineIcon color="inherit" className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='About' />
</ListItem>
<ListItem button key='Donations' onClick={() => { this.handleDrawerDonationClick() }}>
<ListItemIcon >
<VolunteerActivismIcon className={classes.menuIcon} color="inherit" />
</ListItemIcon>
<ListItemText primary='Donations' />
</ListItem>
</List>
</TemporaryDrawer>
{!this.state.tinyToolBar && (
<Toolbar className={classes.toolBarSpacer} variant="dense"
/>
)}
<main className={classes.mainFrame} >
<div className={classes.mainSizingPosition}>
<div className={classes.heroContent}>
{(this.state.displayState !== State.Loading) && (
<MainPage hasTinyToolBar={this.state.tinyToolBar} />
)}
</div>
</div>
</main>
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
<SettingsDialog
open={this.state.isSettingsDialogOpen}
onboarding={this.state.onboarding}
onClose={() => this.handleSettingsDialogClose()} />
<RenameDialog
open={this.state.renameBankDialogOpen || this.state.saveBankAsDialogOpen}
defaultName={this.model_.banks.get().getSelectedEntryName()}
acceptActionName={this.state.renameBankDialogOpen ? "Rename" : "Save as"}
onClose={() => {
this.setState({
renameBankDialogOpen: false,
saveBankAsDialogOpen: false
})
}}
onOk={(text: string) => {
if (this.state.renameBankDialogOpen) {
this.handleBankRenameOk(text);
} else if (this.state.saveBankAsDialogOpen) {
this.handleSaveBankAsOk(text);
}
}
{
showBankSelectDialog && (
<ListItem button key={'bankDOTDOTDOT'} selected={false}
onClick={() => this.handleDrawerSelectBank()}
>
<ListItemText primary={"..."} />
</ListItem>
)
}
</List>
<Divider />
<List>
<ListItem button key='RenameBank' onClick={() => { this.handleDrawerRenameBank() }}>
<ListItemIcon >
<RenameOutlineIcon color='inherit' className={classes.menuIcon} />
</ListItemIcon>
<ListItemText primary='Rename Bank' />
</ListItem>
<ListItem button key='SaveBank' onClick={() => { this.handleDrawerSaveBankAs() }} >
<ListItemIcon>
<SaveBankAsIcon color="inherit" className={classes.menuIcon}/>
</ListItemIcon>
<ListItemText primary='Save As New Bank' />
</ListItem>
<ListItem button key='EditBanks' onClick={() => { this.handleDrawerManageBanks(); }}>
<ListItemIcon>
<EditBanksIcon color="inherit" className={classes.menuIcon}/>
</ListItemIcon>
<ListItemText primary='Manage Banks...' />
</ListItem>
</List>
<Divider />
<List>
<ListItem button key='Settings' onClick={() => { this.handleDrawerSettingsClick() }}>
<ListItemIcon>
<SettingsIcon color="inherit" className={classes.menuIcon}/>
</ListItemIcon>
<ListItemText primary='Settings' />
</ListItem>
<ListItem button key='About' onClick={() => { this.handleDrawerAboutClick() }}>
<ListItemIcon>
<HelpOutlineIcon color="inherit" className={classes.menuIcon}/>
</ListItemIcon>
<ListItemText primary='About' />
</ListItem>
<ListItem button key='Donations' onClick={() => { this.handleDrawerDonationClick() }}>
<ListItemIcon >
<VolunteerActivismIcon className={classes.menuIcon} color="inherit" />
</ListItemIcon>
<ListItemText primary='Donations' />
</ListItem>
</List>
</TemporaryDrawer>
{!this.state.tinyToolBar && (
<Toolbar className={classes.toolBarSpacer} variant="dense"
/>
)}
<main className={classes.mainFrame} >
<div className={classes.mainSizingPosition}>
<div className={classes.heroContent}>
{(this.state.displayState !== State.Loading) && (
<MainPage hasTinyToolBar={this.state.tinyToolBar} />
)}
</div>
</div>
</main>
<BankDialog show={this.state.bankDialogOpen} isEditDialog={this.state.editBankDialogOpen} onDialogClose={() => this.setState({ bankDialogOpen: false })} />
<AboutDialog open={this.state.aboutDialogOpen} onClose={() => this.setState({ aboutDialogOpen: false })} />
<SettingsDialog
open={this.state.isSettingsDialogOpen}
onboarding={this.state.onboarding}
onClose={() => this.handleSettingsDialogClose()} />
<RenameDialog
open={this.state.renameBankDialogOpen || this.state.saveBankAsDialogOpen}
defaultName={this.model_.banks.get().getSelectedEntryName()}
acceptActionName={this.state.renameBankDialogOpen ? "Rename": "Save as"}
onClose={() => {
this.setState({
renameBankDialogOpen: false,
saveBankAsDialogOpen: false
})
}}
onOk={(text: string) => {
if (this.state.renameBankDialogOpen) {
this.handleBankRenameOk(text);
} else if (this.state.saveBankAsDialogOpen) {
this.handleSaveBankAsOk(text);
<ZoomedUiControl
dialogOpen={this.state.zoomedControlOpen}
controlInfo={this.state.zoomedControlInfo}
onDialogClose={() => { this.setState({ zoomedControlOpen: false }); }}
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
}
}
}
/>
<ZoomedUiControl
dialogOpen={this.state.zoomedControlOpen}
controlInfo={this.state.zoomedControlInfo}
onDialogClose={() => { this.setState({ zoomedControlOpen: false }); }}
onDialogClosed={() => { this.model_.zoomedUiControl.set(undefined); }
}
/>
<DialogEx
tag="Alert"
open={this.state.alertDialogOpen}
onClose={this.handleCloseAlert}
aria-describedby="alert-dialog-description"
>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<Typography variant="body2">
{
this.state.alertDialogMessage
}
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant="dialogPrimary" onClick={this.handleCloseAlert} color="primary" autoFocus>
OK
</Button>
</DialogActions>
</DialogEx>
{this.state.showStatusMonitor && (<JackStatusView />)}
<div className={classes.errorContent} style={{
display: (this.state.displayState === State.Reconnecting || this.state.displayState === State.ApplyingChanges)
? "block" : "none"
}}
>
<div className={classes.errorContentMask} />
<div className={classes.loadingBox}>
<div className={classes.loadingBoxItem}>
<CircularProgress color="inherit" className={classes.loadingBoxItem} />
</div>
<Typography noWrap variant="body2" className={classes.progressText}>
{this.state.displayState === State.ApplyingChanges ? "Applying\u00A0changes..." : "Reconnecting..."}
</Typography>
</div>
</div>
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Error ? "flex" : "none" }}
onMouseDown={preventDefault} onKeyDown={preventDefault}
>
<div className={classes.errorContentMask} />
<div style={{ flex: "2 2 3px", height: 20 }} >&nbsp;</div>
<div className={classes.errorMessageBox} style={{ position: "relative" }} >
<div style={{ fontSize: "30px", position: "absolute", left: 0, top: 3, color: "#A00" }}>
<ErrorOutlineIcon color="inherit" fontSize="inherit" style={{ float: "left", marginRight: "12px" }} />
</div>
<div style={{ marginLeft: 40, marginTop: 3 }}>
<p className={classes.errorText}>
Error: {this.state.errorMessage}
</p>
</div>
<div style={{ paddingTop: 50, paddingLeft: 36, textAlign: "left" }}>
<Button variant='contained' color="primary" component='button'
onClick={() => this.handleReload()} >
Reload
/>
<DialogEx
tag="Alert"
open={this.state.alertDialogOpen}
onClose={this.handleCloseAlert}
aria-describedby="alert-dialog-description"
>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<Typography variant="body2">
{
this.state.alertDialogMessage
}
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant="dialogPrimary" onClick={this.handleCloseAlert} color="primary" autoFocus>
OK
</Button>
</DialogActions>
</DialogEx>
{this.state.showStatusMonitor && (<JackStatusView />)}
<div className={classes.errorContent} style={{
display: (
this.state.displayState === State.Reconnecting
|| this.state.displayState === State.ApplyingChanges
|| this.state.displayState === State.ReloadingPlugins)
? "block" : "none"
}}
>
<div className={classes.errorContentMask} />
<div className={classes.loadingBox}>
<div className={classes.loadingBoxItem}>
<CircularProgress color="inherit" className={classes.loadingBoxItem} />
</div>
<Typography noWrap variant="body2" className={classes.progressText}>
{this.getReloadingMessage()}
</Typography>
</div>
</div>
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Error ? "flex" : "none" }}
onMouseDown={preventDefault} onKeyDown={preventDefault}
>
<div className={classes.errorContentMask} />
<div style={{ flex: "2 2 3px", height: 20 }} >&nbsp;</div>
<div className={classes.errorMessageBox} style={{ position: "relative" }} >
<div style={{ fontSize: "30px", position: "absolute", left: 0, top: 3, color: "#A00" }}>
<ErrorOutlineIcon color="inherit" fontSize="inherit" style={{ float: "left", marginRight: "12px" }} />
</div>
<div style={{ marginLeft: 40, marginTop: 3 }}>
<p className={classes.errorText}>
Error: {this.state.errorMessage}
</p>
</div>
<div style={{ paddingTop: 50, paddingLeft: 36, textAlign: "left" }}>
<Button variant='contained' color="primary" component='button'
onClick={() => this.handleReload()} >
Reload
</Button>
</div>
</div>
<div style={{ flex: "5 5 auto", height: 20 }} >&nbsp;</div>
</div>
<div style={{ flex: "5 5 auto", height: 20 }} >&nbsp;</div>
</div>
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Loading ? "block" : "none" }}>
<div className={classes.errorContentMask} />
<div className={classes.loadingBox}>
<div className={classes.loadingBoxItem}>
<CircularProgress color="inherit" className={classes.loadingBoxItem} />
<div className={classes.errorContent} style={{ display: this.state.displayState === State.Loading ? "block" : "none" }}>
<div className={classes.errorContentMask} />
<div className={classes.loadingBox}>
<div className={classes.loadingBoxItem}>
<CircularProgress color="inherit" className={classes.loadingBoxItem} />
</div>
<Typography noWrap variant="body2" className={classes.loadingBoxItem}>
Loading...
</Typography>
</div>
<Typography noWrap variant="body2" className={classes.loadingBoxItem}>
Loading...
</Typography>
</div>
</div>
</div >
);
}
</div >
);
}
}
);
+96 -76
View File
@@ -42,6 +42,7 @@ import SearchFilter from './SearchFilter';
import { FixedSizeGrid } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import StarBorderIcon from '@mui/icons-material/StarBorder';
import StarIcon from '@mui/icons-material/Star';
import { createStyles, Theme } from '@mui/material/styles';
import { WithStyles, withStyles } from '@mui/styles';
import FilterListIcon from '@mui/icons-material/FilterList';
@@ -86,7 +87,7 @@ const pluginGridStyles = (theme: Theme) => createStyles({
justifyContent: "space-between",
paddingLeft: "24px",
paddingRight: "48px",
},
paper: {
position: "relative",
@@ -172,8 +173,9 @@ type PluginGridState = {
grid_cell_width: number,
grid_cell_columns: number,
minimumItemWidth: number,
favoritesList: FavoritesList
gridItems: UiPlugin[];
favoritesList: FavoritesList,
uiPlugins: UiPlugin[]
//gridItems: UiPlugin[];
}
@@ -181,8 +183,7 @@ type PluginGridState = {
export const LoadPluginDialog =
withStyles(pluginGridStyles, { withTheme: true })(
class extends ResizeResponsiveComponent<PluginGridProps, PluginGridState>
{
class extends ResizeResponsiveComponent<PluginGridProps, PluginGridState> {
model: PiPedalModel;
searchInputRef: React.RefObject<HTMLInputElement>;
@@ -208,7 +209,7 @@ export const LoadPluginDialog =
grid_cell_columns: this.getCellColumns(window.innerWidth),
minimumItemWidth: props.minimumItemWidth ? props.minimumItemWidth : 220,
favoritesList: this.model.favorites.get(),
gridItems: this.getFilteredPlugins("", filterType_, this.model.favorites.get())
uiPlugins: this.model.ui_plugins.get()
};
@@ -218,11 +219,12 @@ export const LoadPluginDialog =
this.handleSearchStringReady = this.handleSearchStringReady.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleFavoritesChanged = this.handleFavoritesChanged.bind(this);
this.handlePluginsChanged = this.handlePluginsChanged.bind(this);
this.requestScrollTo();
}
nominal_column_width: number = 250;
nominal_column_width: number = 350;
margin_reserve: number = 30;
getCellColumns(width: number) {
@@ -281,23 +283,31 @@ export const LoadPluginDialog =
this.setState(
{
favoritesList: favorites,
gridItems: this.getFilteredPlugins(null, null, favorites)
});
}
handlePluginsChanged(plugins: UiPlugin[]) {
this.setState(
{
uiPlugins: plugins
}
);
}
componentDidMount() {
super.componentDidMount();
this.updateWindowSize();
window.addEventListener('resize', this.updateWindowSize);
this.model.favorites.addOnChangedHandler(this.handleFavoritesChanged);
let favorites = this.model.favorites.get();
this.model.ui_plugins.addOnChangedHandler(this.handlePluginsChanged);
this.setState({
favoritesList: favorites,
gridItems: this.getFilteredPlugins(null, null, favorites)
favoritesList: this.model.favorites.get(),
uiPlugins: this.model.ui_plugins.get()
});
}
componentWillUnmount() {
this.model.ui_plugins.removeOnChangedHandler(this.handlePluginsChanged);
this.model.favorites.removeOnChangedHandler(this.handleFavoritesChanged);
super.componentWillUnmount();
window.removeEventListener('resize', this.updateWindowSize);
@@ -311,7 +321,6 @@ export const LoadPluginDialog =
this.setState({
search_string: "",
search_collapsed: true,
gridItems: this.getFilteredPlugins("", null, null)
});
}
this.requestScrollTo();
@@ -332,7 +341,6 @@ export const LoadPluginDialog =
this.requestScrollTo();
this.setState({
filterType: filterValue,
gridItems: this.getFilteredPlugins(null, filterValue, null)
});
}
onClearFilter(): void {
@@ -341,8 +349,7 @@ export const LoadPluginDialog =
if (this.state.filterType !== value) {
this.requestScrollTo();
this.setState({
filterType: value,
gridItems: this.getFilteredPlugins(null, value, null)
filterType: value
});
}
}
@@ -436,15 +443,15 @@ export const LoadPluginDialog =
if (uiPlugin.author_name !== "") {
if (uiPlugin.author_homepage !== "") {
return (<Fragment>
<div style={{flex: "0 1 auto"}}>
<div style={{ flex: "0 1 auto" }}>
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.name + ",\u00A0"}</Typography>
</div>
<div style={{flex: "0 1 auto"}}>
<div style={{ flex: "0 1 auto" }}>
<a href={uiPlugin.author_homepage} target="_blank" rel="noopener noreferrer" >
<Typography variant='body2' color="textSecondary" noWrap>{uiPlugin.author_name}</Typography>
</a>
</div>
<div style={{flex: "0 0 auto"}}>
<div style={{ flex: "0 0 auto" }}>
<Typography variant='body2' color="textSecondary" noWrap>{stereoIndicator}</Typography>
</div>
</Fragment>);
@@ -481,51 +488,59 @@ export const LoadPluginDialog =
return result;
}
getFilteredPlugins(searchString: string | null, filterType: PluginType | null, favoritesList: FavoritesList | null): UiPlugin[] {
if (searchString === null) {
searchString = this.state.search_string;
}
if (filterType === null) {
filterType = this.state.filterType;
}
if (favoritesList === null) {
favoritesList = this.state.favoritesList;
}
let plugins = this.model.ui_plugins.get();
getFilteredPlugins(plugins: UiPlugin[], searchString: string | null, filterType: PluginType | null, favoritesList: FavoritesList | null): UiPlugin[] {
try {
if (searchString === null) {
searchString = this.state.search_string;
}
if (filterType === null) {
filterType = this.state.filterType;
}
if (favoritesList === null) {
favoritesList = this.state.favoritesList;
}
let results: { score: number; plugin: UiPlugin }[] = [];
let searchFilter = new SearchFilter(searchString);
let rootClass = this.model.plugin_classes.get();
let results: { score: number; plugin: UiPlugin }[] = [];
let searchFilter = new SearchFilter(searchString);
let rootClass = this.model.plugin_classes.get();
for (let i = 0; i < plugins.length; ++i) {
let plugin = plugins[i];
try {
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
let score: number = 0;
if (plugin.is_vst3) {
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name, "vst3");
} else {
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name);
}
for (let i = 0; i < plugins.length; ++i) {
let plugin = plugins[i];
if (filterType === PluginType.Plugin || rootClass.is_type_of(filterType, plugin.plugin_type)) {
let score: number = 0;
if (plugin.is_vst3) {
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name, "vst3");
} else {
score = searchFilter.score(plugin.name, plugin.plugin_display_type, plugin.author_name);
}
if (score !== 0) {
if (favoritesList[plugin.uri]) {
score += 32768;
if (score !== 0) {
if (favoritesList[plugin.uri]) {
score += 32768;
}
results.push({ score: score, plugin: plugin });
}
}
results.push({ score: score, plugin: plugin });
} catch (e: any) {
alert("Bad plugin:" + (plugin?.name ?? "null") + " plugin type: " + (plugin?.plugin_type ?? "null"
+ " " + e.toString()));
}
}
results.sort((left: { score: number; plugin: UiPlugin }, right: { score: number; plugin: UiPlugin }) => {
if (right.score < left.score) return -1;
if (right.score > left.score) return 1;
return left.plugin.name.localeCompare(right.plugin.name);
});
let t: UiPlugin[] = [];
for (let i = 0; i < results.length; ++i) {
t.push(results[i].plugin);
}
return t;
} catch (e: any) {
alert("getFilteredPlugins: " + e.toString());
throw e;
}
results.sort((left: { score: number; plugin: UiPlugin }, right: { score: number; plugin: UiPlugin }) => {
if (right.score < left.score) return -1;
if (right.score > left.score) return 1;
return left.plugin.name.localeCompare(right.plugin.name);
});
let t: UiPlugin[] = [];
for (let i = 0; i < results.length; ++i) {
t.push(results[i].plugin);
}
return t;
}
changedSearchString?: string = undefined;
@@ -536,12 +551,14 @@ export const LoadPluginDialog =
this.scrollToRequested = true;
}
private cachedGridItems: UiPlugin[] = []; // retained for the scrollTo callback.
handleScrollToCallback(element: FixedSizeGrid): void {
if (element) {
if (this.scrollToRequested) {
this.scrollToRequested = false;
let position = -1;
let gridItems = this.state.gridItems;
let gridItems = this.cachedGridItems;
for (let i = 0; i < gridItems.length; ++i) {
if (this.state.selected_uri === gridItems[i].uri) {
position = i;
@@ -560,8 +577,7 @@ export const LoadPluginDialog =
if (this.changedSearchString !== undefined) {
this.requestScrollTo();
this.setState({
search_string: this.changedSearchString,
gridItems: this.getFilteredPlugins(this.changedSearchString, null, null)
search_string: this.changedSearchString
});
this.changedSearchString = undefined;
}
@@ -580,9 +596,8 @@ export const LoadPluginDialog =
2000);
}
renderItem(row: number, column: number): React.ReactNode {
renderItem(gridItems: UiPlugin[], row: number, column: number): React.ReactNode {
let item: number = (row) * this.gridColumnCount + (column);
let gridItems = this.state.gridItems;
if (item >= gridItems.length) {
return (<div />);
}
@@ -613,7 +628,7 @@ export const LoadPluginDialog =
{
isFavorite && (
<div style={{ flex: "0 0 auto" }}>
<StarBorderIcon sx={{ color: "#C80", fontSize: 16, marginRight: "2px" }} />
<StarIcon sx={{ color: "#C80", fontSize: 16, marginRight: "2px" }} />
</div>
)
}
@@ -639,6 +654,7 @@ export const LoadPluginDialog =
}
gridColumnCount: number = 1;
render() {
const { classes } = this.props;
@@ -693,8 +709,7 @@ export const LoadPluginDialog =
this.requestScrollTo();
this.setState({
search_collapsed: true,
search_string: "",
gridItems: this.getFilteredPlugins("", null, null)
search_string: ""
});
} else {
@@ -710,8 +725,7 @@ export const LoadPluginDialog =
this.requestScrollTo();
this.setState({
search_collapsed: true,
search_string: "",
gridItems: this.getFilteredPlugins("", null, null)
search_string: ""
});
} else {
@@ -756,7 +770,9 @@ export const LoadPluginDialog =
}
let width = arg.width ?? 1;
let height = arg.height ?? 1;
let gridItems = this.state.gridItems;
let gridItems = this.getFilteredPlugins(
this.state.uiPlugins, this.state.search_string, this.state.filterType, this.state.favoritesList);
this.cachedGridItems = gridItems;
let scrollRef = (grid: FixedSizeGrid) => { this.handleScrollToCallback(grid); }
@@ -785,7 +801,7 @@ export const LoadPluginDialog =
{(arg: { columnIndex: number, rowIndex: number, style: CSSProperties }) => (
<div style={arg.style} >
{
this.renderItem(arg.rowIndex, arg.columnIndex)
this.renderItem(gridItems, arg.rowIndex, arg.columnIndex)
}
</div>
)}
@@ -829,17 +845,21 @@ export const LoadPluginDialog =
color: isFavorite ? "#F80" : "#CCC", display: (this.state.selected_uri ? "block" : "block"),
position: "relative"
}}>
<IconButton color="inherit" aria-label="Set as favorite"
onClick={() => { this.setFavorite(this.state.selected_uri, !isFavorite); }}
>
<StarBorderIcon />
</IconButton>
{
this.state.selected_uri !== "uri://two-play/pipedal/pedalboard#Empty" && (
<IconButton color="inherit" aria-label="Set as favorite"
onClick={() => { this.setFavorite(this.state.selected_uri, !isFavorite); }}
>
<StarBorderIcon />
</IconButton>
)
}
</div>
</div>
<div className={classes.bottom} style={{height: 64}}>
<div className={classes.bottom} style={{ height: 64 }}>
<div style={{ flex: "1 1 1px" }} />
<div style={{ position: "relative", flex: "0 1 auto", display: "flex", alignItems: "center" }}>
<Button variant="dialogSecondary" onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
<Button variant="dialogSecondary" onClick={this.handleCancel} style={{ height: 48 }} >Cancel</Button>
<Button variant="dialogPrimary" onClick={this.handleOk} disabled={selectedPlugin === null} style={{ height: 48 }} >SELECT</Button>
</div>
</div>
+38 -19
View File
@@ -48,7 +48,14 @@ export enum State {
Error,
Background,
Reconnecting,
ApplyingChanges
ApplyingChanges,
ReloadingPlugins
};
export enum ReconnectReason {
Disconnected,
LoadingSettings,
ReloadingPlugins,
};
export type ControlValueChangedHandler = (key: string, value: number) => void;
@@ -356,6 +363,7 @@ export class PiPedalModel //implements PiPedalModel
lv2Path: string = "";
webSocket?: PiPedalSocket;
ui_plugins: ObservableProperty<UiPlugin[]>
= new ObservableProperty<UiPlugin[]>([]);
state: ObservableProperty<State> = new ObservableProperty<State>(State.Loading);
@@ -426,13 +434,20 @@ export class PiPedalModel //implements PiPedalModel
onSocketReconnecting(retry: number, maxRetries: number): void {
if (this.visibilityState.get() === VisibilityState.Hidden) return;
//if (retry !== 0) {
if (this.restartExpected) {
this.setState(State.ApplyingChanges);
this.restartExpected = false;
} else {
switch (this.reconnectReason)
{
case ReconnectReason.Disconnected:
default:
this.setState(State.Reconnecting);
break;
case ReconnectReason.LoadingSettings:
this.setState(State.ApplyingChanges);
break;
case ReconnectReason.ReloadingPlugins:
this.setState(State.ReloadingPlugins);
break;
}
//}
}
@@ -602,10 +617,17 @@ export class PiPedalModel //implements PiPedalModel
{
this.showAlert(body as string);
} else {
throw new PiPedalStateError("Unrecognized message received from server: " + message);
}
else if (message === "onLv2PluginsChanging")
{
this.onLv2PluginsChanging();
}
}
onLv2PluginsChanging() : void {
this.reconnectReason = ReconnectReason.ReloadingPlugins;
// this.webSocket?.reconnect(); // let the server do it for us.
}
setError(message: string): void {
this.errorMessage.set(message);
this.setState(State.Error);
@@ -630,6 +652,11 @@ export class PiPedalModel //implements PiPedalModel
}
private reconnectReason: ReconnectReason = ReconnectReason.Disconnected;
isReloading(): boolean {
return this.state.get() !== State.Ready;
}
getWebSocket(): PiPedalSocket {
if (this.webSocket === undefined) {
@@ -654,7 +681,7 @@ export class PiPedalModel //implements PiPedalModel
this.androidHost?.setDisconnected(false);
}
this.restartExpected = false;
this.reconnectReason = ReconnectReason.Disconnected;
if (this.visibilityState.get() === VisibilityState.Hidden) return;
// reload state, but not configuration.
@@ -1689,15 +1716,9 @@ export class PiPedalModel //implements PiPedalModel
this.alertMessage.set(message);
}
restartExpected: boolean = false;
expectRestart() {
this.restartExpected = true;
}
setJackSettings(jackSettings: JackChannelSelection): void {
this.expectRestart();
this.reconnectReason = ReconnectReason.LoadingSettings;
this.webSocket?.send("setJackSettings", jackSettings);
}
@@ -2423,7 +2444,7 @@ export class PiPedalModel //implements PiPedalModel
resolve();
});
this.restartExpected = true;
this.reconnectReason = ReconnectReason.LoadingSettings;
this.webSocket?.reconnect(); // close immediately, and wait for recoonnect.
return result;
}
@@ -2619,10 +2640,8 @@ export class PiPedalModel //implements PiPedalModel
}
}
reloadRequested: boolean = false;
reloadPage() {
this.reloadRequested = true;
// eslint-disable-next-line no-restricted-globals
let url = window.location.href.split('#')[0];
window.location.href = url;
+4 -4
View File
@@ -62,7 +62,7 @@ const styles = (theme: Theme) => createStyles({
transition: theme.transitions.create(['opacity', 'background-color'], {
duration: theme.transitions.duration.shortest
}),
backgroundColor: theme.palette.secondary.main,
backgroundColor: theme.palette.primary.main,
opacity: theme.palette.mode === 'light' ? 0.38 : 0.3
},
displayValue: {
@@ -525,7 +525,7 @@ const PluginControl =
if (control.isOnOffSwitch()) {
// normal gray unchecked state.
return (
<Switch checked={value !== 0} color="secondary"
<Switch checked={value !== 0} color="primary"
onChange={(event) => {
this.onCheckChanged(event.target.checked);
}}
@@ -535,14 +535,14 @@ const PluginControl =
if (control.isAbToggle()) {
// unchecked color is not gray.
return (
<Switch checked={value !== 0} color="secondary"
<Switch checked={value !== 0} color="primary"
onChange={(event) => {
this.onCheckChanged(event.target.checked);
}}
classes={{
track: this.props.classes.switchTrack
}}
style={{ color: this.props.theme.palette.secondary.main }}
style={{ color: this.props.theme.palette.primary.main }}
/>
);
} else {
+1 -1
View File
@@ -272,7 +272,7 @@ const WifiConfigDialog = withStyles(styles, { withTheme: true })(
<Switch
checked={this.state.enabled}
onChange={(e: any) => this.handleEnableChanged(e)}
color="secondary"
color="primary"
/>
)}
label="Enable"
+1 -1
View File
@@ -325,7 +325,7 @@ const WifiDirectConfigDialog = withStyles(styles, { withTheme: true })(
<Switch
checked={this.state.enabled}
onChange={(e: any) => this.handleEnableChanged(e)}
color="secondary"
color="primary"
/>
)}
label="Wi-Fi Direct Hotspot"
+4 -4
View File
@@ -44,7 +44,7 @@ const styles = (theme: Theme) => createStyles({
transition: theme.transitions.create(['opacity', 'background-color'], {
duration: theme.transitions.duration.shortest
}),
backgroundColor: theme.palette.secondary.main,
backgroundColor: theme.palette.primary.main,
opacity: theme.palette.mode === 'light' ? 0.38 : 0.3
}
@@ -155,7 +155,7 @@ const ZoomedUiControl = withStyles(styles, { withTheme: true })(
if (control.isOnOffSwitch()) {
// normal gray unchecked state.
return (
<Switch checked={value !== 0} color="secondary"
<Switch checked={value !== 0} color="primary"
onChange={(event) => {
this.onCheckChanged(event.target.checked);
}}
@@ -165,14 +165,14 @@ const ZoomedUiControl = withStyles(styles, { withTheme: true })(
if (control.isAbToggle()) {
// unchecked color is not gray.
return (
<Switch checked={value !== 0} color="secondary"
<Switch checked={value !== 0} color="primary"
onChange={(event) => {
this.onCheckChanged(event.target.checked);
}}
classes={{
track: this.props.classes.switchTrack
}}
style={{ color: this.props.theme.palette.secondary.main }}
style={{ color: this.props.theme.palette.primary.main }}
/>
);
} else {
-45
View File
@@ -1,45 +0,0 @@
src/svg/
src/svg/fx_constant.svg
src/svg/fx_distortion.svg
src/svg/fx_oscillator.svg
src/svg/fx_filter.svg
src/svg/fx_function.svg
src/svg/fx_generator.svg
src/svg/fx_compressor.svg
src/svg/file_download_black_24dp.svg
src/svg/fx_filter_hp.svg
src/svg/fx_plugin.svg
src/svg/fx_error.svg
src/svg/fx_gate.svg
src/svg/ic_midi.svg
src/svg/ic_pluginpreset.svg
src/svg/fx_split_a.svg
src/svg/fx_utility.svg
src/svg/ic_pluginpreset2.svg
src/svg/fx_parametric_eq.svg
src/svg/fx_terminal.svg
src/svg/ic_presets.svg
src/svg/fx_flanger.svg
src/svg/fx_analyzer.svg
src/svg/fx_delay.svg
src/svg/fx_spectral.svg
src/svg/fx_chorus.svg
src/svg/fx_simulator.svg
src/svg/fx_mixer.svg
src/svg/fx_eq.svg
src/svg/fx_modulator.svg
src/svg/file_upload_black_24dp.svg
src/svg/fx_pitch.svg
src/svg/fx_phaser.svg
src/svg/fx_amplifier.svg
src/svg/fx_lr.svg
src/svg/fx_instrument.svg
src/svg/fx_reverb.svg
src/svg/fx_limiter.svg
src/svg/fx_empty.svg
src/svg/fx_dial.svg
src/svg/fx_flanger2.svg
src/svg/fx_split_b.svg
src/svg/fx_spatial.svg
src/svg/fx_converter.svg
src/svg/old_delete_outline_24dp.svg
+1
View File
@@ -132,6 +132,7 @@ else()
endif()
set (PIPEDAL_SOURCES
Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp
WebServerConfig.cpp WebServerConfig.hpp
Locale.hpp Locale.cpp
Finally.hpp
+2 -1
View File
@@ -18,6 +18,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include <iostream>
#include <utility>
#include <stdlib.h>
#include <unistd.h>
#include "CommandLineParser.hpp"
@@ -603,7 +604,7 @@ void SetVarPermissions(
using namespace std::filesystem;
try {
if (fs::exists(path)) {
chown(path.c_str(),uid,gid);
std::ignore = chown(path.c_str(),uid,gid);
if (fs::is_directory(path)) {
fs::permissions(path, directoryPermissions, fs::perm_options::replace);
for (const auto& entry : fs::recursive_directory_iterator(path)) {
+2 -1
View File
@@ -22,6 +22,7 @@
#include <sstream>
#include <cstdint>
#include <string>
#include <utility>
#include "json.hpp"
#include "json_variant.hpp"
@@ -46,7 +47,7 @@ TEST_CASE("inverting_mutext test", "[inverting_mutex_test]")
std::thread thread(
[&mutex]() mutable
{
nice(5);
std::ignore = nice(5);
{
std::lock_guard<inverting_mutex> lock(mutex);
SetThreadName("imTest");
+4 -6
View File
@@ -39,18 +39,16 @@ std::string timeTag()
using namespace std::chrono;
auto t = std::chrono::system_clock::now()- timeZero;
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
auto hours_ = duration_cast<hours>(t).count() % 24;
auto minutes_ = duration_cast<minutes>(t).count() % 60;
auto seconds_ = duration_cast<seconds>(t).count() % 60;
auto milliseconds_ = duration_cast<milliseconds>(t).count() % 1000;
std::stringstream s;
using namespace std;
s << setfill('0') << setw(2) << hours_ << ':' << setw(2) << minutes_ << ':' << setw(2) << seconds_ << "." << setw(3) << milliseconds_ << " ";
s << std::put_time(std::localtime(&now_c), "%Y-%m-%d %H:%M:%S") << "." << std::setw(3) << std::setfill('0') << milliseconds.count() << " ";
return s.str();
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright (c) 2024 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "Lv2PluginChangeMonitor.hpp"
#include "Lv2Log.hpp"
#include <sys/inotify.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <Finally.hpp>
#include <chrono>
#include <sys/eventfd.h>
#include "PiPedalModel.hpp"
using namespace pipedal;
Lv2PluginChangeMonitor::Lv2PluginChangeMonitor(PiPedalModel&model)
:model(model)
{
shutdown_eventfd = eventfd(0, 0);
monitorThread = std::make_unique<std::thread>([this]() { ThreadProc();});
}
void Lv2PluginChangeMonitor::Shutdown()
{
if (monitorThread)
{
terminateThread = true;
monitorThread->join();
monitorThread = nullptr;
}
}
Lv2PluginChangeMonitor::~Lv2PluginChangeMonitor()
{
Shutdown();
}
void Lv2PluginChangeMonitor::ThreadProc()
{
using clock = std::chrono::steady_clock;
int inotify_fd = inotify_init();
if (inotify_fd == -1) {
Lv2Log::error("Failed to initialize inotify");
return;
}
Finally f1 ([inotify_fd]() {
close(inotify_fd);
});
// Add the directory to the inotify watch list
int watch_descriptor = inotify_add_watch(inotify_fd, "/usr/lib/lv2", IN_MODIFY | IN_CREATE | IN_DELETE);
if (watch_descriptor == -1) {
Lv2Log::error("Failed to add directory to inotify watch list");
return;
}
Finally f2([inotify_fd,watch_descriptor]() {
inotify_rm_watch(inotify_fd, watch_descriptor);
});
bool updating = false;
clock::time_point updateTime;
// Monitor for file system events
while (true) {
struct pollfd pfds[2] = {
{.fd = inotify_fd, .events = POLLIN},
{.fd = shutdown_eventfd, .events = POLLIN}
};
int ret = poll(pfds, 2,500); // infinite wait
if (ret == -1) {
Lv2Log::error("Error in poll()");
break;
}
if (ret == 0)
{
// timeout.
if (updating && clock::now() >= updateTime)
{
updating = false;
model.OnLv2PluginsChanged();
}
continue;
}
if (pfds[1].revents & POLLIN) {
// Shutdown event received
break;
}
char buffer[4096];
ssize_t num_bytes = read(inotify_fd, buffer, sizeof(buffer));
if (num_bytes == -1) {
Lv2Log::error("Error reading from inotify");
break;
}
size_t i = 0;
bool updated = false;
while (i < static_cast<size_t>(num_bytes)) {
struct inotify_event* event = reinterpret_cast<struct inotify_event*>(&buffer[i]);
if (event->len > 0) {
if (event->mask & IN_MODIFY) {
updated = true;
} else if (event->mask & IN_CREATE) {
updated = true;
} else if (event->mask & IN_DELETE) {
updated = true;
}
}
i += sizeof(struct inotify_event) + event->len;
}
if (updated)
{
updating = true;
updateTime = clock::now() + std::chrono::duration_cast<clock::duration>(std::chrono::seconds(5));
}
}
// Clean up
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright (c) 2024 Robin Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#pragma once
#include <thread>
#include <atomic>
namespace pipedal
{
class PiPedalModel;
class Lv2PluginChangeMonitor {
public:
Lv2PluginChangeMonitor(PiPedalModel&model);
~Lv2PluginChangeMonitor();
void Shutdown();
private:
void ThreadProc();
int shutdown_eventfd;
bool isClosed = false;
PiPedalModel&model;
std::unique_ptr<std::thread> monitorThread;
std::atomic<bool> terminateThread {false};
};
}
+2 -1
View File
@@ -19,6 +19,7 @@
#include "MimeTypes.hpp"
#include "ss.hpp"
using namespace pipedal;
@@ -136,7 +137,7 @@ static std::string toLower(const std::string&value)
void MimeTypes::AddMimeType(const std::string&extension_, const std::string&mimeType)
{
std::string extension = std::string(".") + toLower(extension_);
std::string extension = SS("." << toLower(extension_));
mimeTypeToExtension[mimeType] = extension;
extensionToMimeType[extension] = mimeType;
if (mimeType.starts_with("audio/"))
+85 -52
View File
@@ -18,6 +18,7 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "pch.h"
#include <future>
#include "ServiceConfiguration.hpp"
#include "AudioConfig.hpp"
#include "ConfigUtil.hpp"
@@ -33,6 +34,7 @@
#include "RingBufferReader.hpp"
#include "PiPedalUI.hpp"
#include "atom_object.hpp"
#include "Lv2PluginChangeMonitor.hpp"
#ifndef NO_MLOCK
#include <sys/mman.h>
@@ -61,8 +63,8 @@ static std::string BytesToHex(const std::vector<uint8_t> &bytes)
}
PiPedalModel::PiPedalModel()
: lv2Host(),
atomConverter(lv2Host.GetMapFeature())
: pluginHost(),
atomConverter(pluginHost.GetMapFeature())
{
this->pedalboard = Pedalboard::MakeDefault();
#if JACK_HOST
@@ -98,6 +100,7 @@ void PiPedalModel::Close()
PiPedalModel::~PiPedalModel()
{
pluginChangeMonitor = nullptr;
try
{
adminClient.UnmonitorGovernor();
@@ -133,12 +136,14 @@ PiPedalModel::~PiPedalModel()
void PiPedalModel::Init(const PiPedalConfiguration &configuration)
{
std::lock_guard<std::recursive_mutex> lock(mutex); // prevent callbacks while we're initializing.
this->configuration = configuration;
lv2Host.SetConfiguration(configuration);
pluginHost.SetConfiguration(configuration);
storage.SetConfigRoot(configuration.GetDocRoot());
storage.SetDataRoot(configuration.GetLocalStoragePath());
storage.Initialize();
lv2Host.SetPluginStoragePath(storage.GetPluginUploadDirectory());
pluginHost.SetPluginStoragePath(storage.GetPluginUploadDirectory());
this->systemMidiBindings = storage.GetSystemMidiBindings();
@@ -157,7 +162,7 @@ void PiPedalModel::LoadLv2PluginInfo()
{
if (!std::filesystem::exists(pluginClassesPath))
throw PiPedalException("File not found.");
lv2Host.LoadPluginClassesFromJson(pluginClassesPath);
pluginHost.LoadPluginClassesFromJson(pluginClassesPath);
}
catch (const std::exception &e)
{
@@ -166,18 +171,19 @@ void PiPedalModel::LoadLv2PluginInfo()
throw PiPedalException(s.str().c_str());
}
lv2Host.Load(configuration.GetLv2Path().c_str());
pluginChangeMonitor = std::make_unique<Lv2PluginChangeMonitor>(*this);
pluginHost.Load(configuration.GetLv2Path().c_str());
// Copy all presets out of Lilv data to json files
// so that we can close lilv while we're actually
// running.
for (const auto &plugin : lv2Host.GetPlugins())
for (const auto &plugin : pluginHost.GetPlugins())
{
if (plugin->has_factory_presets())
{
if (!storage.HasPluginPresets(plugin->uri()))
{
PluginPresets pluginPresets = lv2Host.GetFactoryPluginPresets(plugin->uri());
PluginPresets pluginPresets = pluginHost.GetFactoryPluginPresets(plugin->uri());
storage.SavePluginPresets(plugin->uri(), pluginPresets);
}
}
@@ -191,7 +197,7 @@ void PiPedalModel::Load()
adminClient.MonitorGovernor(storage.GetGovernorSettings());
// lv2Host.Load(configuration.GetLv2Path().c_str());
// pluginHost.Load(configuration.GetLv2Path().c_str());
this->pedalboard = storage.GetCurrentPreset(); // the current *saved* preset.
@@ -211,7 +217,7 @@ void PiPedalModel::Load()
}
UpdateDefaults(&this->pedalboard);
std::unique_ptr<AudioHost> p{AudioHost::CreateInstance(lv2Host.asIHost())};
std::unique_ptr<AudioHost> p{AudioHost::CreateInstance(pluginHost.asIHost())};
this->audioHost = std::move(p);
this->audioHost->SetNotificationCallbacks(this);
@@ -270,7 +276,7 @@ void PiPedalModel::Load()
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
selection = selection.RemoveInvalidChannels(jackConfiguration);
this->lv2Host.OnConfigurationChanged(jackConfiguration, selection);
this->pluginHost.OnConfigurationChanged(jackConfiguration, selection);
try
{
audioHost->Open(this->jackServerSettings, selection);
@@ -380,7 +386,6 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
item->stateUpdateCount(item->stateUpdateCount() + 1);
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()];
Lv2PluginState newState = item->lv2State();
@@ -392,7 +397,7 @@ void PiPedalModel::OnNotifyMaybeLv2StateChanged(uint64_t instanceId)
{
for (size_t i = 0; i < n; ++i)
{
t[i]->OnLv2StateChanged(instanceId,newState);
t[i]->OnLv2StateChanged(instanceId, newState);
}
}
delete[] t;
@@ -1151,7 +1156,6 @@ void PiPedalModel::RestartAudio()
// do a complete reload.
this->audioHost->SetPedalboard(nullptr);
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
@@ -1183,7 +1187,7 @@ void PiPedalModel::RestartAudio()
}
this->audioHost->Open(this->jackServerSettings, channelSelection);
this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection);
this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection);
std::vector<std::string> errorMessages;
@@ -1205,7 +1209,7 @@ void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSe
std::lock_guard<std::recursive_mutex> lock(mutex); // copy atomically.
this->storage.SetJackChannelSelection(channelSelection);
this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection);
this->pluginHost.OnConfigurationChanged(jackConfiguration, channelSelection);
}
RestartAudio(); // no lock to avoid mutex deadlock when reader thread is sending notifications..
@@ -1283,7 +1287,7 @@ void PiPedalModel::OnNotifyMidiValueChanged(int64_t instanceId, int portIndex, f
}
else
{
pPluginInfo = lv2Host.GetPluginInfo(item->uri());
pPluginInfo = pluginHost.GetPluginInfo(item->uri());
}
if (pPluginInfo)
{
@@ -1475,7 +1479,7 @@ void PiPedalModel::SendSetPatchProperty(
}
}};
LV2_URID urid = this->lv2Host.GetLv2Urid(propertyUri.c_str());
LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str());
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
@@ -1536,7 +1540,7 @@ void PiPedalModel::SendGetPatchProperty(
}
}};
LV2_URID urid = this->lv2Host.GetLv2Urid(uri.c_str());
LV2_URID urid = this->pluginHost.GetLv2Urid(uri.c_str());
RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest(
onRequestComplete,
clientId, instanceId, urid, onSuccess, onError);
@@ -1669,7 +1673,7 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet
FireJackConfigurationChanged(this->jackConfiguration);
// restart the pedalboard on a new instance.
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{this->lv2Host.CreateLv2Pedalboard(this->pedalboard)};
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{this->pluginHost.CreateLv2Pedalboard(this->pedalboard)};
this->lv2Pedalboard = lv2Pedalboard;
audioHost->SetPedalboard(lv2Pedalboard);
@@ -1684,7 +1688,7 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet
void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem)
{
std::shared_ptr<Lv2PluginInfo> pPlugin = lv2Host.GetPluginInfo(pedalboardItem->uri());
std::shared_ptr<Lv2PluginInfo> pPlugin = pluginHost.GetPluginInfo(pedalboardItem->uri());
if (!pPlugin)
{
if (pedalboardItem->uri() == SPLIT_PEDALBOARD_ITEM_URI)
@@ -1817,19 +1821,19 @@ void PiPedalModel::DeleteMidiListeners(int64_t clientId)
audioHost->SetListenForMidiEvent(midiEventListeners.size() != 0);
}
void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom*atomValue)
void PiPedalModel::OnPatchSetReply(uint64_t instanceId, LV2_URID patchSetProperty, const LV2_Atom *atomValue)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
std::string propertyUri = lv2Host.GetMapFeature().UridToString(patchSetProperty);
std::string propertyUri = pluginHost.GetMapFeature().UridToString(patchSetProperty);
{
PedalboardItem *item = pedalboard.GetItem((int64_t)instanceId);
if (item == nullptr) return;
atom_object atomObject { atomValue };
PedalboardItem::PropertyMap& properties = item->PatchProperties();
if (item == nullptr)
return;
atom_object atomObject{atomValue};
PedalboardItem::PropertyMap &properties = item->PatchProperties();
if (properties.contains(propertyUri))
{
if (properties[propertyUri] == atomObject)
@@ -1924,25 +1928,25 @@ void PiPedalModel::MonitorPatchProperty(int64_t clientId, int64_t clientHandle,
LV2_URID propertyUrid = 0;
if (propertyUri.length() != 0)
{
propertyUrid = lv2Host.GetMapFeature().GetUrid(propertyUri.c_str());
propertyUrid = pluginHost.GetMapFeature().GetUrid(propertyUri.c_str());
}
AtomOutputListener listener{clientId, clientHandle, instanceId, propertyUrid};
atomOutputListeners.push_back(listener);
audioHost->SetListenForAtomOutput(true);
PedalboardItem*item = this->pedalboard.GetItem(instanceId );
PedalboardItem *item = this->pedalboard.GetItem(instanceId);
if (item)
{
auto& map = item->PatchProperties();
auto &map = item->PatchProperties();
if (map.contains(propertyUri))
{
const auto&value = map[propertyUri];
const auto &value = map[propertyUri];
std::string json = this->audioHost->AtomToJson(value.get());
for (auto &subscriber: this->subscribers)
for (auto &subscriber : this->subscribers)
{
if (subscriber->GetClientId() == clientId)
{
subscriber->OnNotifyPatchProperty(clientHandle,instanceId,propertyUri,json);
subscriber->OnNotifyPatchProperty(clientHandle, instanceId, propertyUri, json);
}
}
}
@@ -2040,11 +2044,11 @@ std::vector<std::string> PiPedalModel::GetFileList(const UiFileProperty &filePro
return std::vector<std::string>(); // don't disclose to users what the problem is.
}
}
std::vector<FileEntry> PiPedalModel::GetFileList2(const std::string &relativePath,const UiFileProperty &fileProperty)
std::vector<FileEntry> PiPedalModel::GetFileList2(const std::string &relativePath, const UiFileProperty &fileProperty)
{
try
{
return this->storage.GetFileList2(relativePath,fileProperty);
return this->storage.GetFileList2(relativePath, fileProperty);
}
catch (const std::exception &e)
{
@@ -2054,12 +2058,12 @@ std::vector<FileEntry> PiPedalModel::GetFileList2(const std::string &relativePat
}
std::string PiPedalModel::RenameFilePropertyFile(
const std::string&oldRelativePath,
const std::string&newRelativePath,
const UiFileProperty&uiFileProperty)
const std::string &oldRelativePath,
const std::string &newRelativePath,
const UiFileProperty &uiFileProperty)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.RenameFilePropertyFile(oldRelativePath,newRelativePath,uiFileProperty);
return storage.RenameFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty);
}
void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
@@ -2068,23 +2072,20 @@ void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName)
storage.DeleteSampleFile(fileName);
}
std::string PiPedalModel::CreateNewSampleDirectory(const std::string&relativePath, const UiFileProperty&uiFileProperty)
std::string PiPedalModel::CreateNewSampleDirectory(const std::string &relativePath, const UiFileProperty &uiFileProperty)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.CreateNewSampleDirectory(relativePath, uiFileProperty);
}
FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty)
FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty)
{
std::lock_guard<std::recursive_mutex> lock(mutex);
return storage.GetFilePropertydirectoryTree(uiFileProperty);
}
std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream&stream, size_t contentLength)
std::string PiPedalModel::UploadUserFile(const std::string &directory, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength)
{
return storage.UploadUserFile(directory, patchProperty, filename, stream,contentLength);
return storage.UploadUserFile(directory, patchProperty, filename, stream, contentLength);
}
uint64_t PiPedalModel::CreateNewPreset()
@@ -2096,19 +2097,18 @@ uint64_t PiPedalModel::CreateNewPreset()
void PiPedalModel::CheckForResourceInitialization(Pedalboard &pedalboard)
{
for (auto item: pedalboard.GetAllPlugins())
for (auto item : pedalboard.GetAllPlugins())
{
if (!item->isSplit())
{
lv2Host.CheckForResourceInitialization(item->uri(),storage.GetPluginUploadDirectory());
pluginHost.CheckForResourceInitialization(item->uri(), storage.GetPluginUploadDirectory());
}
}
}
bool PiPedalModel::LoadCurrentPedalboard()
{
Lv2PedalboardErrorList errorMessages;
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{this->lv2Host.CreateLv2Pedalboard(this->pedalboard, errorMessages)};
std::shared_ptr<Lv2Pedalboard> lv2Pedalboard{this->pluginHost.CreateLv2Pedalboard(this->pedalboard, errorMessages)};
this->lv2Pedalboard = lv2Pedalboard;
// apply the error messages to the lv2Pedalboard.
@@ -2138,4 +2138,37 @@ void PiPedalModel::OnNotifyLv2RealtimeError(int64_t instanceId, const std::strin
std::filesystem::path PiPedalModel::GetPluginUploadDirectory() const
{
return storage.GetPluginUploadDirectory();
}
}
void PiPedalModel::OnLv2PluginsChanged()
{
Lv2Log::info("Lv2 plugins have changed. Reloading plugins.");
std::lock_guard<std::recursive_mutex> lock(mutex);
{
// Notify clients.
size_t n = subscribers.size();
IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[n];
for (size_t i = 0; i < n; ++i)
{
t[i] = this->subscribers[i];
}
for (size_t i = 0; i < n; ++i)
{
t[i]->OnLv2PluginsChanging();
}
delete[] t;
}
std::thread(
[this]()
{
// wait for the message to propagate. It would be better to use some kind of flush()
// operation, but it's not clear how to do that with asyncio.
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
restartListener();
})
.detach();
}
void PiPedalModel::SetRestartListener(std::function<void(void)> &&listener)
{
this->restartListener = std::move(listener);
}
+10 -4
View File
@@ -46,6 +46,7 @@ namespace pipedal
struct RealtimeMidiProgramRequest;
struct RealtimeNextMidiProgramRequest;
class Lv2PluginChangeMonitor;
class IPiPedalModelSubscriber
{
@@ -78,12 +79,16 @@ namespace pipedal
virtual void OnSystemMidiBindingsChanged(const std::vector<MidiBinding>&bindings) = 0;
//virtual void OnPatchPropertyChanged(int64_t clientId, int64_t instanceId,const std::string& propertyUri,const json_variant& value) = 0;
virtual void OnErrorMessage(const std::string&message) = 0;
virtual void OnLv2PluginsChanging() = 0;
virtual void Close() = 0;
};
class PiPedalModel : private IAudioHostCallbacks
{
private:
std::function<void(void)> restartListener;
std::unique_ptr<Lv2PluginChangeMonitor> pluginChangeMonitor;
std::unique_ptr<std::jthread> pingThread;
@@ -123,8 +128,8 @@ namespace pipedal
std::vector<AtomOutputListener> atomOutputListeners;
JackServerSettings jackServerSettings;
PluginHost lv2Host;
AtomConverter atomConverter; // must be AFTER lv2Host!
PluginHost pluginHost;
AtomConverter atomConverter; // must be AFTER pluginHost!
Pedalboard pedalboard;
Storage storage;
@@ -146,7 +151,6 @@ namespace pipedal
void UpdateDefaults(PedalboardItem *pedalboardItem);
void UpdateDefaults(Pedalboard *pedalboard);
class VuSubscription
{
public:
@@ -199,6 +203,8 @@ namespace pipedal
std::filesystem::path GetPluginUploadDirectory() const;
void Close();
void SetRestartListener(std::function<void(void)> &&listener);
void OnLv2PluginsChanged();
void SetOnboarding(bool value);
void UpdateDnsSd();
@@ -210,7 +216,7 @@ namespace pipedal
void LoadLv2PluginInfo();
void Load();
const PluginHost &GetLv2Host() const { return lv2Host; }
const PluginHost &GetLv2Host() const { return pluginHost; }
Pedalboard GetCurrentPedalboardCopy()
{
std::lock_guard<std::recursive_mutex> guard(mutex);
+9
View File
@@ -1587,6 +1587,12 @@ private:
Send("onLv2StateChanged",message);
}
virtual void OnLv2PluginsChanging() override {
Send("onLv2PluginsChanging",true);
Flush();
}
virtual void OnErrorMessage(const std::string&message)
{
Send("onErrorMessage",message);
@@ -1778,6 +1784,9 @@ private:
}
}
void Flush() {
}
int outstandingNotifyAtomOutputs = 0;
class PendingNotifyAtomOutput
+1
View File
@@ -1470,6 +1470,7 @@ void PluginHost::CheckForResourceInitialization(const std::string &pluginUri,con
}
}
}
// void PiPedalHostLogError(const std::string &error)
// {
// Lv2Log::error("%s",error.c_str());
+1
View File
@@ -813,6 +813,7 @@ namespace pipedal
public:
virtual MapFeature &GetMapFeature() { return this->mapFeature; }
void CheckForResourceInitialization(const std::string& pluginUri,const std::filesystem::path& pluginUploadDirectory);
void ReloadPlugins();
private:
+3 -2
View File
@@ -39,7 +39,8 @@
#include <lv2/lv2plug.in/ns/ext/worker/worker.h>
#include "Lv2Log.hpp"
#include <iostream>
#include <unistd.h> // for nice()
#include <unistd.h> // for nice(
#include <utility>
#include "util.hpp"
using namespace pipedal;
@@ -190,7 +191,7 @@ void HostWorkerThread::ThreadProc() noexcept
// run nice +2 (priority -2 on Windows)
SetThreadName("lv2_worker");
errno = 0;
(void)nice(2);
std::ignore = nice(2);
if (errno != 0)
{
std::cout << "Warning: Unable to run Lv2 schedule thread at nice +1" << std::endl;
+33 -48
View File
@@ -47,19 +47,13 @@
#include <systemd/sd-daemon.h>
using namespace pipedal;
#ifdef __ARM_ARCH_ISA_A64
#define AARCH64
#endif
sem_t signalSemaphore;
bool HasAlsaDevice(const std::vector<AlsaDeviceInfo> devices, const std::string &deviceId)
{
for (auto &device : devices)
@@ -77,21 +71,16 @@ public:
std::string message(int ev) const { return "error message"; }
};
static volatile bool g_SigBreak = false;
std::atomic<bool> ga_SigBreak { false };
static std::atomic<bool> g_SigBreak = false;
static std::atomic<bool> g_restart = false;
void sig_handler(int signo)
{
if (!g_SigBreak)
{
g_SigBreak = true;
sem_post(&signalSemaphore);
}
// we're using sig_wait. No need to do anything.
}
void throwSystemError(int error)
{
}
static bool isJackServiceRunning()
@@ -104,8 +93,6 @@ static bool isJackServiceRunning()
int main(int argc, char *argv[])
{
sem_init(&signalSemaphore, 0, 0);
#ifndef WIN32
umask(002); // newly created files in /var/pipedal get 775-ish permissions, which improves debugging/live-service interaction.
@@ -176,7 +163,6 @@ int main(int argc, char *argv[])
}
SetThreadName("main");
std::filesystem::path doc_root = parser.Arguments()[0];
std::filesystem::path web_root = doc_root;
if (parser.Arguments().size() >= 2)
@@ -194,7 +180,7 @@ int main(int argc, char *argv[])
std::stringstream s;
s << "Unable to read configuration from '" << (doc_root / "config.json") << "'. (" << e.what() << ")";
Lv2Log::error(s.str());
return EXIT_FAILURE;
return EXIT_SUCCESS; // indicate to systemd that we don't want a restart.
}
Lv2Log::log_level(configuration.GetLogLevel());
@@ -214,7 +200,7 @@ int main(int argc, char *argv[])
auto const threads = std::max<int>(1, configuration.GetThreads());
server = WebServer::create(
address, port, web_root.c_str(), threads,configuration.GetMaxUploadSize());
address, port, web_root.c_str(), threads, configuration.GetMaxUploadSize());
Lv2Log::info("Document root: %s Threads: %d", doc_root.c_str(), (int)threads);
@@ -225,7 +211,7 @@ int main(int argc, char *argv[])
std::stringstream s;
s << "Fatal error: " << e.what() << std::endl;
Lv2Log::error(s.str());
return EXIT_FAILURE;
return EXIT_SUCCESS; // indiate to systemd that we don't want a restart.
}
try
@@ -233,15 +219,25 @@ int main(int argc, char *argv[])
{
auto locale = Locale::GetInstance();
Lv2Log::info(SS("Locale: " << locale->CurrentLocale()));
try {
try
{
auto collator = locale->GetCollator();
} catch (std::exception&e)
}
catch (std::exception &e)
{
Lv2Log::error(e.what());
return EXIT_SUCCESS; //tell systemd not to auto-restart.
return EXIT_SUCCESS; // tell systemd not to auto-restart.
}
}
PiPedalModel model;
model.SetRestartListener(
[]()
{
g_restart = true;
raise(SIGTERM); // throws an exception under gdb, but correctly restarts the service when running live.
});
model.Init(configuration);
// Get heavy IO out of the way before letting dependent (Jack/ALSA) services run.
@@ -254,10 +250,8 @@ int main(int argc, char *argv[])
(unsigned long)getpid());
}
auto serverSettings = model.GetJackServerSettings();
{
// Wait for selected audio device to be initialized.
// It may take some time for ALSA to publish all available devices when rebooting.
@@ -270,7 +264,8 @@ int main(int argc, char *argv[])
if (HasAlsaDevice(devices, serverSettings.GetAlsaInputDevice()))
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
} else
}
else
{
for (int i = 0; i < 5; ++i)
{
@@ -292,13 +287,16 @@ int main(int argc, char *argv[])
if (found)
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaInputDevice() << "."));
} else {
}
else
{
Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaInputDevice() << " not found."));
}
}
} else {
}
else
{
Lv2Log::info("No ALSA device selected.");
}
// pre-cache device info before we let audio services run.
@@ -360,7 +358,7 @@ int main(int argc, char *argv[])
server->AddSocketFactory(pipedalSocketFactory);
ConfigureWebServer(*server,model,port,configuration.GetMaxUploadSize());
ConfigureWebServer(*server, model, port, configuration.GetMaxUploadSize());
{
server->RunInBackground(-1);
@@ -368,24 +366,9 @@ int main(int argc, char *argv[])
model.UpdateDnsSd(); // now that the server is running, publish a DNS-SD announcement.
SetThreadName("main");
// AARCH64 sem_wait pins CPU 100%.
// static_assert(std::atomic<bool>::is_always_lock_free);
// while (true)
// {
// auto sigBreak = ga_SigBreak.load();
// if (sigBreak)
// {
// break;
// }
// std::this_thread::sleep_for(std::chrono::milliseconds(50));
// }
{
sigwait(&sigSet,&sig);
sigwait(&sigSet, &sig);
if (systemd)
{
sd_notify(0, "STOPPING=1");
@@ -395,12 +378,14 @@ int main(int argc, char *argv[])
Lv2Log::info("Closing audio session.");
model.Close();
Lv2Log::info("Stopping web server.");
server->ShutDown(5000);
server->Join();
Lv2Log::info("Shutdown complete.");
if (g_restart) return EXIT_FAILURE; // indicate to systemd that we want a restart.
}
catch (const std::exception &e)
{
+1 -1
View File
@@ -16,7 +16,7 @@ User=pipedal_d
Group=pipedal_d
Restart=always
TimeoutStartSec=60
RestartSec=25
RestartSec=5
TimeoutStopSec=15
WorkingDirectory=/var/pipedal
+4 -1
View File
@@ -1,5 +1,8 @@
- change icons on gallery in index.html
- provide direct link to docs on both landing pages.
- feature: redo ./makepackage so that it runs without sudo.
- Bug: text Cursor hovering over the breadcrumb bar in file property dialog.
- Turning off wifi-direct sould re-enable NetworkManager.
- Bug: Turning off wifi-direct sould re-enable NetworkManager.
- ToobML save/restore state.