0.9.11-rc-2

Switch from JACK to ALSA audio.
This commit is contained in:
Robin Davies
2022-07-01 21:15:57 -04:00
parent 0995e0fb16
commit 0a67b39103
51 changed files with 4301 additions and 632 deletions
+39 -1
View File
@@ -5,6 +5,7 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "(gdb) generic", "name": "(gdb) generic",
"type": "cppdbg", "type": "cppdbg",
@@ -77,7 +78,9 @@
"request": "launch", "request": "launch",
// Resolved by CMake Tools: // Resolved by CMake Tools:
"program": "${command:cmake.launchTargetPath}", "program": "${command:cmake.launchTargetPath}",
"args": [ "[avahi_service]" ],
"args": [ "[alsa_midi_test]" ],
"stopAtEntry": false, "stopAtEntry": false,
"cwd": "${workspaceFolder}", "cwd": "${workspaceFolder}",
"environment": [ "environment": [
@@ -102,6 +105,41 @@
} }
] ]
}, },
{
"name": "(gdb) pipedal_latency_test Launch",
"type": "cppdbg",
"request": "launch",
// Resolved by CMake Tools:
"program": "${command:cmake.launchTargetPath}",
"args": [ "--rate", "48000", "hw:M2" ],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [
{
// add the directory where our target was built to the PATHs
// it gets resolved by CMake Tools:
"name": "PATH",
"value": "$PATH:${command:cmake.launchTargetDirectory}"
},
{
"name": "OTHER_VALUE",
"value": "Something something"
}
],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
},
{ {
"name": "(gdb) pipedal Launch", "name": "(gdb) pipedal Launch",
"type": "cppdbg", "type": "cppdbg",
+5 -3
View File
@@ -1,10 +1,11 @@
cmake_minimum_required(VERSION 3.16.0) cmake_minimum_required(VERSION 3.16.0)
project(pipedal project(pipedal
VERSION 0.9.10 VERSION 0.9.11
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi" DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
HOMEPAGE_URL "https://rerdavies.github.io/pipedal" HOMEPAGE_URL "https://rerdavies.github.io/pipedal"
) )
set (DISPLAY_VERSION "v0.9.10-rc-1")
set (DISPLAY_VERSION "v0.9.11-rc-2")
set (CMAKE_INSTALL_PREFIX "/usr/") set (CMAKE_INSTALL_PREFIX "/usr/")
@@ -85,7 +86,8 @@ set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "IoT guitar effect for Raspberry Pi")
set(CPACK_DEBIAN_PACKAGE_SECTION sound) set(CPACK_DEBIAN_PACKAGE_SECTION sound)
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON) set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE) set(CPACK_DEBIAN_PACKAGE_CONTROL_STRICT_PERMISSION TRUE)
set(CPACK_DEBIAN_PACKAGE_DEPENDS "jackd2, hostapd, dnsmasq" ) #set(CPACK_DEBIAN_PACKAGE_DEPENDS "jackd2, hostapd, dnsmasq" )
set(CPACK_DEBIAN_PACKAGE_DEPENDS "hostapd, dnsmasq" )
#set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64") #set(CPACK_DEBIAN_PACKAGE_ARCHITECTURE "arm64")
set(CPACK_PACKAGING_INSTALL_PREFIX /usr) set(CPACK_PACKAGING_INSTALL_PREFIX /usr)
set(CPACK_PROJECT_NAME ${PROJECT_NAME}) set(CPACK_PROJECT_NAME ${PROJECT_NAME})
+1 -1
View File
@@ -1,7 +1,7 @@
<img src='docs/GithubBanner.png' width="100%" /> <img src='docs/GithubBanner.png' width="100%" />
Download: <a href='docs/download.md'>v0.9.10-rc-1</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). Download: <a href='docs/download.md'>v0.9.11-rc-2</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet. Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet.
+2 -2
View File
@@ -4,14 +4,14 @@
Download the most recent Debian (.deb) package for your platform: Download the most recent Debian (.deb) package for your platform:
- [Raspberry Pi OS (64-bit) v0.9.10-rc-1](https://github.com/rerdavies/pipedal/releases/download/v0.9.10-rc-1/pipedal_0.9.10_arm64.deb) - [Raspberry Pi OS (64-bit) v0.9.11-rc-2](https://github.com/rerdavies/pipedal/releases/download/v0.9.11-rc-2/pipedal_0.9.11_arm64.deb)
Install the package by running Install the package by running
``` ```
sudo apt update sudo apt update
cd ~/Downloads cd ~/Downloads
sudo apt-get install ./pipedal_0.9.10_arm64.deb sudo apt-get install ./pipedal_0.9.11_arm64.deb
``` ```
After installing, follow the instructions in [Configuring PiPedal after Installation](Configuring.md). After installing, follow the instructions in [Configuring PiPedal after Installation](Configuring.md).
+2 -2
View File
@@ -4,14 +4,14 @@
Download the most recent Debian (.deb) package for your platform: Download the most recent Debian (.deb) package for your platform:
- [Ubuntu or Raspberry Pi OS (64-bit)](https://github.com/rerdavies/pipedal/releases/download/v0.9.10-rc-1/pipedal_0.9.10_arm64.deb) v0.9.10-rc-1 - [Ubuntu or Raspberry Pi OS (64-bit)](https://github.com/rerdavies/pipedal/releases/download/v0.9.11-rc-2/pipedal_0.9.11_arm64.deb) v0.9.11-rc-2
Install the package by running Install the package by running
``` ```
sudo apt update sudo apt update
cd ~/Downloads cd ~/Downloads
sudo apt-get install ./pipedal_0.9.10_arm64.deb sudo apt-get install ./pipedal_0.9.11_arm64.deb
``` ```
Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation. Follow the instructions in [_Configuring PiPedal After Installation_](https://rerdavies.github.io/pipedal/Configuring.html) to complete the installation.
+1 -1
View File
@@ -1,7 +1,7 @@
<img src="GithubBanner.png" width="100%"/> <img src="GithubBanner.png" width="100%"/>
<a href="Installing.html"><i>v0.9.10-rc-1</i></a> <a href="Installing.html"><i>v0.9.11-rc-2</i></a>
Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet. Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet.
+1
View File
@@ -21,6 +21,7 @@ add_custom_command(
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react
DEPENDS DEPENDS
src/AlsaMidiDeviceInfo.tsx
src/SearchControl.tsx src/SearchControl.tsx
src/SplitControlView.tsx src/SplitControlView.tsx
src/WifiConfigSettings.tsx src/WifiConfigSettings.tsx
+45
View File
@@ -0,0 +1,45 @@
/*
* MIT License
*
* Copyright (c) 2022 Robin E. R. Davies
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
* of the Software, and to permit persons to whom the Software is furnished to do
* so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export class AlsaMidiDeviceInfo {
deserialize(input: any) : AlsaMidiDeviceInfo{
this.name = input.name;
this.description = input.description;
return this;
}
static deserializeArray(input: any[]): AlsaMidiDeviceInfo[] {
let result: AlsaMidiDeviceInfo[] = [];
for (let i = 0; i < input.length; ++i) {
result[i] = new AlsaMidiDeviceInfo().deserialize(input[i]);
}
return result;
}
name: string = "";
description: string = "";
equals(other: AlsaMidiDeviceInfo) : boolean {
return this.name === other.name && this.description === other.description;
}
}
+7 -6
View File
@@ -18,13 +18,14 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import {PiPedalArgumentError} from './PiPedalError'; import {PiPedalArgumentError} from './PiPedalError';
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
export class JackChannelSelection { export class JackChannelSelection {
deserialize(input: any): JackChannelSelection deserialize(input: any): JackChannelSelection
{ {
this.inputAudioPorts = input.inputAudioPorts.slice(); this.inputAudioPorts = input.inputAudioPorts.slice();
this.outputAudioPorts = input.outputAudioPorts.slice(); this.outputAudioPorts = input.outputAudioPorts.slice();
this.inputMidiPorts = input.inputMidiPorts.slice(); this.inputMidiDevices = AlsaMidiDeviceInfo.deserializeArray(input.inputMidiDevices);
this.sampleRate = input.sampleRate; this.sampleRate = input.sampleRate;
this.bufferSize = input.bufferSize; this.bufferSize = input.bufferSize;
this.numberOfBuffers = input.numberOfBuffers; this.numberOfBuffers = input.numberOfBuffers;
@@ -35,7 +36,7 @@ export class JackChannelSelection {
} }
inputAudioPorts: string[] = []; inputAudioPorts: string[] = [];
outputAudioPorts: string[] = []; outputAudioPorts: string[] = [];
inputMidiPorts: string[] = []; inputMidiDevices: AlsaMidiDeviceInfo[] = [];
sampleRate: number = 48000; sampleRate: number = 48000;
bufferSize: number = 64; bufferSize: number = 64;
@@ -45,6 +46,7 @@ export class JackChannelSelection {
let result = new JackChannelSelection(); let result = new JackChannelSelection();
result.inputAudioPorts = jackConfiguration.inputAudioPorts.slice(0,2); result.inputAudioPorts = jackConfiguration.inputAudioPorts.slice(0,2);
result.outputAudioPorts = jackConfiguration.inputAudioPorts.slice(0,2); result.outputAudioPorts = jackConfiguration.inputAudioPorts.slice(0,2);
result.inputMidiDevices = [];
return result; return result;
} }
getChannelDisplayValue(selectedChannels: string[], availableChannels: string[],isConfigValid: boolean): string getChannelDisplayValue(selectedChannels: string[], availableChannels: string[],isConfigValid: boolean): string
@@ -97,8 +99,7 @@ export class JackConfiguration {
this.maxAllowedMidiDelta = input.maxAllowedMidiDelta; this.maxAllowedMidiDelta = input.maxAllowedMidiDelta;
this.inputAudioPorts = input.inputAudioPorts; this.inputAudioPorts = input.inputAudioPorts;
this.outputAudioPorts = input.outputAudioPorts; this.outputAudioPorts = input.outputAudioPorts;
this.inputMidiPorts = input.inputMidiPorts; this.inputMidiDevices = AlsaMidiDeviceInfo.deserializeArray(input.inputMidiDevices);
this.outputMidiPorts = input.outputMidiPorts;
return this; return this;
} }
isValid: boolean = false; isValid: boolean = false;
@@ -111,8 +112,8 @@ export class JackConfiguration {
inputAudioPorts: string[] = []; inputAudioPorts: string[] = [];
outputAudioPorts: string[] = []; outputAudioPorts: string[] = [];
inputMidiPorts: string[] = []; inputMidiDevices: AlsaMidiDeviceInfo[] = [];
outputMidiPorts: string[] = [];
}; };
+3 -1
View File
@@ -58,6 +58,7 @@ export default class JackHostStatus {
deserialize(input: any): JackHostStatus { deserialize(input: any): JackHostStatus {
this.active = input.active; this.active = input.active;
this.restarting = input.restarting; this.restarting = input.restarting;
this.errorMessage = input.errorMessage;
this.underruns = input.underruns; this.underruns = input.underruns;
this.cpuUsage = input.cpuUsage; this.cpuUsage = input.cpuUsage;
this.msSinceLastUnderrun = input.msSinceLastUnderrun; this.msSinceLastUnderrun = input.msSinceLastUnderrun;
@@ -71,6 +72,7 @@ export default class JackHostStatus {
return this.temperaturemC >= -100000; return this.temperaturemC >= -100000;
} }
active: boolean = false; active: boolean = false;
errorMessage: string = "";
restarting: boolean = false; restarting: boolean = false;
underruns: number = 0; underruns: number = 0;
cpuUsage: number = 0; cpuUsage: number = 0;
@@ -136,7 +138,7 @@ export default class JackHostStatus {
<Typography variant="caption" color="textSecondary">{label}</Typography> <Typography variant="caption" color="textSecondary">{label}</Typography>
<span style={{ color: RED_COLOR }}> <span style={{ color: RED_COLOR }}>
<Typography variant="caption" color="inherit">Stopped&nbsp;&nbsp;</Typography> <Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Stopped" : status.errorMessage}&nbsp;&nbsp;</Typography>
</span> </span>
{ {
status.temperaturemC > -100000 && status.temperaturemC > -100000 &&
+1 -1
View File
@@ -135,7 +135,7 @@ const JackServerSettingsDialog = withStyles(styles)(
} }
if (result.sampleRate === 0) result.sampleRate = 48000; if (result.sampleRate === 0) result.sampleRate = 48000;
if (result.bufferSize === 0) result.bufferSize = 64; if (result.bufferSize === 0) result.bufferSize = 64;
if (result.numberOfBuffers === 0) result.numberOfBuffers = 3; if (result.numberOfBuffers === 0) result.numberOfBuffers = 2;
result.sampleRate = selectedDevice.closestSampleRate(result.sampleRate); result.sampleRate = selectedDevice.closestSampleRate(result.sampleRate);
result.bufferSize = selectedDevice.closestBufferSize(result.bufferSize); result.bufferSize = selectedDevice.closestBufferSize(result.bufferSize);
result.valid = true; result.valid = true;
+1 -1
View File
@@ -96,6 +96,7 @@ export const MidiBindingDialog =
hasHooks: boolean = false; hasHooks: boolean = false;
handleClose() { handleClose() {
this.cancelListenForControl();
this.props.onClose(); this.props.onClose();
} }
@@ -120,7 +121,6 @@ export const MidiBindingDialog =
handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number) handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number)
{ {
this.listenHandle = undefined; // (one-shot event)
this.cancelListenForControl(); this.cancelListenForControl();
let pedalBoard = this.model.pedalBoard.get(); let pedalBoard = this.model.pedalBoard.get();
+6
View File
@@ -1826,12 +1826,18 @@ class PiPedalModelImpl implements PiPedalModel {
} }
} }
cancelListenForMidiEvent(listenHandle: ListenHandle): void { cancelListenForMidiEvent(listenHandle: ListenHandle): void {
let found = false;
for (let i = 0; i < this.midiListeners.length; ++i) { for (let i = 0; i < this.midiListeners.length; ++i) {
if (this.midiListeners[i].handle === listenHandle._handle) { if (this.midiListeners[i].handle === listenHandle._handle) {
this.midiListeners.splice(i, 1); this.midiListeners.splice(i, 1);
found = true;
break; break;
} }
} }
if (!found)
{
console.log('cancelListenForMidiEvent: event not found.');
}
this.webSocket?.send("cancelListenForMidiEvent", listenHandle._handle); this.webSocket?.send("cancelListenForMidiEvent", listenHandle._handle);
} }
cancelListenForAtomOutput(listenHandle: ListenHandle): void { cancelListenForAtomOutput(listenHandle: ListenHandle): void {
+20 -15
View File
@@ -27,38 +27,43 @@ import Dialog from '@mui/material/Dialog';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
export interface SelectMidiChannelsDialogProps { export interface SelectMidiChannelsDialogProps {
open: boolean; open: boolean;
selectedChannels: string[]; selectedChannels: AlsaMidiDeviceInfo[];
availableChannels: string[]; availableChannels: AlsaMidiDeviceInfo[];
onClose: (selectedChannels: string[] | null) => void; onClose: (selectedChannels: AlsaMidiDeviceInfo[] | null) => void;
} }
function isChecked(selectedChannels: string[], channel: string): boolean { function isChecked(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo): boolean {
for (let i = 0; i < selectedChannels.length; ++i) { for (let i = 0; i < selectedChannels.length; ++i) {
if (selectedChannels[i] === channel) return true; if (selectedChannels[i].equals(channel)) return true;
} }
return false; return false;
} }
function addPort(availableChannels: string[], selectedChannels: string[], newChannel: string) { function addPort(availableChannels: AlsaMidiDeviceInfo[], selectedChannels: AlsaMidiDeviceInfo[], newChannel: AlsaMidiDeviceInfo)
let result: string[] = []; :AlsaMidiDeviceInfo[]
{
let result: AlsaMidiDeviceInfo[] = [];
for (let i = 0; i < availableChannels.length; ++i) { for (let i = 0; i < availableChannels.length; ++i) {
let channel = availableChannels[i]; let channel = availableChannels[i];
if (isChecked(selectedChannels, channel) || channel === newChannel) { if (isChecked(selectedChannels, channel) || channel.equals(newChannel)) {
result.push(channel); result.push(channel);
} }
} }
return result; return result;
} }
function removePort(selectedChannels: string[], channel: string) { function removePort(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo)
let result: string[] = []; :AlsaMidiDeviceInfo[]
{
let result: AlsaMidiDeviceInfo[] = [];
for (let i = 0; i < selectedChannels.length; ++i) { for (let i = 0; i < selectedChannels.length; ++i) {
if (selectedChannels[i] !== channel) { if (!selectedChannels[i].equals(channel)) {
result.push(selectedChannels[i]); result.push(selectedChannels[i]);
} }
} }
@@ -72,7 +77,7 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
let toggleSelect = (value: string) => { let toggleSelect = (value: AlsaMidiDeviceInfo) => {
if (!isChecked(currentSelection, value)) { if (!isChecked(currentSelection, value)) {
setCurrentSelection(addPort(availableChannels, currentSelection, value)); setCurrentSelection(addPort(availableChannels, currentSelection, value));
} else { } else {
@@ -90,15 +95,15 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
return ( return (
<Dialog onClose={handleClose} aria-labelledby="select-channels-title" open={open}> <Dialog onClose={handleClose} aria-labelledby="select-channels-title" open={open}>
<DialogTitle id="simple-dialog-title">Select Channels</DialogTitle> <DialogTitle id="simple-dialog-title">Select MIDI Device</DialogTitle>
<List> <List>
{availableChannels.map((channel) => ( {availableChannels.map((channel) => (
<ListItem button > <ListItem button >
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox checked={isChecked(currentSelection, channel)} onClick={() => toggleSelect(channel)} key={channel} /> <Checkbox checked={isChecked(currentSelection, channel)} onClick={() => toggleSelect(channel)} key={channel.name} />
} }
label={channel} label={channel.description}
/> />
</ListItem> </ListItem>
) )
+10 -9
View File
@@ -42,6 +42,7 @@ import WifiConfigDialog from './WifiConfigDialog';
import WifiDirectConfigDialog from './WifiDirectConfigDialog'; import WifiDirectConfigDialog from './WifiDirectConfigDialog';
import DialogEx from './DialogEx' import DialogEx from './DialogEx'
import GovernorSettings from './GovernorSettings'; import GovernorSettings from './GovernorSettings';
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
import Slide, { SlideProps } from '@mui/material/Slide'; import Slide, { SlideProps } from '@mui/material/Slide';
import { createStyles, Theme } from '@mui/material/styles'; import { createStyles, Theme } from '@mui/material/styles';
@@ -390,10 +391,10 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
}); });
} }
handleSelectMidiDialogResult(channels: string[] | null): void { handleSelectMidiDialogResult(channels: AlsaMidiDeviceInfo[] | null): void {
if (channels) { if (channels) {
let newSelection = this.state.jackSettings.clone(); let newSelection: JackChannelSelection = this.state.jackSettings.clone();
newSelection.inputMidiPorts = channels; newSelection.inputMidiDevices = channels;
this.model.setJackSettings(newSelection); this.model.setJackSettings(newSelection);
} }
@@ -430,9 +431,9 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
} }
midiSummary(): string { midiSummary(): string {
let ports = this.state.jackSettings.inputMidiPorts; let ports = this.state.jackSettings.inputMidiDevices;
if (ports.length === 0) return "Disabled"; if (ports.length === 0) return "Disabled";
if (ports.length === 1) return ports[0]; if (ports.length === 1) return ports[0].description;
return ports.length + " channels"; return ports.length + " channels";
} }
handleShowWifiConfigDialog() { handleShowWifiConfigDialog() {
@@ -592,7 +593,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
<ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiSelection()} > <ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiSelection()} >
<SelectHoverBackground selected={false} showHover={true} /> <SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}> <div style={{ width: "100%" }}>
<Typography className={classes.primaryItem} display="block" variant="body2" noWrap>Select MIDI input channels</Typography> <Typography className={classes.primaryItem} display="block" variant="body2" noWrap>Select MIDI input</Typography>
<Typography className={classes.secondaryItem} display="block" variant="caption" color="textSecondary" noWrap>{this.midiSummary()}</Typography> <Typography className={classes.secondaryItem} display="block" variant="caption" color="textSecondary" noWrap>{this.midiSummary()}</Typography>
</div> </div>
@@ -767,9 +768,9 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
(this.state.showMidiSelectDialog) && (this.state.showMidiSelectDialog) &&
( (
<SelectMidiChannelsDialog open={this.state.showMidiSelectDialog} <SelectMidiChannelsDialog open={this.state.showMidiSelectDialog}
onClose={(selectedChannels: string[] | null) => this.handleSelectMidiDialogResult(selectedChannels)} onClose={(selectedChannels: AlsaMidiDeviceInfo[] | null) => this.handleSelectMidiDialogResult(selectedChannels)}
selectedChannels={this.state.jackSettings.inputMidiPorts} selectedChannels={this.state.jackSettings.inputMidiDevices}
availableChannels={this.state.jackConfiguration.inputMidiPorts} availableChannels={this.state.jackConfiguration.inputMidiDevices}
/> />
) )
+6
View File
@@ -204,10 +204,16 @@ bool setJackConfiguration(JackServerSettings serverSettings)
{ {
bool success = true; bool success = true;
#if JACK_HOST
serverSettings.Write(); serverSettings.Write();
silentSysExec("/usr/bin/systemctl unmask jack"); silentSysExec("/usr/bin/systemctl unmask jack");
silentSysExec("/usr/bin/systemctl enable jack"); silentSysExec("/usr/bin/systemctl enable jack");
#else
// otherwise,
// the config was written before invoking admin main. but we still need to restart the service.
#endif
std::thread delayedRestartThread(delayedRestartProc); std::thread delayedRestartThread(delayedRestartProc);
delayedRestartThread.detach(); delayedRestartThread.detach();
+1686
View File
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
#pragma once
#include "AudioDriver.hpp"
#include "JackServerSettings.hpp"
namespace pipedal {
bool GetAlsaChannels(const JackServerSettings &jackServerSettings,
std::vector<std::string>&inputAudioPorts,
std::vector<std::string>&outputAudioPorts
);
AudioDriver* CreateAlsaDriver(AudioDriverHost*driverHost);
void MidiDecoderTest();
}
+278
View File
@@ -0,0 +1,278 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
#include "pch.h"
#include "catch.hpp"
#include <cmath>
#include <mutex>
#include <iostream>
#include "AlsaDriver.hpp"
#include "JackDriver.hpp"
using namespace pipedal;
using namespace std;
class AlsaTester: private AudioDriverHost {
public:
enum class TestType { Oscillator, LatencyMonitor, NullTest};
private:
AudioDriver *audioDriver = nullptr;
TestType testType;
public:
AlsaTester(TestType testType)
: testType(testType)
{
// audioDriver = CreateAlsaDriver(this);
// audioDriver = CreateJackDriver(this);
}
~AlsaTester()
{
delete audioDriver;
delete[] inputBuffers;
delete[] outputBuffers;
}
bool useJack = false;
void Test()
{
JackServerSettings serverSettings("hw:M2",48000,32,3);
JackConfiguration jackConfiguration;
if (useJack)
{
jackConfiguration.JackInitialize();
} else {
jackConfiguration.AlsaInitialize(serverSettings);
}
JackChannelSelection channelSelection(
jackConfiguration.GetInputAudioPorts(),
jackConfiguration.GetOutputAudioPorts(),
jackConfiguration.GetInputMidiDevices());
#if JACK_HOST
if (useJack)
{
audioDriver = CreateJackDriver(this);
} else {
audioDriver = CreateAlsaDriver(this);
}
#else
audioDriver = CreateAlsaDriver(this);
#endif
oscillator.Init(440,jackConfiguration.GetSampleRate());
latencyMonitor.Init(jackConfiguration.GetSampleRate());
audioDriver->Open(serverSettings,channelSelection);
inputBuffers = new float*[channelSelection.GetInputAudioPorts().size()];
outputBuffers = new float*[channelSelection.GetOutputAudioPorts().size()];
audioDriver->Activate();
for (int i = 0; i < 10; ++i)
{
sleep(1);
if (testType == TestType::LatencyMonitor)
{
auto latency = this->latencyMonitor.GetLatency();
double ms = 1000.0*latency/jackConfiguration.GetSampleRate();
cout << "Latency: " << latency << " samples " << ms << "ms" << " xruns: " << GetXruns() << " Cpu: " << audioDriver->CpuUse() << "%" << endl;
}
}
audioDriver->Deactivate();
audioDriver->Close();
}
float**inputBuffers = nullptr;
float**outputBuffers = nullptr;
class Oscillator {
private:
double dx = 0;
double x = 0;
double dx2 = 0;
double x2 = 0;
public:
void Init(float frequency, size_t sampleRate)
{
dx = frequency*3.141592736*2/sampleRate;
dx2 = 0.5*3.141592736*2/sampleRate;
}
float Next() {
float result = (float)std::cos(x);
float env = (float)std::cos(x2);
x += dx;
x2 += dx2;
return result*env;
}
};
class LatencyMonitor {
enum class State {
Idle,
Waiting,
};
State state = State::Idle;
uint64_t t;
uint64_t idle_samples;
uint64_t waiting_samples;
size_t current_latency = 0;
size_t latency = 0;
std::mutex sync;
public:
void Init(uint64_t sampleRate)
{
idle_samples = (uint64_t)sampleRate*2;
waiting_samples = (uint64_t)sampleRate*2;
state = State::Idle;
t = idle_samples;
latency = 0;
}
size_t GetLatency() {
std::lock_guard lock { sync};
return latency;
}
float Next(float input)
{
switch(state)
{
default:
case State::Idle:
{
if (t-- == 0) {
state = State::Waiting;
current_latency = 0;
}
return 0.01;
}
break;
case State::Waiting:
{
if (std::abs(input) > 0.1 || current_latency > 500) {
{
std::lock_guard lock { sync};
latency = current_latency;
}
state = State::Idle;
t = idle_samples;
} else {
++current_latency;
}
return current_latency < 100 ? 0.25 : 0.0;
}
break;
}
}
};
Oscillator oscillator;
LatencyMonitor latencyMonitor;
virtual void OnAudioStopped() {
}
virtual void OnProcess(size_t nFrames) {
if (testType == TestType::NullTest) return;
size_t inputs = audioDriver->InputBufferCount();
size_t outputs = audioDriver->OutputBufferCount();
for (size_t i = 0; i < inputs; ++i)
{
inputBuffers[i] = audioDriver->GetInputBuffer(i,nFrames);
}
for (size_t i = 0; i < outputs; ++i)
{
outputBuffers[i] = audioDriver->GetOutputBuffer(i,nFrames);
}
if (this->testType == TestType::Oscillator)
{
for (size_t i = 0; i < nFrames; ++i)
{
float v = oscillator.Next()*0.25f;
for (size_t c = 0; c < outputs; ++c)
{
outputBuffers[c][i] = v;
}
}
}
else {
for (size_t i = 0; i < nFrames; ++i)
{
float v = latencyMonitor.Next(inputBuffers[0][i]);
for (size_t c = 0; c < outputs; ++c)
{
outputBuffers[c][i] = v;
}
}
}
}
std::mutex sync;
uint64_t xruns;
uint64_t GetXruns() {
lock_guard lock { sync};
return xruns;
}
virtual void OnUnderrun() {
lock_guard lock { sync };
++xruns;
}
};
TEST_CASE( "alsa_test", "[alsa_test]" ) {
AlsaTester alsaDriver(AlsaTester::TestType::Oscillator);
alsaDriver.Test();
}
TEST_CASE( "alsa_midi_test", "[alsa_midi_test]" ) {
AlsaTester alsaDriver(AlsaTester::TestType::Oscillator);
MidiDecoderTest();
}
+32
View File
@@ -0,0 +1,32 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
#pragma once
#define JACK_HOST 0
#define ALSA_HOST 1
#if JACK_HOST && ALSA_HOST
#error Choose either JACK or ALSA
#endif
+89
View File
@@ -0,0 +1,89 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
#pragma once
#include "JackConfiguration.hpp"
#include <functional>
namespace pipedal {
using ProcessCallback = std::function<void (size_t)>;
struct MidiEvent
{
/* BINARY COMPATIBLE WITH jack_midi_event_t;! (in case we ever want to resurrect Jack support) */
uint32_t time; /**< Sample index at which event is valid */
size_t size; /**< Number of bytes of data in \a buffer */
uint8_t *buffer; /**< Raw MIDI data */
};
class AudioDriverHost {
public:
virtual void OnProcess(size_t nFrames) = 0;
virtual void OnUnderrun() = 0;
virtual void OnAudioStopped() = 0;
};
class AudioDriver {
public:
virtual ~AudioDriver() { }
virtual float CpuUse() = 0;
virtual float CpuOverhead() = 0;
virtual uint32_t GetSampleRate() = 0;
virtual size_t MidiInputBufferCount() const = 0;
virtual void*GetMidiInputBuffer(size_t channel, size_t nFrames) = 0;
virtual size_t GetMidiInputEventCount(void*buffer) = 0;
virtual bool GetMidiInputEvent(MidiEvent*event, void*portBuf, size_t nFrame) = 0;
virtual void FillMidiBuffers() = 0;
virtual size_t InputBufferCount() const = 0;
virtual float*GetInputBuffer(size_t channel, size_t nFrames) = 0;
virtual size_t OutputBufferCount() const = 0;
virtual float*GetOutputBuffer(size_t channel, size_t nFrames) = 0;
virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0;
virtual void Activate() = 0;
virtual void Deactivate() = 0;
virtual void Close() = 0;
};
};
+32 -3
View File
@@ -94,6 +94,7 @@ add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY -DBOOST_BIND_GLOBAL_PLACEHOLDERS
set (PIPEDAL_SOURCES set (PIPEDAL_SOURCES
CpuUse.hpp CpuUse.cpp
P2pConfigFiles.hpp P2pConfigFiles.hpp
AvahiService.cpp AvahiService.hpp AvahiService.cpp AvahiService.hpp
DeviceIdFile.cpp DeviceIdFile.hpp DeviceIdFile.cpp DeviceIdFile.hpp
@@ -153,6 +154,11 @@ set (PIPEDAL_SOURCES
InheritPriorityMutex.hpp InheritPriorityMutex.cpp InheritPriorityMutex.hpp InheritPriorityMutex.cpp
UnixSocket.cpp UnixSocket.hpp UnixSocket.cpp UnixSocket.hpp
JackDriver.cpp JackDriver.hpp
AlsaDriver.cpp AlsaDriver.hpp
AudioDriver.hpp
AudioConfig.hpp
) )
@@ -165,7 +171,9 @@ set (PIPEDAL_INCLUDES
) )
set(PIPEDAL_LIBS libpipedald pthread atomic stdc++fs asound avahi-common avahi-client systemd set(PIPEDAL_LIBS libpipedald pthread atomic stdc++fs asound avahi-common avahi-client systemd
${LILV_0_LIBRARIES} ${JACK_LIBRARIES} ${LIBNL3_LIBRARIES} ) ${LILV_0_LIBRARIES}
# ${JACK_LIBRARIES}
${LIBNL3_LIBRARIES} )
########################## ##########################
@@ -195,6 +203,7 @@ target_link_libraries(pipedald PRIVATE
################################# #################################
add_executable(pipedaltest testMain.cpp add_executable(pipedaltest testMain.cpp
jsonTest.cpp jsonTest.cpp
AlsaDriverTest.cpp
AvahiServiceTest.cpp AvahiServiceTest.cpp
WifiChannelsTest.cpp WifiChannelsTest.cpp
PiPedalAlsaTest.cpp PiPedalAlsaTest.cpp
@@ -259,6 +268,27 @@ add_executable(pipedalconfig
target_link_libraries(pipedalconfig PRIVATE pthread atomic uuid stdc++fs target_link_libraries(pipedalconfig PRIVATE pthread atomic uuid stdc++fs
) )
add_executable(pipedal_latency_test
PrettyPrinter.hpp
CommandLineParser.hpp
PiLatencyMain.cpp
PiPedalAlsa.hpp PiPedalAlsa.cpp
json.hpp json.cpp
HtmlHelper.cpp HtmlHelper.hpp
asan_options.cpp
Lv2Log.cpp Lv2Log.hpp
AlsaDriver.cpp AlsaDriver.hpp
JackConfiguration.hpp JackConfiguration.cpp
JackServerSettings.hpp JackServerSettings.cpp
CpuUse.hpp
CpuUse.cpp
)
target_link_libraries(pipedal_latency_test PRIVATE pthread asound
)
add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp
UnixSocket.cpp UnixSocket.hpp UnixSocket.cpp UnixSocket.hpp
DeviceIdFile.cpp DeviceIdFile.hpp DeviceIdFile.cpp DeviceIdFile.hpp
@@ -281,7 +311,6 @@ add_executable(pipedaladmind AdminMain.cpp CommandLineParser.hpp
) )
target_link_libraries(pipedaladmind PRIVATE pthread atomic stdc++fs systemd) target_link_libraries(pipedaladmind PRIVATE pthread atomic stdc++fs systemd)
add_executable(processcopyrights copyrightMain.cpp add_executable(processcopyrights copyrightMain.cpp
CommandLineParser.hpp CommandLineParser.hpp
PiPedalException.hpp PiPedalException.hpp
@@ -323,7 +352,7 @@ add_custom_target (
) )
install (TARGETS pipedalconfig pipedald pipedaladmind DESTINATION ${CMAKE_INSTALL_PREFIX}/bin install (TARGETS pipedalconfig pipedald pipedaladmind pipedal_latency_test DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
EXPORT pipedalTargets) EXPORT pipedalTargets)
+37
View File
@@ -157,16 +157,53 @@ namespace pipedal
} }
} }
void AddOption(const std::string &shortOption, const std::string &longOption, bool *pResult)
{
if (shortOption.length() != 0)
{
auto option = "-" + longOption;
options.push_back(new BooleanOption(option, pResult, true));
options.push_back(new BooleanOption(option + "+", pResult, true));
options.push_back(new BooleanOption(option + "-", pResult, false));
}
if (longOption.length() != 0)
{
auto option = "--" + longOption;
options.push_back(new BooleanOption(option, pResult, true));
options.push_back(new BooleanOption(option + "+", pResult, true));
options.push_back(new BooleanOption(option + "-", pResult, false));
}
}
void AddOption(const std::string &option, bool *pResult) void AddOption(const std::string &option, bool *pResult)
{ {
options.push_back(new BooleanOption(option, pResult, true)); options.push_back(new BooleanOption(option, pResult, true));
options.push_back(new BooleanOption(option + "+", pResult, true)); options.push_back(new BooleanOption(option + "+", pResult, true));
options.push_back(new BooleanOption(option + "-", pResult, false)); options.push_back(new BooleanOption(option + "-", pResult, false));
} }
void AddOption(const std::string &shortOption, const std::string &longOption, std::string *pResult)
{
options.push_back(new StringOption("-" + shortOption, pResult));
options.push_back(new StringOption("--" + longOption, pResult));
}
void AddOption(const std::string &option, std::string *pResult) void AddOption(const std::string &option, std::string *pResult)
{ {
options.push_back(new StringOption(option, pResult)); options.push_back(new StringOption(option, pResult));
} }
template <typename T>
void AddOption(const std::string &shortOption, const std::string &longOption, T *pResult)
{
if (shortOption.length() != 0)
options.push_back(new Option<T>("-" + shortOption, pResult));
if (longOption.length() != 0)
options.push_back(new Option<T>("--" + longOption, pResult));
}
template <typename T> template <typename T>
void AddOption(const std::string &option, T *pResult) void AddOption(const std::string &option, T *pResult)
{ {
+36 -12
View File
@@ -36,6 +36,17 @@
#include "DeviceIdFile.hpp" #include "DeviceIdFile.hpp"
#include <uuid/uuid.h> #include <uuid/uuid.h>
#include <random> #include <random>
#include "AudioConfig.hpp"
#if JACK_HOST
#define INSTALL_JACK_SERVICE 1
#else
#define INSTALL_JACK_SERVICE 0
#endif
#define UNINSTALL_JACK_SERVICE 1
#define SS(x) (((std::stringstream &)(std::stringstream() << x)).str()) #define SS(x) (((std::stringstream &)(std::stringstream() << x)).str())
@@ -110,6 +121,14 @@ void EnableService()
} }
void DisableService() void DisableService()
{ {
#if INSTALL_JACK_SERVICE || UNINSTALL_JACK_SERVICE
if (sysExec(SYSTEMCTL_BIN " disable " JACK_SERVICE ".service") != EXIT_SUCCESS)
{
#if INSTALL_JACK_SERVICE
cout << "Error: Failed to disable the " JACK_SERVICE " service.";
#endif
}
#endif
if (sysExec(SYSTEMCTL_BIN " disable " NATIVE_SERVICE ".service") != EXIT_SUCCESS) if (sysExec(SYSTEMCTL_BIN " disable " NATIVE_SERVICE ".service") != EXIT_SUCCESS)
{ {
cout << "Error: Failed to disable the " NATIVE_SERVICE " service."; cout << "Error: Failed to disable the " NATIVE_SERVICE " service.";
@@ -145,10 +164,14 @@ void StopService(bool excludeShutdownService = false)
} }
#endif #endif
} }
#if INSTALL_JACK_SERVICE || UNINSTALL_JACK_SERVICE
if (sysExec(SYSTEMCTL_BIN " stop " JACK_SERVICE ".service") != EXIT_SUCCESS) if (sysExec(SYSTEMCTL_BIN " stop " JACK_SERVICE ".service") != EXIT_SUCCESS)
{ {
#if INSTALL_JACK_SERVICE
throw PiPedalException("Failed to stop the " JACK_SERVICE " service."); throw PiPedalException("Failed to stop the " JACK_SERVICE " service.");
#endif
} }
#endif
} }
void StartService(bool excludeShutdownService = false) void StartService(bool excludeShutdownService = false)
@@ -166,6 +189,7 @@ void StartService(bool excludeShutdownService = false)
{ {
throw PiPedalException("Failed to start the " NATIVE_SERVICE " service."); throw PiPedalException("Failed to start the " NATIVE_SERVICE " service.");
} }
#if INSTALL_JACK_SERVICE
JackServerSettings serverSettings; JackServerSettings serverSettings;
serverSettings.ReadJackConfiguration(); serverSettings.ReadJackConfiguration();
if (serverSettings.IsValid()) if (serverSettings.IsValid())
@@ -175,6 +199,7 @@ void StartService(bool excludeShutdownService = false)
throw PiPedalException("Failed to start the " JACK_SERVICE " service."); throw PiPedalException("Failed to start the " JACK_SERVICE " service.");
} }
} }
#endif
} }
void RestartService(bool excludeShutdownService) void RestartService(bool excludeShutdownService)
{ {
@@ -258,12 +283,15 @@ static void RemoveLine(const std::string &path, const std::string lineToRemove)
const std::string PAM_LINE = "JACK_PROMISCUOUS_SERVER DEFAULT=" JACK_SERVICE_GROUP_NAME; const std::string PAM_LINE = "JACK_PROMISCUOUS_SERVER DEFAULT=" JACK_SERVICE_GROUP_NAME;
void UninstallPamEnv() void UninstallPamEnv()
{ {
#if UNINSTALL_JACK_SERVICE
RemoveLine( RemoveLine(
"/etc/security/pam_env.conf", "/etc/security/pam_env.conf",
PAM_LINE); PAM_LINE);
#endif
} }
void InstallPamEnv() void InstallPamEnv()
{ {
#if INSTALL_JACK_SERVICE
std::string newLine = PAM_LINE; std::string newLine = PAM_LINE;
std::vector<std::string> lines; std::vector<std::string> lines;
std::filesystem::path path = "/etc/security/pam_env.conf"; std::filesystem::path path = "/etc/security/pam_env.conf";
@@ -313,6 +341,7 @@ void InstallPamEnv()
{ {
cout << "Failed to update " << path << ". " << e.what(); cout << "Failed to update " << path << ". " << e.what();
} }
#endif
} }
void InstallLimits() void InstallLimits()
@@ -322,12 +351,12 @@ void InstallLimits()
throw PiPedalException("Failed to create audio service group."); throw PiPedalException("Failed to create audio service group.");
} }
std::filesystem::path limitsConfig = "/usr/security/pipedal.conf"; std::filesystem::path limitsConfig = "/etc/security/limits.d/audio.conf";
if (!std::filesystem::exists(limitsConfig)) if (!std::filesystem::exists(limitsConfig))
{ {
ofstream output(limitsConfig); ofstream output(limitsConfig);
output << "# Realtime priority group used by pipedal/jack daemon" output << "# Realtime priority group used by pipedal audio (and also by jack)"
<< "\n"; << "\n";
output << "@audio - rtprio 95" output << "@audio - rtprio 95"
<< "\n"; << "\n";
@@ -336,15 +365,6 @@ void InstallLimits()
} }
} }
void UninstallLimits()
{
std::filesystem::path limitsConfig = "/usr/security/pipedal.conf";
if (std::filesystem::exists(limitsConfig))
{
std::filesystem::remove(limitsConfig);
}
}
void MaybeStartJackService() void MaybeStartJackService()
{ {
@@ -363,6 +383,8 @@ void InstallJackService()
{ {
InstallLimits(); InstallLimits();
InstallPamEnv(); InstallPamEnv();
#if INSTALL_JACK_SERVICE
if (sysExec(GROUPADD_BIN " -f " JACK_SERVICE_GROUP_NAME) != EXIT_SUCCESS) if (sysExec(GROUPADD_BIN " -f " JACK_SERVICE_GROUP_NAME) != EXIT_SUCCESS)
{ {
throw PiPedalException("Failed to create jack service group."); throw PiPedalException("Failed to create jack service group.");
@@ -391,6 +413,8 @@ void InstallJackService()
WriteTemplateFile(map, GetServiceFileName(JACK_SERVICE)); WriteTemplateFile(map, GetServiceFileName(JACK_SERVICE));
MaybeStartJackService(); MaybeStartJackService();
#endif
} }
int SudoExec(char **argv) int SudoExec(char **argv)
@@ -463,7 +487,7 @@ void Uninstall()
{ {
} }
UninstallPamEnv(); UninstallPamEnv();
UninstallLimits(); // UninstallLimits();
sysExec(SYSTEMCTL_BIN " daemon-reload"); sysExec(SYSTEMCTL_BIN " daemon-reload");
} }
catch (const std::exception &e) catch (const std::exception &e)
+38
View File
@@ -0,0 +1,38 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
#include "CpuUse.hpp"
using namespace pipedal;
using namespace std;
CpuUse::CpuUseAverager::CpuUseAverager()
{
for (size_t i = 0; i < NUM_SAMPLES; ++i)
{
samples[i] = 0;
}
sampleTotal = 0;
}
+167
View File
@@ -0,0 +1,167 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
#pragma once
#include <chrono>
#include <mutex>
namespace pipedal
{
enum ProfileCategory
{
Init,
Read,
Driver,
Execute,
Write,
MaxCategory
};
constexpr size_t NUM_PROFILE_CATEGORIES = (size_t)ProfileCategory::MaxCategory;
class CpuUse
{
private:
using ClockT = std::chrono::steady_clock;
using TimeT = ClockT::time_point;
using DurationT = ClockT::duration;
using SampleT = DurationT::rep;
public:
class CpuUseAverager
{
private:
static constexpr size_t NUM_SAMPLES = 20; // average over N samples.
SampleT sampleTotal = 0;
SampleT samples[NUM_SAMPLES];
size_t sampleIndex = 0;
public:
CpuUseAverager();
SampleT GetAverage() const
{
return sampleTotal / NUM_SAMPLES;
}
SampleT GetTotal() const {
return sampleTotal;
}
void AddSample(DurationT duration)
{
SampleT sample = duration.count();
sampleTotal = sampleTotal + sample-samples[sampleIndex];
samples[sampleIndex] = sample;
++sampleIndex;
if (sampleIndex >= NUM_SAMPLES)
{
sampleIndex = 0;
}
}
};
private:
std::mutex sync;
TimeT lastSample;
CpuUseAverager profileTimes[NUM_PROFILE_CATEGORIES];
float currentCpuUse = 0;
float currentOverhead = 0;
CpuUseAverager &GetCategory(ProfileCategory category) {
return profileTimes[(size_t)category];
}
public:
TimeT Now() {
return ClockT::now();
}
void SetStartTime(TimeT time)
{
lastSample = time;
}
void AddSample(ProfileCategory category, TimeT time)
{
profileTimes[(size_t)category].AddSample((time-lastSample));
lastSample = time;
}
void AddSample(ProfileCategory category)
{
AddSample(category,Now());
}
void AddSample(ProfileCategory category, TimeT startTime, TimeT endTime)
{
profileTimes[(size_t)category].AddSample((endTime-startTime));
}
void UpdateCpuUse() {
SampleT readTime = GetCategory(ProfileCategory::Read).GetTotal();
SampleT writeTime = GetCategory(ProfileCategory::Driver).GetTotal();
SampleT processingTime = GetCategory(ProfileCategory::Execute).GetTotal() + GetCategory(ProfileCategory::Driver).GetTotal();
// most of the waiting occurs in the write. Assume that the difference between readTime and write Time is how long we spent waiting (i.e. free time)
SampleT waitTime;
SampleT overheadTime;
if (readTime > writeTime)
{
waitTime = readTime-writeTime;
overheadTime = writeTime;
} else {
waitTime = writeTime-readTime;
overheadTime = readTime;
}
SampleT totalTime = writeTime+ readTime + processingTime;
SampleT maxTime = waitTime+processingTime;
float result = 100.0f*(processingTime)/(maxTime);
float overhead = 100.0F*(overheadTime*2)/totalTime;
{
std::lock_guard lock { sync};
currentCpuUse = result;
currentOverhead = overhead;
}
}
float GetCpuUse()
{
std::lock_guard lock { sync};
return currentCpuUse;
}
float GetCpuOverhead()
{
std::lock_guard lock { sync};
return currentOverhead;
}
};
}
+101 -39
View File
@@ -19,50 +19,24 @@
#include "pch.h" #include "pch.h"
#include "JackConfiguration.hpp" #include "JackConfiguration.hpp"
#include "AudioConfig.hpp"
#include "AlsaDriver.hpp"
#include "Lv2Log.hpp" #include "Lv2Log.hpp"
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
#if JACK_HOST
#include <jack/jack.h> #include <jack/jack.h>
#include <jack/types.h> #include <jack/types.h>
#include <jack/session.h> #include <jack/session.h>
#endif
#include <string.h> #include <string.h>
#include "defer.hpp" #include "defer.hpp"
#include <algorithm> #include <algorithm>
using namespace pipedal; using namespace pipedal;
std::string JackStatusToString(jack_status_t status)
{
switch (status)
{
default:
return "Unknown Jack Audio error.";
case JackStatus::JackFailure:
return "Jack Audio operation failed.";
case JackStatus::JackInvalidOption:
return "Invalid Jack Audio operation.";
case JackStatus::JackNameNotUnique:
return "Jack Audio name in use.";
case JackStatus::JackServerStarted:
return "Jack Audio Server started.";
case JackStatus::JackServerFailed:
return "Jack Audio Server failed.";
case JackStatus::JackServerError:
return "Can't connect to the Jack Audio Server.";
case JackStatus::JackNoSuchClient:
return "Jack Audio client not found.";
case JackLoadFailure:
return "Jack Audio client failed to load.";
case JackStatus::JackInitFailure:
return "Failed to initialize the Jack Audio client.";
case JackStatus::JackShmFailure:
return "Jack Audio failed to access shared memory.";
case JackStatus::JackVersionError:
return "Jack Audio Server version error.";
case JackStatus::JackClientZombie:
return "JackClientZombie error.";
}
};
JackConfiguration::JackConfiguration() JackConfiguration::JackConfiguration()
{ {
@@ -73,10 +47,12 @@ JackConfiguration::~JackConfiguration()
{ {
} }
#if JACK_HOST
static void AddPorts(jack_client_t *client, std::vector<std::string> *output, const char *port_name_pattern, static void AddPorts(jack_client_t *client, std::vector<std::string> *output, const char *port_name_pattern,
const char *type_name_pattern, const char *type_name_pattern,
unsigned long flags) unsigned long flags)
{ {
#if JACK_HOST
const char**ports = jack_get_ports(client, port_name_pattern, type_name_pattern, flags); const char**ports = jack_get_ports(client, port_name_pattern, type_name_pattern, flags);
if (ports != nullptr) if (ports != nullptr)
{ {
@@ -88,9 +64,14 @@ static void AddPorts(jack_client_t *client, std::vector<std::string> *output, co
jack_free(ports); jack_free(ports);
} }
#else
throw PiPedalStateException("JACK_HOST not enabled at compile time.");
#endif
} }
#endif
namespace pipedal { namespace pipedal {
#if JACK_HOST
std::string GetJackErrorMessage(jack_status_t status) std::string GetJackErrorMessage(jack_status_t status)
{ {
std::stringstream s; std::stringstream s;
@@ -122,9 +103,43 @@ namespace pipedal {
return s.str(); return s.str();
} }
#endif
} }
void JackConfiguration::Initialize()
void JackConfiguration::AlsaInitialize(
const JackServerSettings &jackServerSettings)
{ {
this->isValid_ = false;
this->errorStatus_ = "";
this->inputMidiDevices_ = GetAlsaMidiInputDevices();
if (jackServerSettings.IsValid())
{
this->blockLength_ = jackServerSettings.GetBufferSize();
this->sampleRate_ = jackServerSettings.GetSampleRate();
try {
if (GetAlsaChannels(jackServerSettings,
inputAudioPorts_,outputAudioPorts_))
{
this->isValid_ = true;
}
} catch (const std::exception& /*ignore*/)
{
}
}
if (!this->isValid_)
{
this->isValid_ = false;
this->errorStatus_ = "Not configured.";
}
}
void JackConfiguration::JackInitialize()
{
#if JACK_HOST
jack_status_t status; jack_status_t status;
jack_client_t *client = nullptr; jack_client_t *client = nullptr;
@@ -161,8 +176,6 @@ void JackConfiguration::Initialize()
AddPorts(client, &this->inputAudioPorts_, "system", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput); AddPorts(client, &this->inputAudioPorts_, "system", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput);
AddPorts(client, &this->outputAudioPorts_, "system", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput); AddPorts(client, &this->outputAudioPorts_, "system", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput);
AddPorts(client, &this->inputMidiPorts_, "system", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput);
AddPorts(client, &this->outputMidiPorts_, "system", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput);
isValid_ = true; isValid_ = true;
this->errorStatus_ = ""; this->errorStatus_ = "";
jack_client_close(client); jack_client_close(client);
@@ -172,6 +185,9 @@ void JackConfiguration::Initialize()
jack_client_close(client); jack_client_close(client);
this->errorStatus_ = e.what(); this->errorStatus_ = e.what();
} }
#else
throw PiPedalStateException("JACK_HOST not enabled at compile time.");
#endif
} }
JackChannelSelection JackChannelSelection::MakeDefault(const JackConfiguration&config){ JackChannelSelection JackChannelSelection::MakeDefault(const JackConfiguration&config){
@@ -211,12 +227,59 @@ static std::vector<std::string> makeValid(const std::vector<std::string> & selec
} }
return result; return result;
} }
static std::vector<AlsaMidiDeviceInfo> makeValid(const std::vector<AlsaMidiDeviceInfo> & selected, const std::vector<AlsaMidiDeviceInfo> &available)
{
// Matching rule:
// same name AND same description (unchanged)
// different name and same description: use the new device name. (i.e. same device plugged into different USB port)
// otherwise discard.
std::vector<AlsaMidiDeviceInfo> sourceDevices = available;
std::vector<AlsaMidiDeviceInfo> result;
std::vector<AlsaMidiDeviceInfo> partialMatch;
for (auto & sel : selected)
{
size_t matchIndex = -1;
for (size_t i = 0; i < sourceDevices.size(); ++i)
{
const AlsaMidiDeviceInfo &candidate = sourceDevices[i];
if (candidate.description_ == sel.description_)
{
if (candidate.name_ == sel.name_)
{
result.push_back(candidate);
} else {
partialMatch.push_back(candidate);
}
sourceDevices.erase(sourceDevices.begin()+i);
break;
}
}
}
for (auto &sel : partialMatch)
{
for (size_t i = 0; i < sourceDevices.size(); ++i)
{
const AlsaMidiDeviceInfo &candidate = sourceDevices[i];
if (candidate.description_ == sel.description_)
{
result.push_back(candidate); // same description, usb address has changed. includeit.
sourceDevices.erase(sourceDevices.begin()+i);
break;
}
}
}
return result;
}
JackChannelSelection JackChannelSelection::RemoveInvalidChannels(const JackConfiguration&config) const JackChannelSelection JackChannelSelection::RemoveInvalidChannels(const JackConfiguration&config) const
{ {
JackChannelSelection result; JackChannelSelection result;
result.inputAudioPorts_ = makeValid(this->inputAudioPorts_,config.GetInputAudioPorts()); result.inputAudioPorts_ = makeValid(this->inputAudioPorts_,config.GetInputAudioPorts());
result.outputAudioPorts_ = makeValid(this->outputAudioPorts_,config.GetOutputAudioPorts()); result.outputAudioPorts_ = makeValid(this->outputAudioPorts_,config.GetOutputAudioPorts());
result.inputMidiPorts_ = makeValid(this->inputMidiPorts_, config.GetInputMidiPorts()); result.inputMidiDevices_ = makeValid(this->inputMidiDevices_, config.GetInputMidiDevices());
if (!result.isValid()) if (!result.isValid())
{ {
return this->MakeDefault(config); return this->MakeDefault(config);
@@ -226,7 +289,7 @@ JackChannelSelection JackChannelSelection::RemoveInvalidChannels(const JackConfi
JSON_MAP_BEGIN(JackChannelSelection) JSON_MAP_BEGIN(JackChannelSelection)
JSON_MAP_REFERENCE(JackChannelSelection,inputAudioPorts) JSON_MAP_REFERENCE(JackChannelSelection,inputAudioPorts)
JSON_MAP_REFERENCE(JackChannelSelection,outputAudioPorts) JSON_MAP_REFERENCE(JackChannelSelection,outputAudioPorts)
JSON_MAP_REFERENCE(JackChannelSelection,inputMidiPorts) JSON_MAP_REFERENCE(JackChannelSelection,inputMidiDevices)
JSON_MAP_END() JSON_MAP_END()
@@ -240,7 +303,6 @@ JSON_MAP_BEGIN(JackConfiguration)
JSON_MAP_REFERENCE(JackConfiguration,maxAllowedMidiDelta) JSON_MAP_REFERENCE(JackConfiguration,maxAllowedMidiDelta)
JSON_MAP_REFERENCE(JackConfiguration,inputAudioPorts) JSON_MAP_REFERENCE(JackConfiguration,inputAudioPorts)
JSON_MAP_REFERENCE(JackConfiguration,outputAudioPorts) JSON_MAP_REFERENCE(JackConfiguration,outputAudioPorts)
JSON_MAP_REFERENCE(JackConfiguration,inputMidiPorts) JSON_MAP_REFERENCE(JackConfiguration,inputMidiDevices)
JSON_MAP_REFERENCE(JackConfiguration,outputMidiPorts)
JSON_MAP_END() JSON_MAP_END()
+23 -9
View File
@@ -22,6 +22,9 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include "json.hpp" #include "json.hpp"
#include "JackServerSettings.hpp"
#include "PiPedalAlsa.hpp"
namespace pipedal namespace pipedal
{ {
@@ -42,11 +45,13 @@ namespace pipedal
std::vector<std::string> inputAudioPorts_; std::vector<std::string> inputAudioPorts_;
std::vector<std::string> outputAudioPorts_; std::vector<std::string> outputAudioPorts_;
std::vector<std::string> inputMidiPorts_; std::vector<AlsaMidiDeviceInfo> inputMidiDevices_;
std::vector<std::string> outputMidiPorts_;
public: public:
JackConfiguration(); JackConfiguration();
void Initialize();
void AlsaInitialize(const JackServerSettings &jackServerSettings); // from also config settings.
void JackInitialize(); // from jack server instance.
~JackConfiguration(); ~JackConfiguration();
bool isValid() const { return isValid_;} bool isValid() const { return isValid_;}
bool isRestarting() const { return isRestarting_; } bool isRestarting() const { return isRestarting_; }
@@ -59,8 +64,7 @@ namespace pipedal
const std::vector<std::string> &GetInputAudioPorts() const { return inputAudioPorts_; } const std::vector<std::string> &GetInputAudioPorts() const { return inputAudioPorts_; }
const std::vector<std::string> &GetOutputAudioPorts() const { return outputAudioPorts_; } const std::vector<std::string> &GetOutputAudioPorts() const { return outputAudioPorts_; }
const std::vector<std::string> &GetInputMidiPorts() const { return inputMidiPorts_; } const std::vector<AlsaMidiDeviceInfo> &GetInputMidiDevices() const { return inputMidiDevices_; }
const std::vector<std::string> &GetOutputMidiPorts() const { return outputMidiPorts_; }
DECLARE_JSON_MAP(JackConfiguration); DECLARE_JSON_MAP(JackConfiguration);
@@ -69,10 +73,20 @@ namespace pipedal
private: private:
std::vector<std::string> inputAudioPorts_; std::vector<std::string> inputAudioPorts_;
std::vector<std::string> outputAudioPorts_; std::vector<std::string> outputAudioPorts_;
std::vector<std::string> inputMidiPorts_; std::vector<AlsaMidiDeviceInfo> inputMidiDevices_;
public: public:
JackChannelSelection() JackChannelSelection()
{ {
}
JackChannelSelection(
std::vector<std::string> inputAudioPorts,
std::vector<std::string> outputAudioPorts,
std::vector<AlsaMidiDeviceInfo> inputMidiDevices
) : inputAudioPorts_(inputAudioPorts),
outputAudioPorts_(outputAudioPorts),
inputMidiDevices_(inputMidiDevices)
{
} }
bool isValid() const { bool isValid() const {
@@ -83,14 +97,14 @@ namespace pipedal
{ {
return inputAudioPorts_; return inputAudioPorts_;
} }
const std::vector<std::string>& getOutputAudioPorts() const const std::vector<std::string>& GetOutputAudioPorts() const
{ {
return outputAudioPorts_; return outputAudioPorts_;
} }
const std::vector<std::string>& GetInputMidiPorts() const const std::vector<AlsaMidiDeviceInfo>& GetInputMidiDevices() const
{ {
return inputMidiPorts_; return inputMidiDevices_;
} }
JackChannelSelection RemoveInvalidChannels(const JackConfiguration&config) const; JackChannelSelection RemoveInvalidChannels(const JackConfiguration&config) const;
+512
View File
@@ -0,0 +1,512 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
/* Status of Jack support.
PiPedal was originally written for Jack, but subsequently ported to Alsa because running
Jack as a systemd daemon proved to be unsupportable,.
*/
#include "pch.h"
#include "JackDriver.hpp"
#include <jack/jack.h>
#include <jack/types.h>
#include <jack/session.h>
#include <jack/midiport.h>
#include "Lv2Log.hpp"
#if JACK_HOST
namespace pipedal {
std::string JackStatusToString(jack_status_t status)
{
switch (status)
{
default:
return "Unknown Jack Audio error.";
case JackStatus::JackFailure:
return "Jack Audio operation failed.";
case JackStatus::JackInvalidOption:
return "Invalid Jack Audio operation.";
case JackStatus::JackNameNotUnique:
return "Jack Audio name in use.";
case JackStatus::JackServerStarted:
return "Jack Audio Server started.";
case JackStatus::JackServerFailed:
return "Jack Audio Server failed.";
case JackStatus::JackServerError:
return "Can't connect to the Jack Audio Server.";
case JackStatus::JackNoSuchClient:
return "Jack Audio client not found.";
case JackLoadFailure:
return "Jack Audio client failed to load.";
case JackStatus::JackInitFailure:
return "Failed to initialize the Jack Audio client.";
case JackStatus::JackShmFailure:
return "Jack Audio failed to access shared memory.";
case JackStatus::JackVersionError:
return "Jack Audio Server version error.";
case JackStatus::JackClientZombie:
return "JackClientZombie error.";
}
};
/*
This code relies on the fact that jack_midi_event_t and pipedal::MidiEvent are binary-compatible.
(the default build does not require Jack Audio headers to be present).
Test to make sure this is still true.
*/
static_assert(sizeof(jack_midi_event_t) == sizeof(pipedal::MidiEvent));
#define CHECK_FIELD(fieldName) \
static_assert(offsetof(jack_midi_event_t,fieldName) == offsetof(pipedal::MidiEvent,fieldName)); \
static_assert(sizeof(jack_midi_event_t::fieldName) == sizeof(pipedal::MidiEvent::fieldName));
CHECK_FIELD(size)
CHECK_FIELD(buffer)
CHECK_FIELD(time)
// private import from JackConfiguration.cpp
std::string GetJackErrorMessage(jack_status_t status);
// forward decl
static int process_fn(jack_nframes_t nframes, void *arg);
static void jack_error_fn(const char *msg)
{
Lv2Log::error("Jack - %s", msg);
}
static void jack_info_fn(const char *msg)
{
Lv2Log::info("Jack - %s", msg);
}
class JackDriverImpl : public AudioDriver {
private:
jack_client_t *client = nullptr;
std::vector<jack_port_t *> inputPorts;
std::vector<jack_port_t *> outputPorts;
std::vector<jack_port_t *> midiInputPorts;
uint32_t sampleRate = 0;
AudioDriverHost *driverHost = nullptr;
public:
JackDriverImpl(AudioDriverHost*driverHost)
: driverHost(driverHost)
{
jack_set_error_function(jack_error_fn);
jack_set_info_function(jack_info_fn);
}
virtual ~JackDriverImpl()
{
jack_set_error_function(nullptr);
jack_set_info_function(nullptr);
}
private:
void OnShutdown()
{
Lv2Log::info("Jack Audio Server has shut down.");
}
static void
jack_shutdown_fn(void *arg)
{
((JackDriverImpl *)arg)->OnShutdown();
}
static int jackInstanceId;
static int xrun_callback_fn(void *arg)
{
((AudioDriverHost *)arg)->OnUnderrun();
return 0;
}
virtual uint32_t GetSampleRate() {
return this->sampleRate;
}
bool open = false;
virtual void Open(const JackServerSettings&jackServerSettings,const JackChannelSelection &channelSelection) {
if (open) throw PiPedalStateException("Already open");
open = true;
this->channelSelection = channelSelection;
jack_status_t status;
// need a unique instance name every timme.
std::stringstream s;
s << "PiPedal";
if (jackInstanceId != 0) {
s << jackInstanceId;
}
++jackInstanceId;
std::string instanceName = s.str();
client = jack_client_open(instanceName.c_str(), JackNullOption, &status);
if (client == nullptr || status & JackFailure)
{
if (client)
{
jack_client_close(client);
client = nullptr;
}
std::string error = GetJackErrorMessage(status);
Lv2Log::error(error);
throw PiPedalStateException(error.c_str());
}
if (status & JackServerStarted)
{
Lv2Log::info("Jack server started.");
}
jack_set_process_callback(client, process_fn, this->driverHost);
jack_on_shutdown(client, jack_shutdown_fn, this);
jack_set_xrun_callback(client, xrun_callback_fn, this->driverHost);
this->sampleRate = jack_get_sample_rate(client);
auto &selectedInputPorts = channelSelection.GetInputAudioPorts();
this->inputPorts.clear();
this->outputPorts.clear();
this->midiInputPorts.clear();
this->inputPorts.resize(selectedInputPorts.size());
for (int i = 0; i < selectedInputPorts.size(); ++i)
{
std::stringstream name;
name << "input" << (i + 1);
this->inputPorts[i] =
jack_port_register(
client, name.str().c_str(),
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsInput, 0);
if (this->inputPorts[i] == nullptr)
{
Lv2Log::error("Can't allocate Jack Audio input ports.");
throw PiPedalStateException("Failed to allocate Jack Audio port.");
}
}
auto &selectedOutputPorts = channelSelection.GetOutputAudioPorts();
this->outputPorts.resize(selectedOutputPorts.size());
for (int i = 0; i < selectedOutputPorts.size(); ++i)
{
std::stringstream name;
name << "output" << (i + 1);
this->outputPorts[i] =
jack_port_register(client, name.str().c_str(),
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
if (this->outputPorts[i] == nullptr)
{
Lv2Log::error("Can't allocate Jack Audio output port.");
throw PiPedalStateException("Failed to allocate Jack Audio port.");
}
}
auto &selectedMidiPorts = channelSelection.GetInputMidiDevices();
this->midiInputPorts.resize(selectedMidiPorts.size());
for (int i = 0; i < selectedMidiPorts.size(); ++i)
{
std::stringstream name;
name << "midiIn" << (i + 1);
this->midiInputPorts[i] =
jack_port_register(client, name.str().c_str(),
JACK_DEFAULT_MIDI_TYPE,
JackPortIsInput, 0);
if (this->midiInputPorts[i] == nullptr)
{
std::stringstream s;
s << "can't register Jack port " << name.str().c_str();
Lv2Log::error(s.str());
throw PiPedalStateException(s.str().c_str());
}
}
}
bool jackActive = false;
JackChannelSelection channelSelection;
virtual void Activate()
{
int activateResult = jack_activate(client);
if (activateResult == 0)
{
jackActive = true;
auto &selectedInputPorts = channelSelection.GetInputAudioPorts();
for (int i = 0; i < selectedInputPorts.size(); ++i)
{
auto result = jack_connect(client, selectedInputPorts[i].c_str(), jack_port_name(this->inputPorts[i]));
if (result)
{
Lv2Log::error("Can't connect input port %s", selectedInputPorts[i].c_str());
throw PiPedalStateException("Jack Audio port connection failed.");
}
}
auto &selectedOutputPorts = channelSelection.GetOutputAudioPorts();
for (int i = 0; i < selectedOutputPorts.size(); ++i)
{
auto result = jack_connect(client, jack_port_name(this->outputPorts[i]), selectedOutputPorts[i].c_str());
if (result)
{
Lv2Log::error("Can't connect output port %s", selectedOutputPorts[i].c_str());
throw PiPedalStateException("Jack Audio port connection failed.");
}
}
auto &selectedMidiPorts = channelSelection.GetInputMidiDevices();
for (int i = 0; i < selectedMidiPorts.size(); ++i)
{
auto result = jack_connect(client, selectedMidiPorts[i].name_.c_str(), jack_port_name(this->midiInputPorts[i]));
if (result)
{
Lv2Log::error("Can't connect midi input port %s", selectedMidiPorts[i].name_.c_str());
throw PiPedalStateException("Jack Audio midi port connection failed.");
}
}
Lv2Log::info("Jack configuration complete.");
}
else
{
Lv2Log::error("Failed to activate Jack Audio client. (%d)", (int)activateResult);
throw PiPedalStateException("Failed to activate Jack Audio client.");
}
}
void FillMidiBuffers()
{
// do nothing. Jack does this for us.
}
virtual void Deactivate()
{
if (!jackActive)
{
return;
}
jackActive = false;
// disconnect ports.
for (size_t i = 0; i < this->inputPorts.size(); ++i)
{
auto port = inputPorts[i];
if (port != nullptr)
{
jack_disconnect(client, channelSelection.GetInputAudioPorts()[i].c_str(), jack_port_name(port));
}
}
for (size_t i = 0; i < outputPorts.size(); ++i)
{
auto port = outputPorts[i];
if (port != nullptr)
{
jack_disconnect(client, jack_port_name(port),channelSelection.GetOutputAudioPorts()[i].c_str());
}
}
for (size_t i = 0; i < midiInputPorts.size(); ++i)
{
auto port = midiInputPorts[i];
if (port != nullptr)
{
jack_disconnect(client, channelSelection.GetInputMidiDevices()[i].name_.c_str(), jack_port_name(port));
}
}
if (jackActive)
{
jack_deactivate(client);
jackActive = false;
}
jack_set_process_callback(client, nullptr, nullptr);
jack_on_shutdown(client, nullptr, nullptr);
#if JACK_SESSION_CALLBACK // (deprecated, and not actually useful)
jack_set_session_callback(client, nullptr, nullptr);
#endif
jack_set_xrun_callback(client, nullptr, nullptr);
}
virtual size_t MidiInputBufferCount() const { return this->midiInputPorts.size(); }
virtual void*GetMidiInputBuffer(size_t channel, size_t nFrames) {
return (void*)jack_port_get_buffer(this->midiInputPorts[channel], nFrames);
}
virtual size_t GetMidiInputEventCount(void *portBuffer)
{
return jack_midi_get_event_count(portBuffer);
}
virtual bool GetMidiInputEvent(MidiEvent*event, void*portBuf, size_t nFrame)
{
return jack_midi_event_get((jack_midi_event_t*)event,portBuf,(uint32_t)nFrame);
}
virtual size_t InputBufferCount() const { return inputPorts.size(); }
virtual float*GetInputBuffer(size_t channel, size_t nFrames) {
return (float*)jack_port_get_buffer(this->inputPorts[channel], nFrames);
}
virtual size_t OutputBufferCount() const { return outputPorts.size(); }
virtual float*GetOutputBuffer(size_t channel, size_t nFrames)
{
return (float*)jack_port_get_buffer(this->outputPorts[channel], nFrames);
}
virtual void Close()
{
if (!open) return;
open = false;
// unregister ports.
for (size_t i = 0; i < this->inputPorts.size(); ++i)
{
auto port = inputPorts[i];
if (port)
{
jack_port_unregister(client, port);
}
}
inputPorts.clear();
for (size_t i = 0; i < this->outputPorts.size(); ++i)
{
auto port = outputPorts[i];
if (port)
{
jack_port_unregister(client, port);
}
}
outputPorts.clear();
for (size_t i = 0; i < this->midiInputPorts.size(); ++i)
{
auto port = midiInputPorts[i];
if (port)
{
jack_port_unregister(client, port);
}
}
midiInputPorts.resize(0);
if (client)
{
jack_client_close(client);
client = nullptr;
}
}
virtual float CpuOverhead() { return 0; }
virtual float CpuUse()
{
if (client)
{
return jack_cpu_load(this->client);
}
else
{
return 0;
}
}
static int process_fn(jack_nframes_t nframes, void *arg)
{
((AudioDriverHost *)arg)->OnProcess(nframes);
return 0;
}
void OnSessionCallback(jack_session_event_t *event)
{
#if JACK_SESSION_CALLBACK // deprecated and not actually useful.
char retval[100];
Lv2Log::info("path %s, uuid %s, type: %s\n", event->session_dir, event->client_uuid, event->type == JackSessionSave ? "save" : "quit");
snprintf(retval, 100, "jack_simple_session_client %s", event->client_uuid);
event->command_line = strdup(retval);
jack_session_reply(client, event);
if (event->type == JackSessionSaveAndQuit)
{
// simple_quit = 1;
}
jack_session_event_free(event);
#endif
}
static void
session_callback_fn(jack_session_event_t *event, void *arg)
{
((JackDriverImpl *)arg)->OnSessionCallback(event);
};
};
AudioDriver* CreateJackDriver(AudioDriverHost*driverHost)
{
return new JackDriverImpl(driverHost);
}
int JackDriverImpl::jackInstanceId = 0;
} // namespace
#endif
+38
View File
@@ -0,0 +1,38 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
#pragma once
#include "AudioDriver.hpp"
#include "AudioConfig.hpp"
#if JACK_HOST
namespace pipedal {
AudioDriver* CreateJackDriver(AudioDriverHost*driverHost);
}
#endif
+94 -323
View File
@@ -21,12 +21,15 @@
#include "Lv2Log.hpp" #include "Lv2Log.hpp"
#include "JackDriver.hpp"
#include "AlsaDriver.hpp"
using namespace pipedal; using namespace pipedal;
#include <jack/jack.h> #include "AudioConfig.hpp"
#include <jack/types.h>
#include <jack/session.h>
#include <jack/midiport.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include <mutex> #include <mutex>
@@ -63,11 +66,6 @@ using namespace pipedal;
const int MIDI_LV2_BUFFER_SIZE = 16 * 1024; const int MIDI_LV2_BUFFER_SIZE = 16 * 1024;
namespace pipedal
{
// private import from JackConfiguration.cpp
std::string GetJackErrorMessage(jack_status_t status);
}
static void GetCpuFrequency(uint64_t*freqMin,uint64_t*freqMax) static void GetCpuFrequency(uint64_t*freqMin,uint64_t*freqMax)
{ {
@@ -105,9 +103,10 @@ static std::string GetGovernor()
class JackHostImpl : public JackHost class JackHostImpl : public JackHost, private AudioDriverHost
{ {
private: private:
AudioDriver*audioDriver = nullptr;
inherit_priority_recursive_mutex mutex; inherit_priority_recursive_mutex mutex;
int64_t overrunGracePeriodSamples = 0; int64_t overrunGracePeriodSamples = 0;
@@ -133,36 +132,26 @@ private:
JackChannelSelection channelSelection; JackChannelSelection channelSelection;
bool active = false; bool active = false;
jack_client_t *client = nullptr;
std::shared_ptr<Lv2PedalBoard> currentPedalBoard; std::shared_ptr<Lv2PedalBoard> currentPedalBoard;
std::vector<std::shared_ptr<Lv2PedalBoard>> activePedalBoards; // pedalboards that have been sent to the audio queue. std::vector<std::shared_ptr<Lv2PedalBoard>> activePedalBoards; // pedalboards that have been sent to the audio queue.
Lv2PedalBoard *realtimeActivePedalBoard = nullptr; Lv2PedalBoard *realtimeActivePedalBoard = nullptr;
std::vector<jack_port_t *> inputPorts;
std::vector<jack_port_t *> outputPorts;
std::vector<jack_port_t *> midiInputPorts;
std::vector<uint8_t *> midiLv2Buffers; std::vector<uint8_t *> midiLv2Buffers;
uint32_t sampleRate; uint32_t sampleRate = 0;
uint64_t currentSample = 0; uint64_t currentSample = 0;
std::atomic<uint64_t> underruns = 0; std::atomic<uint64_t> underruns = 0;
std::atomic<std::chrono::system_clock::time_point> lastUnderrunTime = std::atomic<std::chrono::system_clock::time_point> lastUnderrunTime =
std::chrono::system_clock::from_time_t(0); std::chrono::system_clock::from_time_t(0);
int XrunCallback() virtual void OnUnderrun()
{ {
++this->underruns; ++this->underruns;
this->lastUnderrunTime = std::chrono::system_clock ::now(); this->lastUnderrunTime = std::chrono::system_clock ::now();
return 0;
} }
static int xrun_callback_fn(void *arg)
{
return ((JackHostImpl *)arg)->XrunCallback();
}
virtual void Close() virtual void Close()
{ {
@@ -174,84 +163,13 @@ private:
StopReaderThread(); StopReaderThread();
if (active) if (active)
{ {
// disconnect ports. audioDriver->Deactivate();
for (size_t i = 0; i < this->inputPorts.size(); ++i)
{
auto port = inputPorts[i];
if (port != nullptr)
{
jack_disconnect(client, channelSelection.GetInputAudioPorts()[i].c_str(), jack_port_name(port));
}
}
for (size_t i = 0; i < outputPorts.size(); ++i)
{
auto port = outputPorts[i];
if (port != nullptr)
{
jack_disconnect(client, jack_port_name(port),channelSelection.getOutputAudioPorts()[i].c_str());
}
}
for (size_t i = 0; i < midiInputPorts.size(); ++i)
{
auto port = midiInputPorts[i];
if (port != nullptr)
{
jack_disconnect(client, channelSelection.GetInputMidiPorts()[i].c_str(), jack_port_name(port));
}
}
jack_deactivate(client);
jack_set_process_callback(client, nullptr, nullptr);
jack_on_shutdown(client, nullptr, nullptr);
#if JACK_SESSION_CALLBACK // (deprecated, and not actually useful)
jack_set_session_callback(client, nullptr, nullptr);
#endif
jack_set_xrun_callback(client, nullptr, nullptr);
active = false; active = false;
} }
// unregister ports.
for (size_t i = 0; i < this->inputPorts.size(); ++i)
{
auto port = inputPorts[i];
if (port)
{
jack_port_unregister(client, port);
}
}
inputPorts.clear();
for (size_t i = 0; i < this->outputPorts.size(); ++i) audioDriver->Close();
{
auto port = outputPorts[i];
if (port)
{
jack_port_unregister(client, port);
}
}
outputPorts.clear();
for (size_t i = 0; i < this->midiInputPorts.size(); ++i)
{
auto port = midiInputPorts[i];
if (port)
{
jack_port_unregister(client, port);
}
}
midiInputPorts.resize(0);
for (size_t i = 0; i < midiLv2Buffers.size(); ++i)
{
delete[] midiLv2Buffers[i];
}
midiLv2Buffers.resize(0);
if (client)
{
jack_client_close(client);
client = nullptr;
}
// release any pdealboards owned by the process thread. // release any pdealboards owned by the process thread.
this->activePedalBoards.resize(0); this->activePedalBoards.resize(0);
this->realtimeActivePedalBoard = nullptr; this->realtimeActivePedalBoard = nullptr;
@@ -268,20 +186,32 @@ private:
delete realtimeMonitorPortSubscriptions; delete realtimeMonitorPortSubscriptions;
realtimeVuBuffers = nullptr; realtimeVuBuffers = nullptr;
} }
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, jack_nframes_t nframes) void ZeroBuffer(float *buffer, size_t nframes)
{ {
for (jack_nframes_t i = 0; i < nframes; ++i) for (size_t i = 0; i < nframes; ++i)
{ {
buffer[i] = 0; buffer[i] = 0;
} }
} }
void ZeroOutputBuffers(jack_nframes_t nframes) Lv2EventBufferUrids eventBufferUrids;
void ZeroOutputBuffers(size_t nframes)
{ {
for (int i = 0; i < this->outputPorts.size(); ++i) for (size_t i = 0; i < audioDriver->OutputBufferCount(); ++i)
{ {
float *out = (float *)jack_port_get_buffer(this->outputPorts[i], nframes); float *out = (float *)audioDriver->GetOutputBuffer(i, nframes);
if (out) if (out)
{ {
ZeroBuffer(out, nframes); ZeroBuffer(out, nframes);
@@ -487,22 +417,26 @@ private:
{ {
Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids); Lv2EventBufferWriter eventBufferWriter(this->eventBufferUrids);
for (int i = 0; i < this->midiInputPorts.size(); ++i) size_t midiInputBufferCount = audioDriver->MidiInputBufferCount();
audioDriver->FillMidiBuffers();
for (size_t i = 0; i < midiInputBufferCount; ++i)
{ {
jack_port_t *port = midiInputPorts[i];
void *portBuffer = jack_port_get_buffer(midiInputPorts[i], 0); void *portBuffer = audioDriver->GetMidiInputBuffer(i,0);
if (portBuffer) if (portBuffer)
{ {
uint8_t *lv2Buffer = this->midiLv2Buffers[i]; uint8_t *lv2Buffer = this->midiLv2Buffers[i];
jack_nframes_t n = jack_midi_get_event_count(portBuffer); size_t n = audioDriver->GetMidiInputEventCount(portBuffer);
eventBufferWriter.Reset(lv2Buffer, MIDI_LV2_BUFFER_SIZE); eventBufferWriter.Reset(lv2Buffer, MIDI_LV2_BUFFER_SIZE);
auto iterator = eventBufferWriter.begin(); auto iterator = eventBufferWriter.begin();
for (jack_nframes_t frame = 0; frame < n; ++frame) for (size_t frame = 0; frame < n; ++frame)
{ {
jack_midi_event_t event; MidiEvent event;
if (jack_midi_event_get(&event, portBuffer, frame) == 0) if (audioDriver->GetMidiInputEvent(&event,portBuffer,frame))
{ {
eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer); eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer);
@@ -528,11 +462,26 @@ private:
#define RESET_XRUN_SAMPLES 22050ul // 1/2 a second-ish. #define RESET_XRUN_SAMPLES 22050ul // 1/2 a second-ish.
int OnProcess(jack_nframes_t nframes) std::mutex audioStoppedMutex;
bool IsAudioActive()
{
std::lock_guard lock { audioStoppedMutex};
return this->active;
}
virtual void OnAudioStopped()
{
std::lock_guard lock { audioStoppedMutex};
this->active = false;
Lv2Log::info("Audio stopped.");
}
virtual void OnProcess(size_t nframes)
{ {
try try
{ {
jack_default_audio_sample_t *in, *out; float *in, *out;
pParameterRequests = nullptr; pParameterRequests = nullptr;
@@ -547,9 +496,9 @@ private:
float *inputBuffers[4]; float *inputBuffers[4];
float *outputBuffers[4]; float *outputBuffers[4];
bool buffersValid = true; bool buffersValid = true;
for (int i = 0; i < inputPorts.size(); ++i) for (int i = 0; i < audioDriver->InputBufferCount(); ++i)
{ {
float *input = (float *)jack_port_get_buffer(inputPorts[i], nframes); float *input = (float *)audioDriver->GetInputBuffer(i, nframes);
if (input == nullptr) if (input == nullptr)
{ {
buffersValid = false; buffersValid = false;
@@ -557,11 +506,11 @@ private:
} }
inputBuffers[i] = input; inputBuffers[i] = input;
} }
inputBuffers[inputPorts.size()] = nullptr; inputBuffers[audioDriver->InputBufferCount()] = nullptr;
for (int i = 0; i < outputPorts.size(); ++i) for (int i = 0; i < audioDriver->OutputBufferCount(); ++i)
{ {
float *output = (float *)jack_port_get_buffer(outputPorts[i], nframes); float *output = audioDriver->GetOutputBuffer(i,nframes);
if (output == nullptr) if (output == nullptr)
{ {
buffersValid = false; buffersValid = false;
@@ -569,7 +518,7 @@ private:
} }
outputBuffers[i] = output; outputBuffers[i] = output;
} }
outputBuffers[outputPorts.size()] = nullptr; outputBuffers[audioDriver->OutputBufferCount()] = nullptr;
if (buffersValid) if (buffersValid)
{ {
@@ -626,61 +575,9 @@ private:
throw; throw;
} }
return 0;
} }
static int process_fn(jack_nframes_t nframes, void *arg)
{
return ((JackHostImpl *)arg)->OnProcess(nframes);
}
void OnShutdown()
{
Lv2Log::info("Jack Audio Server has shut down.");
}
static void
jack_shutdown_fn(void *arg)
{
((JackHostImpl *)arg)->OnShutdown();
}
void OnSessionCallback(jack_session_event_t *event)
{
#if JACK_SESSION_CALLBACK // deprecated and not actually useful.
char retval[100];
Lv2Log::info("path %s, uuid %s, type: %s\n", event->session_dir, event->client_uuid, event->type == JackSessionSave ? "save" : "quit");
snprintf(retval, 100, "jack_simple_session_client %s", event->client_uuid);
event->command_line = strdup(retval);
jack_session_reply(client, event);
if (event->type == JackSessionSaveAndQuit)
{
// simple_quit = 1;
}
jack_session_event_free(event);
#endif
}
static void jack_error_fn(const char *msg)
{
Lv2Log::error("Jack - %s", msg);
}
static void jack_info_fn(const char *msg)
{
Lv2Log::info("Jack - %s", msg);
}
static void
session_callback_fn(jack_session_event_t *event, void *arg)
{
((JackHostImpl *)arg)->OnSessionCallback(event);
};
Lv2EventBufferUrids eventBufferUrids;
public: public:
JackHostImpl(IHost *pHost) JackHostImpl(IHost *pHost)
: inputRingBuffer(RING_BUFFER_SIZE), : inputRingBuffer(RING_BUFFER_SIZE),
@@ -691,22 +588,29 @@ public:
hostWriter(&this->inputRingBuffer), hostWriter(&this->inputRingBuffer),
eventBufferUrids(pHost) eventBufferUrids(pHost)
{ {
jack_set_error_function(jack_error_fn);
jack_set_info_function(jack_info_fn); #if JACK_HOST
audioDriver = CreateJackDriver(this);
#endif
#if ALSA_HOST
audioDriver = CreateAlsaDriver(this);
#endif
} }
virtual ~JackHostImpl() virtual ~JackHostImpl()
{ {
Close(); Close();
CleanRestartThreads(true); CleanRestartThreads(true);
jack_set_error_function(nullptr);
jack_set_info_function(nullptr); delete audioDriver;
} }
virtual JackConfiguration GetServerConfiguration() virtual JackConfiguration GetServerConfiguration()
{ {
JackConfiguration result; JackConfiguration result;
result.Initialize(); result.JackInitialize();
return result; return result;
} }
@@ -980,14 +884,13 @@ public:
return result; return result;
} }
static int jackInstanceId;
virtual void Open(const JackChannelSelection &channelSelection) virtual void Open(const JackServerSettings &jackServerSettings,const JackChannelSelection &channelSelection)
{ {
std::lock_guard guard(mutex); std::lock_guard guard(mutex);
if (channelSelection.GetInputAudioPorts().size() == 0 if (channelSelection.GetInputAudioPorts().size() == 0
|| channelSelection.getOutputAudioPorts().size() == 0) || channelSelection.GetOutputAudioPorts().size() == 0)
{ {
return; return;
} }
@@ -1008,162 +911,32 @@ public:
StartReaderThread(); StartReaderThread();
jack_status_t status; try {
try
{
// need a unique instance name every timme.
std::stringstream s;
s << "PiPedal";
if (jackInstanceId != 0) {
s << jackInstanceId;
}
++jackInstanceId;
std::string instanceName = s.str(); audioDriver->Open(jackServerSettings,this->channelSelection);
client = jack_client_open(instanceName.c_str(), JackNullOption, &status); this->sampleRate = audioDriver->GetSampleRate();
if (client == nullptr || status & JackFailure)
{
if (client)
{
jack_client_close(client);
client = nullptr;
}
std::string error = GetJackErrorMessage(status);
Lv2Log::error(error);
throw PiPedalStateException(error.c_str());
}
if (status & JackServerStarted)
{
Lv2Log::info("Jack server started.");
}
jack_set_process_callback(client, process_fn, this);
jack_on_shutdown(client, jack_shutdown_fn, this);
#if JACK_SESSION_CALLBACK
jack_set_session_callback(client, session_callback_fn, NULL);
#endif
jack_set_xrun_callback(client, xrun_callback_fn, this);
this->sampleRate = jack_get_sample_rate(client);
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); this->vuSamplesPerUpdate = (size_t)(sampleRate * VU_UPDATE_RATE_S);
auto &selectedInputPorts = channelSelection.GetInputAudioPorts();
this->inputPorts.clear();
this->outputPorts.clear();
this->midiInputPorts.clear();
this->inputPorts.resize(selectedInputPorts.size()); midiLv2Buffers.resize(audioDriver->MidiInputBufferCount());
for (int i = 0; i < selectedInputPorts.size(); ++i) for (size_t i = 0; i < audioDriver->MidiInputBufferCount(); ++i)
{
std::stringstream name;
name << "input" << (i + 1);
this->inputPorts[i] =
jack_port_register(
client, name.str().c_str(),
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsInput, 0);
if (this->inputPorts[i] == nullptr)
{
Lv2Log::error("Can't allocate Jack Audio input ports.");
throw PiPedalStateException("Failed to allocate Jack Audio port.");
}
}
auto &selectedOutputPorts = channelSelection.getOutputAudioPorts();
this->outputPorts.resize(selectedOutputPorts.size());
for (int i = 0; i < selectedOutputPorts.size(); ++i)
{
std::stringstream name;
name << "output" << (i + 1);
this->outputPorts[i] =
jack_port_register(client, name.str().c_str(),
JACK_DEFAULT_AUDIO_TYPE,
JackPortIsOutput, 0);
if (this->outputPorts[i] == nullptr)
{
Lv2Log::error("Can't allocate Jack Audio output port.");
throw PiPedalStateException("Failed to allocate Jack Audio port.");
}
}
auto &selectedMidiPorts = channelSelection.GetInputMidiPorts();
midiLv2Buffers.resize(selectedMidiPorts.size());
for (size_t i = 0; i < selectedMidiPorts.size(); ++i)
{ {
midiLv2Buffers[i] = AllocateRealtimeBuffer(MIDI_LV2_BUFFER_SIZE); midiLv2Buffers[i] = AllocateRealtimeBuffer(MIDI_LV2_BUFFER_SIZE);
} }
this->midiInputPorts.resize(selectedMidiPorts.size());
for (int i = 0; i < selectedMidiPorts.size(); ++i)
{
std::stringstream name;
name << "midiIn" << (i + 1);
this->midiInputPorts[i] =
jack_port_register(client, name.str().c_str(),
JACK_DEFAULT_MIDI_TYPE,
JackPortIsInput, 0);
if (this->midiInputPorts[i] == nullptr)
{
std::stringstream s;
s << "can't register Jack port " << name.str().c_str();
Lv2Log::error(s.str());
throw PiPedalStateException(s.str().c_str());
}
}
int activateResult = jack_activate(client); active = true;
if (activateResult == 0) audioDriver->Activate();
{ Lv2Log::info("Audio started.");
active = true;
for (int i = 0; i < selectedInputPorts.size(); ++i)
{
auto result = jack_connect(client, selectedInputPorts[i].c_str(), jack_port_name(this->inputPorts[i]));
if (result)
{
Lv2Log::error("Can't connect input port %s", selectedInputPorts[i].c_str());
throw PiPedalStateException("Jack Audio port connection failed.");
}
}
for (int i = 0; i < selectedOutputPorts.size(); ++i)
{
auto result = jack_connect(client, jack_port_name(this->outputPorts[i]), selectedOutputPorts[i].c_str());
if (result)
{
Lv2Log::error("Can't connect output port %s", selectedOutputPorts[i].c_str());
throw PiPedalStateException("Jack Audio port connection failed.");
}
}
for (int i = 0; i < selectedMidiPorts.size(); ++i)
{
auto result = jack_connect(client, selectedMidiPorts[i].c_str(), jack_port_name(this->midiInputPorts[i]));
if (result)
{
Lv2Log::error("Can't connect midi input port %s", selectedMidiPorts[i].c_str());
throw PiPedalStateException("Jack Audio midi port connection failed.");
}
}
Lv2Log::info("Jack configuration complete.");
}
else
{
Lv2Log::error("Failed to activate Jack Audio client. (%d)", (int)activateResult);
throw PiPedalStateException("Failed to activate Jack Audio client.");
}
} }
catch (PiPedalException &e) catch (PiPedalException &e)
{ {
Close(); Close();
active = false;
throw; throw;
} }
} }
@@ -1366,6 +1139,8 @@ private:
AdminClient client; AdminClient client;
client.SetJackServerConfiguration(jackServerSettings); client.SetJackServerConfiguration(jackServerSettings);
//this_->Open(this_->channelSelection); //this_->Open(this_->channelSelection);
this_->restarting = false; this_->restarting = false;
onComplete(true, ""); onComplete(true, "");
@@ -1468,16 +1243,12 @@ public:
result.temperaturemC_ = GetRaspberryPiTemperature(); result.temperaturemC_ = GetRaspberryPiTemperature();
result.active_ = this->active; result.active_ = IsAudioActive();
result.restarting_ = this->restarting; result.restarting_ = this->restarting;
if (client != nullptr) if (this->audioDriver != nullptr)
{ {
result.cpuUsage_ = jack_cpu_load(this->client); result.cpuUsage_ = audioDriver->CpuUse();
}
else
{
result.cpuUsage_ = 0;
} }
GetCpuFrequency(&result.cpuFreqMax_,&result.cpuFreqMin_); GetCpuFrequency(&result.cpuFreqMax_,&result.cpuFreqMin_);
result.governor_ = GetGovernor(); result.governor_ = GetGovernor();
@@ -1497,7 +1268,6 @@ public:
} }
}; };
int JackHostImpl::jackInstanceId = 0;
JackHost *JackHost::CreateInstance(IHost *pHost) JackHost *JackHost::CreateInstance(IHost *pHost)
{ {
@@ -1506,6 +1276,7 @@ JackHost *JackHost::CreateInstance(IHost *pHost)
JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_BEGIN(JackHostStatus)
JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, active)
JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
JSON_MAP_REFERENCE(JackHostStatus, restarting) JSON_MAP_REFERENCE(JackHostStatus, restarting)
JSON_MAP_REFERENCE(JackHostStatus, underruns) JSON_MAP_REFERENCE(JackHostStatus, underruns)
JSON_MAP_REFERENCE(JackHostStatus, cpuUsage) JSON_MAP_REFERENCE(JackHostStatus, cpuUsage)
+2 -1
View File
@@ -106,6 +106,7 @@ public:
class JackHostStatus { class JackHostStatus {
public: public:
bool active_; bool active_;
std::string errorMessage_;
bool restarting_; bool restarting_;
uint64_t underruns_; uint64_t underruns_;
float cpuUsage_ = 0; float cpuUsage_ = 0;
@@ -139,7 +140,7 @@ public:
virtual void SetListenForAtomOutput(bool listen) = 0; virtual void SetListenForAtomOutput(bool listen) = 0;
virtual void Open(const JackChannelSelection & channelSelection) = 0; virtual void Open(const JackServerSettings&jackServerSettings,const JackChannelSelection & channelSelection) = 0;
virtual void Close() = 0; virtual void Close() = 0;
virtual uint32_t GetSampleRate() = 0; virtual uint32_t GetSampleRate() = 0;
+4
View File
@@ -157,6 +157,7 @@ void JackServerSettings::ReadJackConfiguration()
void JackServerSettings::Write() void JackServerSettings::Write()
{ {
#if JACK_HOST
this->valid_ = false; this->valid_ = false;
std::vector<std::string> precedingLines; std::vector<std::string> precedingLines;
@@ -232,6 +233,9 @@ void JackServerSettings::Write()
catch (const std::exception &e) catch (const std::exception &e)
{ {
} }
#else
throw PiPedalStateException("JACK_HOST not enabled at compile time.");
#endif
} }
JSON_MAP_BEGIN(JackServerSettings) JSON_MAP_BEGIN(JackServerSettings)
+9
View File
@@ -33,6 +33,15 @@ namespace pipedal {
public: public:
JackServerSettings(); JackServerSettings();
JackServerSettings(const std::string alsaDevice, uint64_t sampleRate, uint32_t bufferSize, uint32_t numberOfBuffers)
: valid_(true),
alsaDevice_(alsaDevice),
sampleRate_(sampleRate),
bufferSize_(bufferSize),
numberOfBuffers_(numberOfBuffers)
{
}
uint64_t GetSampleRate() const { return sampleRate_; } uint64_t GetSampleRate() const { return sampleRate_; }
uint32_t GetBufferSize() const { return bufferSize_; } uint32_t GetBufferSize() const { return bufferSize_; }
+1 -1
View File
@@ -266,7 +266,7 @@ void Lv2Host::OnConfigurationChanged(const JackConfiguration &configuration, con
if (configuration.isValid()) if (configuration.isValid())
{ {
this->numberOfAudioInputChannels = settings.GetInputAudioPorts().size(); this->numberOfAudioInputChannels = settings.GetInputAudioPorts().size();
this->numberOfAudioOutputChannels = settings.getOutputAudioPorts().size(); this->numberOfAudioOutputChannels = settings.GetOutputAudioPorts().size();
this->maxBufferSize = configuration.GetBlockLength(); this->maxBufferSize = configuration.GetBlockLength();
optionsFeature.Prepare(this->mapFeature, configuration.GetSampleRate(), configuration.GetBlockLength(), GetAtomBufferSize()); optionsFeature.Prepare(this->mapFeature, configuration.GetSampleRate(), configuration.GetBlockLength(), GetAtomBufferSize());
} }
+8 -5
View File
@@ -31,9 +31,12 @@ using namespace pipedal;
static auto timeZero = std::chrono::system_clock::now(); static auto timeZero = std::chrono::system_clock::now();
bool Lv2Log::show_time_ = true;
std::string timeTag() std::string timeTag()
{ {
if (!Lv2Log::show_time()) return "";
using namespace std::chrono; using namespace std::chrono;
auto t = std::chrono::system_clock::now()- timeZero; auto t = std::chrono::system_clock::now()- timeZero;
@@ -47,7 +50,7 @@ std::string timeTag()
std::stringstream s; std::stringstream s;
using namespace std; using namespace std;
s << setfill('0') << setw(2) << hours_ << ':' << setw(2) << minutes_ << ':' << setw(2) << seconds_ << "." << setw(3) << milliseconds_; s << setfill('0') << setw(2) << hours_ << ':' << setw(2) << minutes_ << ':' << setw(2) << seconds_ << "." << setw(3) << milliseconds_ << " ";
return s.str(); return s.str();
} }
@@ -61,23 +64,23 @@ class StdErrLogger : public Lv2Logger
virtual void onError(const char* message) virtual void onError(const char* message)
{ {
std::lock_guard lock(m); std::lock_guard lock(m);
std::cerr << timeTag() << " ERR: " << message << std::endl; std::cerr << timeTag() << "ERR: " << message << std::endl;
} }
virtual void onWarning(const char* message) virtual void onWarning(const char* message)
{ {
std::lock_guard lock(m); std::lock_guard lock(m);
std::cerr << timeTag() << " WRN: " << message << std::endl; std::cerr << timeTag() << "WRN: " << message << std::endl;
} }
virtual void onDebug(const char* message) virtual void onDebug(const char* message)
{ {
std::lock_guard lock(m); std::lock_guard lock(m);
std::cerr << timeTag() << " DBG: " << message << std::endl; std::cerr << timeTag() << "DBG: " << message << std::endl;
} }
virtual void onInfo(const char* message) virtual void onInfo(const char* message)
{ {
std::lock_guard lock(m); std::lock_guard lock(m);
std::cerr << timeTag() << " INF: " << message << std::endl; std::cerr << timeTag() << "INF: " << message << std::endl;
} }
}; };
+9
View File
@@ -47,8 +47,10 @@ namespace pipedal
class Lv2Log class Lv2Log
{ {
private: private:
static Lv2Logger *logger_; static Lv2Logger *logger_;
static LogLevel log_level_; static LogLevel log_level_;
static bool show_time_;
static void v_error(const char *format, va_list arglist); static void v_error(const char *format, va_list arglist);
static void v_warning(const char *format, va_list arglist); static void v_warning(const char *format, va_list arglist);
@@ -68,6 +70,13 @@ namespace pipedal
{ {
return Lv2Log::log_level_; return Lv2Log::log_level_;
} }
static void show_time(bool show)
{
show_time_ = show;
}
static bool show_time() {
return show_time_;
}
// static void error(const char* message) { // static void error(const char* message) {
// if (Lv2Log::log_level_ >= LogLevel::Error) // if (Lv2Log::log_level_ >= LogLevel::Error)
+474
View File
@@ -0,0 +1,474 @@
/*
* MIT License
*
* Copyright (c) 2022 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.
*/
#include "CommandLineParser.hpp"
#include "PrettyPrinter.hpp"
#include <iostream>
#include "PiPedalAlsa.hpp"
#include "Lv2Log.hpp"
#include <mutex>
#include "AlsaDriver.hpp"
#include <iomanip>
#include <chrono>
using namespace pipedal;
constexpr int E_NOSIGNAL = 1;
constexpr int E_OPEN_FAILURE = 2;
constexpr int E_XRUN = 3;
constexpr uint64_t NO_SIGNAL_VALUE = 0x7000000;
struct TestResult {
int error = 0;
uint64_t latency = 0;
float cpuOverhead = 0;
};
void PrintHelp()
{
PrettyPrinter pp;
pp.width(160);
pp << "PiPedal Latency Tester\n";
pp << "Copyright (c) 2022 Robin Davies\n";
pp << "\n";
pp << Indent(0) << "Syntax\n\n";
pp << Indent(4) << "pipedal_latency_test [<options>] <device-name>\n\n";
pp << Indent(0) << "Options\n\n";
pp << Indent(15);
pp << HangingIndent() << " -l --list\t"
<< "List available devices.\n\n";
pp << HangingIndent() << " -r --rate\t"
<< "Sample rate (default 48000).\n\n";
pp << HangingIndent() << " -h --help\t"
<< "Display this message.\n\n";
pp << Indent(0) << "Remarks\n\n";
pp << Indent(4);
pp << "PiPedal Latency Tester measure actual audio latency from output to input of an ALSA device. "
<< "To run a latency test, you must connect an audio cable from left (first) output of the device "
"under test to the left (first) input of the device under test.\n\n"
<< "PiPedal Latency Tester will measure the time it takes for a signal to propagate from output to input.\n\n"
<< "The tests run over a variety of buffer sizes. A nominal compute load is provided in order to put some "
"stress on the audio system.\n\n"
<< "You may need to stop the pipedald audio service in order to access the ALSA device:\n\n"
<< Indent(8) << "sudo systemctl stop pipedald\n\n";
pp << Indent(0) << "Examples\n\n";
pp << Indent(4) << "pipedal_latency_test --list\n\n";
pp << Indent(4) << "pipedal_latency_test M2\n\n";
}
void ListDevices()
{
PiPedalAlsaDevices alsaDevices;
auto devices = alsaDevices.GetAlsaDevices();
PrettyPrinter pp;
if (devices.size() == 0)
{
pp << "No devices found.\n";
}
else
{
pp << Indent(0);
pp << "Alsa Devices\n";
pp << Indent(15);
for (auto &device : devices)
{
pp << HangingIndent() << (device.id_) << "\t"
<< device.longName_ << "\n";
}
}
}
class AlsaTester : private AudioDriverHost
{
public:
enum class TestType
{
Oscillator,
LatencyMonitor,
NullTest
};
private:
AudioDriver *audioDriver = nullptr;
const std::string &deviceId;
uint32_t sampleRate;
int bufferSize;
int buffers;
public:
AlsaTester(const std::string &deviceId, uint32_t sampleRate, int bufferSize, int buffers)
: deviceId(deviceId),
sampleRate(sampleRate),
bufferSize(bufferSize),
buffers(buffers)
{
}
~AlsaTester()
{
delete audioDriver;
delete[] inputBuffers;
delete[] outputBuffers;
}
TestResult Test()
{
TestResult result;
try
{
JackServerSettings serverSettings(deviceId, sampleRate, bufferSize, buffers);
JackConfiguration jackConfiguration;
jackConfiguration.AlsaInitialize(serverSettings);
JackChannelSelection channelSelection(
jackConfiguration.GetInputAudioPorts(),
jackConfiguration.GetOutputAudioPorts(),
std::vector<AlsaMidiDeviceInfo>());
audioDriver = CreateAlsaDriver(this);
latencyMonitor.Init(jackConfiguration.GetSampleRate());
audioDriver->Open(serverSettings, channelSelection);
inputBuffers = new float *[channelSelection.GetInputAudioPorts().size()];
outputBuffers = new float *[channelSelection.GetOutputAudioPorts().size()];
audioDriver->Activate();
sleep(3); // let audio stabilize.
this->SetXruns(0);
sleep(7); // run for a bit.
audioDriver->Deactivate();
audioDriver->Close();
if (this->GetXruns() != 0)
{
result.error = E_XRUN;
return result;
}
result.latency = latencyMonitor.GetLatency();
if (result.latency == NO_SIGNAL_VALUE)
{
result.error = E_NOSIGNAL;
}
result.cpuOverhead = audioDriver->CpuOverhead();
}
catch (const std::exception &e)
{
result.error = E_OPEN_FAILURE;
}
return result;
}
float **inputBuffers = nullptr;
float **outputBuffers = nullptr;
class Oscillator
{
private:
double dx = 0;
double x = 0;
double dx2 = 0;
double x2 = 0;
public:
void Init(float frequency, size_t sampleRate)
{
dx = frequency * 3.141592736 * 2 / sampleRate;
dx2 = 0.5 * 3.141592736 * 2 / sampleRate;
}
float Next()
{
float result = (float)std::cos(x);
float env = (float)std::cos(x2);
x += dx;
x2 += dx2;
return result * env;
}
};
class LatencyMonitor
{
enum class State
{
Idle,
Waiting,
};
State state = State::Idle;
uint64_t t;
uint64_t idle_samples;
uint64_t waiting_samples;
size_t current_latency = 0;
size_t latency = 0;
std::mutex sync;
public:
void Init(uint64_t sampleRate)
{
idle_samples = (uint64_t)(sampleRate * 0.5);
waiting_samples = (uint64_t)(sampleRate * 0.5);
state = State::Idle;
t = idle_samples;
latency = 0;
}
void StartTest()
{
}
size_t GetLatency()
{
std::lock_guard lock{sync};
return latency;
}
float Next(float input)
{
switch (state)
{
default:
case State::Idle:
{
if (t-- == 0)
{
state = State::Waiting;
current_latency = 0;
}
return 0.01;
}
break;
case State::Waiting:
{
if (std::abs(input) > 0.1 || current_latency >= 2000)
{
{
std::lock_guard lock{sync};
if (latency >= 2000)
{
latency = NO_SIGNAL_VALUE;
}
else
{
latency = current_latency;
}
}
state = State::Idle;
t = idle_samples;
}
else
{
++current_latency;
}
return current_latency < 100 ? 0.25 : 0.0;
}
break;
}
}
};
Oscillator oscillator;
LatencyMonitor latencyMonitor;
virtual void OnAudioStopped()
{
}
virtual void OnProcess(size_t nFrames)
{
size_t inputs = audioDriver->InputBufferCount();
size_t outputs = audioDriver->OutputBufferCount();
for (size_t i = 0; i < inputs; ++i)
{
inputBuffers[i] = audioDriver->GetInputBuffer(i, nFrames);
}
for (size_t i = 0; i < outputs; ++i)
{
outputBuffers[i] = audioDriver->GetOutputBuffer(i, nFrames);
}
for (size_t i = 0; i < nFrames; ++i)
{
float v = latencyMonitor.Next(inputBuffers[0][i]);
for (size_t c = 0; c < outputs; ++c)
{
outputBuffers[c][i] = v;
}
}
}
std::mutex sync;
uint64_t xruns;
uint64_t GetXruns()
{
lock_guard lock{sync};
return xruns;
}
void SetXruns(uint64_t value)
{
lock_guard lock{sync};
xruns = value;
}
virtual void OnUnderrun()
{
lock_guard lock{sync};
++xruns;
}
};
TestResult RunLatencyTest(const std::string deviceId, uint32_t sampleRate, int bufferSize, int buffers)
{
AlsaTester tester(deviceId, sampleRate, bufferSize, buffers);
return tester.Test();
}
static std::string msDisplay(float value)
{
std::stringstream s;
s << fixed;
s.precision(1);
s << value << "ms";
return s.str();
}
static std::string overheadDisplay(float value)
{
std::stringstream s;
s << setw(3) << value << "%";
return s.str();
}
void RunLatencyTest(const std::string &deviceId, uint32_t sampleRate)
{
PrettyPrinter pp;
pp << "Device: " << deviceId << " Rate: " << sampleRate << "\n\n";
const int SIZE_COLUMN_WIDTH = 8;
const int BUFFERS_COLUMN_WIDTH = 20;
static int bufferCounts[] = {2, 3, 4};
static int bufferSizes[] = {16, 24, 32, 48, 64, 128};
pp << Column(SIZE_COLUMN_WIDTH) << "Buffers\n";
pp << "Size";
int column = SIZE_COLUMN_WIDTH;
for (auto bufferCount : bufferCounts)
{
pp << Column(column) << bufferCount;
column += BUFFERS_COLUMN_WIDTH;
}
pp << "\n";
for (auto bufferSize : bufferSizes)
{
pp << bufferSize;
int column = SIZE_COLUMN_WIDTH;
for (auto bufferCount : bufferCounts)
{
auto result = RunLatencyTest(deviceId, sampleRate, bufferSize, bufferCount);
pp.Column(column);
column += BUFFERS_COLUMN_WIDTH;
switch (result.error)
{
case E_NOSIGNAL:
pp << "No signal";
break;
case E_OPEN_FAILURE:
pp << "Failed";
break;
case E_XRUN:
pp << "Xrun";
break;
default:
{
float ms = 1000.0f * result.latency / sampleRate;
pp << result.latency << "/" << msDisplay(ms) ;
break;
}
}
}
pp << "\n";
}
}
int main(int argc, const char **argv)
{
Lv2Log::log_level(LogLevel::Warning);
CommandLineParser parser;
std::string deviceName;
bool listDevices = false;
bool help = false;
uint32_t sampleRate = 48000;
parser.AddOption("l", "list", &listDevices);
parser.AddOption("h", "help", &help);
parser.AddOption("r", "rate", &sampleRate);
try
{
parser.Parse(argc, argv);
if (help)
{
PrintHelp();
}
else if (listDevices)
{
ListDevices();
}
else if (parser.Arguments().size() == 1)
{
RunLatencyTest(parser.Arguments()[0], sampleRate);
}
else
{
PrintHelp();
}
}
catch (std::exception &e)
{
cout << "Error: " << e.what() << endl;
return 1;
}
return 0;
}
+95
View File
@@ -167,9 +167,99 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
} }
} }
snd_config_update_free_global(); snd_config_update_free_global();
Lv2Log::debug("GetAlsaDevices --");
for (auto& device: result)
{
Lv2Log::debug(SS(" " << device.name_ << " " << device.longName_ << " " << device.cardId_));
}
return result; return result;
} }
static std::vector<AlsaMidiDeviceInfo> GetAlsaDevices(const char*devname, const char*direction)
{
std::vector<AlsaMidiDeviceInfo> result;
{
char **hints;
int err;
char **n;
char *name;
char *desc;
char *ioid;
/* Enumerate sound devices */
err = snd_device_name_hint(-1, devname, (void ***)&hints);
if (err != 0)
{
return result;
}
n = hints;
while (*n != NULL)
{
name = snd_device_name_get_hint(*n, "NAME");
desc = snd_device_name_get_hint(*n, "DESC");
ioid = snd_device_name_get_hint(*n, "IOID");
if (desc != nullptr) // skip virtual device
{
if (ioid == nullptr || strcmp(ioid,direction) == 0)
{
result.push_back(AlsaMidiDeviceInfo(name,desc));
}
}
if (name && strcmp("null", name) != 0)
free(name);
if (desc && strcmp("null", desc) != 0)
free(desc);
if (ioid && strcmp("null", ioid) != 0)
free(ioid);
n++;
}
// Free hint buffer too
snd_device_name_free_hint((void **)hints);
}
return result;
}
std::vector<AlsaMidiDeviceInfo> pipedal::GetAlsaMidiInputDevices()
{
return GetAlsaDevices("rawmidi","Input");
}
std::vector<AlsaMidiDeviceInfo> pipedal::GetAlsaMidiOutputDevices()
{
return GetAlsaDevices("rawmidi","Output");
}
AlsaMidiDeviceInfo::AlsaMidiDeviceInfo(const char*name, const char*description)
:name_(name)
{
// extract just the display name from description.
// undocumented but e.g.: M2, M2\nM2 Raw Midi
const char *p = description;
const char *pEnd = p;
// undocumented but e.g.: M2, M2\nM2 Raw Midi
while (pEnd != nullptr && *pEnd != 0
&& *pEnd != ','
&& *pEnd != '\n'
)
{
++pEnd;
}
if (p != pEnd)
{
description_ = std::string(p,pEnd);
} else {
description = name;
}
}
JSON_MAP_BEGIN(AlsaDeviceInfo) JSON_MAP_BEGIN(AlsaDeviceInfo)
JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId) JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId)
JSON_MAP_REFERENCE(AlsaDeviceInfo, id) JSON_MAP_REFERENCE(AlsaDeviceInfo, id)
@@ -179,3 +269,8 @@ JSON_MAP_REFERENCE(AlsaDeviceInfo, sampleRates)
JSON_MAP_REFERENCE(AlsaDeviceInfo, minBufferSize) JSON_MAP_REFERENCE(AlsaDeviceInfo, minBufferSize)
JSON_MAP_REFERENCE(AlsaDeviceInfo, maxBufferSize) JSON_MAP_REFERENCE(AlsaDeviceInfo, maxBufferSize)
JSON_MAP_END() JSON_MAP_END()
JSON_MAP_BEGIN(AlsaMidiDeviceInfo)
JSON_MAP_REFERENCE(AlsaMidiDeviceInfo, name)
JSON_MAP_REFERENCE(AlsaMidiDeviceInfo, description)
JSON_MAP_END()
+12
View File
@@ -33,6 +33,16 @@ namespace pipedal {
DECLARE_JSON_MAP(AlsaDeviceInfo); DECLARE_JSON_MAP(AlsaDeviceInfo);
};
class AlsaMidiDeviceInfo {
public:
AlsaMidiDeviceInfo() { }
AlsaMidiDeviceInfo(const char*name, const char*description);
std::string name_;
std::string description_;
DECLARE_JSON_MAP(AlsaMidiDeviceInfo);
}; };
class PiPedalAlsaDevices { class PiPedalAlsaDevices {
@@ -45,4 +55,6 @@ namespace pipedal {
std::vector<AlsaDeviceInfo> GetAlsaDevices(); std::vector<AlsaDeviceInfo> GetAlsaDevices();
}; };
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiInputDevices();
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiOutputDevices();
} }
+8 -1
View File
@@ -32,7 +32,14 @@ TEST_CASE( "ALSA Test", "[pipedal_alsa_test][Build][Dev]" ) {
PiPedalAlsaDevices devices; PiPedalAlsaDevices devices;
auto result = devices.GetAlsaDevices(); auto result = devices.GetAlsaDevices();
std::cout << result.size() << " ALSA devices found." << 0; std::cout << result.size() << " ALSA devices found." << std::endl;
auto midiInputDevices = GetAlsaMidiInputDevices();
std::cout << midiInputDevices.size() << " ALSA MIDI input devices found." << std::endl;
auto midiOutputDevices = GetAlsaMidiOutputDevices();
std::cout << midiOutputDevices.size() << " ALSA MIDI output devices found." << std::endl;
} }
+4
View File
@@ -57,6 +57,10 @@ namespace pipedal
: PiPedalException(std::forward<std::string>(what)) : PiPedalException(std::forward<std::string>(what))
{ {
} }
PiPedalStateException(const std::string &what)
: PiPedalException(what)
{
}
}; };
class PiPedalLogicException : public PiPedalException class PiPedalLogicException : public PiPedalException
{ {
+42 -10
View File
@@ -19,6 +19,7 @@
#include "pch.h" #include "pch.h"
#include "DeviceIdFile.hpp" #include "DeviceIdFile.hpp"
#include "AudioConfig.hpp"
#include <sched.h> #include <sched.h>
#include "PiPedalModel.hpp" #include "PiPedalModel.hpp"
#include "JackHost.hpp" #include "JackHost.hpp"
@@ -49,7 +50,12 @@ std::string AtomToJson(int length, uint8_t *pData)
PiPedalModel::PiPedalModel() PiPedalModel::PiPedalModel()
{ {
this->pedalBoard = PedalBoard::MakeDefault(); this->pedalBoard = PedalBoard::MakeDefault();
#if JACK_HOST
this->jackServerSettings.ReadJackConfiguration(); this->jackServerSettings.ReadJackConfiguration();
#else
this->jackServerSettings = this->storage.GetJackServerSettings();
#endif
} }
@@ -108,14 +114,19 @@ PiPedalModel::~PiPedalModel()
#include <fstream> #include <fstream>
void PiPedalModel::Init(const PiPedalConfiguration&configuration)
void PiPedalModel::LoadLv2PluginInfo(const PiPedalConfiguration&configuration)
{ {
this->configuration = configuration;
storage.SetConfigRoot(configuration.GetDocRoot()); storage.SetConfigRoot(configuration.GetDocRoot());
storage.SetDataRoot(configuration.GetLocalStoragePath()); storage.SetDataRoot(configuration.GetLocalStoragePath());
storage.Initialize(); storage.Initialize();
this->jackServerSettings.ReadJackConfiguration();
}
void PiPedalModel::LoadLv2PluginInfo()
{
// Not all Lv2 directories have the lv2 base declarations. Load a full set of plugin classes generated on a default /usr/local/lib/lv2 directory. // Not all Lv2 directories have the lv2 base declarations. Load a full set of plugin classes generated on a default /usr/local/lib/lv2 directory.
std::filesystem::path pluginClassesPath = configuration.GetDocRoot() / "plugin_classes.json"; std::filesystem::path pluginClassesPath = configuration.GetDocRoot() / "plugin_classes.json";
try try
@@ -149,11 +160,10 @@ void PiPedalModel::LoadLv2PluginInfo(const PiPedalConfiguration&configuration)
} }
} }
void PiPedalModel::Load(const PiPedalConfiguration &configuration) void PiPedalModel::Load()
{ {
this->webRoot = configuration.GetWebRoot(); this->webRoot = configuration.GetWebRoot();
this->webPort = (uint16_t)configuration.GetSocketServerPort(); this->webPort = (uint16_t)configuration.GetSocketServerPort();
this->jackServerSettings.ReadJackConfiguration();
adminClient.MonitorGovernor(storage.GetGovernorSettings()); adminClient.MonitorGovernor(storage.GetGovernorSettings());
@@ -192,8 +202,12 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
scheduler_params.sched_priority = 10; scheduler_params.sched_priority = 10;
memset(&scheduler_params,0,sizeof(sched_param)); memset(&scheduler_params,0,sizeof(sched_param));
sched_setscheduler(0,SCHED_RR,&scheduler_params); sched_setscheduler(0,SCHED_RR,&scheduler_params);
#if JACK_HOST
this->jackConfiguration = jackHost->GetServerConfiguration(); this->jackConfiguration = this->jackConfiguration.JackInitialize();
#else
this->jackServerSettings = storage.GetJackServerSettings();
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
#endif
if (this->jackConfiguration.isValid()) if (this->jackConfiguration.isValid())
{ {
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration); JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
@@ -202,7 +216,7 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
this->lv2Host.OnConfigurationChanged(jackConfiguration, selection); this->lv2Host.OnConfigurationChanged(jackConfiguration, selection);
try try
{ {
jackHost->Open(selection); jackHost->Open(this->jackServerSettings,selection);
std::shared_ptr<Lv2PedalBoard> lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; std::shared_ptr<Lv2PedalBoard> lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)};
this->lv2PedalBoard = lv2PedalBoard; this->lv2PedalBoard = lv2PedalBoard;
@@ -212,6 +226,8 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
{ {
Lv2Log::error("Failed to load initial plugin. (%s)", e.what()); Lv2Log::error("Failed to load initial plugin. (%s)", e.what());
} }
} else {
Lv2Log::error("Audio device not configured.");
} }
} }
@@ -780,15 +796,27 @@ void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSe
this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection); this->lv2Host.OnConfigurationChanged(jackConfiguration, channelSelection);
if (this->jackHost->IsOpen()) if (this->jackHost->IsOpen())
{ {
// do a complete reload.
this->jackHost->Close(); this->jackHost->Close();
#ifdef JUNK
// restarting is a bit dodgy. It was impossible with Jack, but
// now very plausible with the ALSA audio stack.
// Still bugs wrt/ restarting the circular buffers for the audio thread.
// for the meantime, just rely on the fact that the admin service will restart
// the process.
//...
// do a complete reload.
this->jackHost->SetPedalBoard(nullptr); this->jackHost->SetPedalBoard(nullptr);
this->jackHost->Open(channelSelection); this->jackHost->Open(this->jackServerSettings,channelSelection);
std::shared_ptr<Lv2PedalBoard> lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; std::shared_ptr<Lv2PedalBoard> lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)};
this->lv2PedalBoard = lv2PedalBoard; this->lv2PedalBoard = lv2PedalBoard;
jackHost->SetPedalBoard(lv2PedalBoard); jackHost->SetPedalBoard(lv2PedalBoard);
this->UpdateRealtimeVuSubscriptions(); this->UpdateRealtimeVuSubscriptions();
#endif
} }
} }
@@ -1127,6 +1155,10 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet
if (adminClient.CanUseShutdownClient()) if (adminClient.CanUseShutdownClient())
{ {
#if ALSA_HOST
storage.SetJackServerSettings(jackServerSettings);
#endif
// save the current (edited) preset now in case the service shutdown isn't clean. // save the current (edited) preset now in case the service shutdown isn't clean.
CurrentPreset currentPreset; CurrentPreset currentPreset;
currentPreset.modified_ = this->hasPresetChanged; currentPreset.modified_ = this->hasPresetChanged;
+5 -2
View File
@@ -153,6 +153,8 @@ private: // IJackHostCallbacks
virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl); virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl);
virtual void OnNotifyAtomOutput(uint64_t instanceId, const std::string&atomType,const std::string&atomJson); virtual void OnNotifyAtomOutput(uint64_t instanceId, const std::string&atomType,const std::string&atomJson);
PiPedalConfiguration configuration;
public: public:
PiPedalModel(); PiPedalModel();
virtual ~PiPedalModel(); virtual ~PiPedalModel();
@@ -164,9 +166,10 @@ public:
AdminClient&GetAdminClient() { return adminClient; } AdminClient&GetAdminClient() { return adminClient; }
void Init(const PiPedalConfiguration&configuration);
void LoadLv2PluginInfo(const PiPedalConfiguration&configuration); void LoadLv2PluginInfo();
void Load(const PiPedalConfiguration&configuration); void Load();
const Lv2Host& GetLv2Host() const { return lv2Host; } const Lv2Host& GetLv2Host() const { return lv2Host; }
PedalBoard GetCurrentPedalBoardCopy() PedalBoard GetCurrentPedalBoardCopy()
+27
View File
@@ -52,6 +52,10 @@ namespace pipedal
{ {
lineBuffer.reserve(120); lineBuffer.reserve(120);
} }
size_t width() const { return lineWidth; }
void width(size_t width) { lineWidth =width; }
std::ostream&Stream() const { return s;} std::ostream&Stream() const { return s;}
PrettyPrinter& Indent(size_t indent) PrettyPrinter& Indent(size_t indent)
@@ -67,6 +71,21 @@ namespace pipedal
column = 0; column = 0;
} }
PrettyPrinter& Column(int n)
{
if (column >= n) {
lineBuffer.push_back(' ');
++column;
return *this;
}
while (column != n)
{
lineBuffer.push_back(' ');
++column;
}
return *this;
}
PrettyPrinter& HangingIndent() PrettyPrinter& HangingIndent()
{ {
hangingIndent = true; hangingIndent = true;
@@ -235,5 +254,13 @@ namespace pipedal
}; };
} }
pp_manip Column(int n)
{
return [n] (PrettyPrinter &pp) -> PrettyPrinter& {
pp.Column(n);
return pp;
};
}
} }
+151 -126
View File
@@ -19,6 +19,7 @@
#include "pch.h" #include "pch.h"
#include "Storage.hpp" #include "Storage.hpp"
#include "AudioConfig.hpp"
#include "PiPedalException.hpp" #include "PiPedalException.hpp"
#include <sstream> #include <sstream>
#include "json.hpp" #include "json.hpp"
@@ -35,8 +36,6 @@ const char *BANKS_FILENAME = "index.banks";
#define WIFI_CONFIG_SETTINGS_FILENAME "wifiConfigSettings.json"; #define WIFI_CONFIG_SETTINGS_FILENAME "wifiConfigSettings.json";
#define USER_SETTINGS_FILENAME "userSettings.json"; #define USER_SETTINGS_FILENAME "userSettings.json";
Storage::Storage() Storage::Storage()
{ {
SetConfigRoot("~/var/Config"); SetConfigRoot("~/var/Config");
@@ -131,7 +130,7 @@ std::string Storage::SafeEncodeName(const std::string &name)
return s.str(); return s.str();
} }
std::filesystem::path ResolveHomePath(const std::filesystem::path& path) std::filesystem::path ResolveHomePath(const std::filesystem::path &path)
{ {
if (path.begin() == path.end()) if (path.begin() == path.end())
return path; return path;
@@ -161,28 +160,27 @@ std::filesystem::path ResolveHomePath(const std::filesystem::path& path)
return result; return result;
} }
void Storage::SetConfigRoot(const std::filesystem::path& path) void Storage::SetConfigRoot(const std::filesystem::path &path)
{ {
this->configRoot = ResolveHomePath(path); this->configRoot = ResolveHomePath(path);
} }
void Storage::SetDataRoot(const std::filesystem::path& path) void Storage::SetDataRoot(const std::filesystem::path &path)
{ {
this->dataRoot = ResolveHomePath(path); this->dataRoot = ResolveHomePath(path);
} }
static void CopyDirectory(const std::filesystem::path &source, const std::filesystem::path &destination) static void CopyDirectory(const std::filesystem::path &source, const std::filesystem::path &destination)
{ {
for (auto &directoryEntry: std::filesystem::directory_iterator(source)) for (auto &directoryEntry : std::filesystem::directory_iterator(source))
{ {
if (directoryEntry.is_regular_file()) if (directoryEntry.is_regular_file())
{ {
std::filesystem::path sourceFile = directoryEntry.path(); std::filesystem::path sourceFile = directoryEntry.path();
std::filesystem::path destFile = destination / sourceFile.filename(); std::filesystem::path destFile = destination / sourceFile.filename();
std::filesystem::copy_file(sourceFile,destFile); std::filesystem::copy_file(sourceFile, destFile);
std::ignore = chmod(destFile.c_str(),S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); std::ignore = chmod(destFile.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
}
}
} }
} }
void Storage::MaybeCopyDefaultPresets() void Storage::MaybeCopyDefaultPresets()
@@ -192,18 +190,15 @@ void Storage::MaybeCopyDefaultPresets()
if (!std::filesystem::exists(presetsDirectory / "index.banks")) if (!std::filesystem::exists(presetsDirectory / "index.banks"))
{ {
CopyDirectory(this->configRoot / "default_presets" / "presets", CopyDirectory(this->configRoot / "default_presets" / "presets",
presetsDirectory presetsDirectory);
);
} }
auto pluginDirectory = this->GetPluginPresetsDirectory(); auto pluginDirectory = this->GetPluginPresetsDirectory();
if (!std::filesystem::exists(pluginDirectory / "index.json")) if (!std::filesystem::exists(pluginDirectory / "index.json"))
{ {
CopyDirectory(this->configRoot / "default_presets" / "plugin_presets", CopyDirectory(this->configRoot / "default_presets" / "plugin_presets",
pluginDirectory pluginDirectory);
);
} }
} }
void Storage::Initialize() void Storage::Initialize()
{ {
@@ -233,7 +228,6 @@ void Storage::Initialize()
LoadUserSettings(); LoadUserSettings();
} }
void Storage::LoadBank(int64_t instanceId) void Storage::LoadBank(int64_t instanceId)
{ {
auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId); auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId);
@@ -265,7 +259,6 @@ std::filesystem::path Storage::GetCurrentPresetPath() const
return this->dataRoot / "currentPreset.json"; return this->dataRoot / "currentPreset.json";
} }
std::filesystem::path Storage::GetChannelSelectionFileName() std::filesystem::path Storage::GetChannelSelectionFileName()
{ {
return this->dataRoot / "JackChannelSelection.json"; return this->dataRoot / "JackChannelSelection.json";
@@ -304,7 +297,7 @@ void Storage::LoadBankIndex()
std::string name = "Default Bank"; std::string name = "Default Bank";
SaveBankFile(name, currentBank); SaveBankFile(name, currentBank);
int64_t selectedBank = bankIndex.addBank(-1,name); int64_t selectedBank = bankIndex.addBank(-1, name);
bankIndex.selectedBank(selectedBank); bankIndex.selectedBank(selectedBank);
} }
SaveBankIndex(); SaveBankIndex();
@@ -365,7 +358,7 @@ void Storage::ReIndex()
if (path.extension() == BANK_EXTENSION) if (path.extension() == BANK_EXTENSION)
{ {
std::string name = SafeDecodeName(path.stem()); std::string name = SafeDecodeName(path.stem());
bankIndex.addBank(-1,name); bankIndex.addBank(-1, name);
} }
} }
} }
@@ -387,11 +380,12 @@ void Storage::CreateBank(const std::string &name)
int64_t instanceId = bankFile.presets()[0]->instanceId(); int64_t instanceId = bankFile.presets()[0]->instanceId();
bankFile.selectedPreset(instanceId); bankFile.selectedPreset(instanceId);
SaveBankFile(name, bankFile); SaveBankFile(name, bankFile);
this->bankIndex.addBank(-1,name); this->bankIndex.addBank(-1, name);
this->SaveBankIndex(); this->SaveBankIndex();
} }
void Storage::GetBankFile(int64_t instanceId,BankFile *pBank) const { void Storage::GetBankFile(int64_t instanceId, BankFile *pBank) const
{
auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId); auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId);
auto name = indexEntry.name(); auto name = indexEntry.name();
std::filesystem::path fileName = GetBankFileName(name); std::filesystem::path fileName = GetBankFileName(name);
@@ -407,7 +401,6 @@ void Storage::LoadBankFile(const std::string &name, BankFile *pBank)
std::ifstream is(fileName); std::ifstream is(fileName);
json_reader reader(is); json_reader reader(is);
reader.read(pBank); reader.read(pBank);
} }
void Storage::SaveBankFile(const std::string &name, const BankFile &bankFile) void Storage::SaveBankFile(const std::string &name, const BankFile &bankFile)
@@ -459,8 +452,9 @@ const PedalBoard &Storage::GetCurrentPreset()
bool Storage::LoadPreset(int64_t instanceId) bool Storage::LoadPreset(int64_t instanceId)
{ {
if (!currentBank.hasItem(instanceId)) if (!currentBank.hasItem(instanceId))
return false; return false;
if (instanceId != currentBank.selectedPreset()) { if (instanceId != currentBank.selectedPreset())
{
currentBank.selectedPreset(instanceId); currentBank.selectedPreset(instanceId);
SaveCurrentBank(); SaveCurrentBank();
} }
@@ -489,13 +483,13 @@ void Storage::SetPresetIndex(const PresetIndex &presets)
std::map<int64_t, std::unique_ptr<BankFileEntry> *> entries; std::map<int64_t, std::unique_ptr<BankFileEntry> *> entries;
for (int i = 0; i < this->currentBank.presets().size(); ++i) for (int i = 0; i < this->currentBank.presets().size(); ++i)
{ {
auto & preset = this->currentBank.presets()[i]; auto &preset = this->currentBank.presets()[i];
entries[preset->instanceId()] = &(this->currentBank.presets()[i]); entries[preset->instanceId()] = &(this->currentBank.presets()[i]);
} }
std::vector<std::unique_ptr<BankFileEntry>*> newPresets; std::vector<std::unique_ptr<BankFileEntry> *> newPresets;
for (size_t i = 0; i < presets.presets().size(); ++i) for (size_t i = 0; i < presets.presets().size(); ++i)
{ {
std::unique_ptr<BankFileEntry>* p = entries[presets.presets()[i].instanceId()]; std::unique_ptr<BankFileEntry> *p = entries[presets.presets()[i].instanceId()];
if (p == nullptr) if (p == nullptr)
{ {
throw PiPedalStateException("Presets do not match the currently loaded bank."); throw PiPedalStateException("Presets do not match the currently loaded bank.");
@@ -544,7 +538,7 @@ PedalBoard Storage::GetPreset(int64_t instanceId) const
int64_t Storage::DeletePreset(int64_t presetId) int64_t Storage::DeletePreset(int64_t presetId)
{ {
int64_t newSelection = currentBank.deletePreset(presetId); int64_t newSelection = currentBank.deletePreset(presetId);
SaveCurrentBank(); SaveCurrentBank();
return newSelection; return newSelection;
} }
@@ -724,7 +718,7 @@ int64_t Storage::SaveBankAs(int64_t bankId, const std::string &newName)
std::filesystem::path newPath = this->GetBankFileName(newName); std::filesystem::path newPath = this->GetBankFileName(newName);
try try
{ {
std::filesystem::copy(oldPath,newPath); std::filesystem::copy(oldPath, newPath);
} }
catch (std::exception &e) catch (std::exception &e)
{ {
@@ -732,36 +726,40 @@ int64_t Storage::SaveBankAs(int64_t bankId, const std::string &newName)
s << "Unable to save the bank. (" << e.what() << ")"; s << "Unable to save the bank. (" << e.what() << ")";
throw PiPedalException(s.str()); throw PiPedalException(s.str());
} }
int64_t newId = this->bankIndex.addBank(bankId,newName); int64_t newId = this->bankIndex.addBank(bankId, newName);
SaveBankIndex(); SaveBankIndex();
return newId; return newId;
} }
void Storage::MoveBank(int from, int to) void Storage::MoveBank(int from, int to)
{ {
this->bankIndex.move(from,to); this->bankIndex.move(from, to);
this->SaveBankIndex(); this->SaveBankIndex();
} }
int64_t Storage::DeleteBank(int64_t bankId) int64_t Storage::DeleteBank(int64_t bankId)
{ {
auto & entries = this->bankIndex.entries(); auto &entries = this->bankIndex.entries();
for (size_t i = 0; i < entries.size(); ++i) for (size_t i = 0; i < entries.size(); ++i)
{ {
auto & entry = entries[i]; auto &entry = entries[i];
if (entry.instanceId() == bankId) { if (entry.instanceId() == bankId)
{
std::filesystem::path fileName = this->GetBankFileName(entry.name()); std::filesystem::path fileName = this->GetBankFileName(entry.name());
entries.erase(entries.begin()+i); entries.erase(entries.begin() + i);
int64_t newSelection; int64_t newSelection;
if (i < entries.size()) if (i < entries.size())
{ {
newSelection = entries[i].instanceId(); newSelection = entries[i].instanceId();
} else if (entries.size() > 0) }
else if (entries.size() > 0)
{
newSelection = entries[entries.size() - 1].instanceId();
}
else
{ {
newSelection = entries[entries.size()-1].instanceId();
} else {
// zero entries? // zero entries?
// Create a default empty bank. // Create a default empty bank.
BankIndexEntry newEntry; BankIndexEntry newEntry;
@@ -773,7 +771,7 @@ int64_t Storage::DeleteBank(int64_t bankId)
std::string name = "Default Bank"; std::string name = "Default Bank";
SaveBankFile(name, defaultBank); SaveBankFile(name, defaultBank);
int64_t selectedBank = bankIndex.addBank(-1,name); int64_t selectedBank = bankIndex.addBank(-1, name);
newSelection = selectedBank; newSelection = selectedBank;
} }
if (this->bankIndex.selectedBank() == bankId) if (this->bankIndex.selectedBank() == bankId)
@@ -793,7 +791,7 @@ int64_t Storage::GetCurrentPresetId() const
return this->currentBank.selectedPreset(); return this->currentBank.selectedPreset();
} }
int64_t Storage::UploadPreset(const BankFile&bankFile,int64_t uploadAfter) int64_t Storage::UploadPreset(const BankFile &bankFile, int64_t uploadAfter)
{ {
int64_t lastPreset = this->currentBank.selectedPreset(); int64_t lastPreset = this->currentBank.selectedPreset();
if (uploadAfter != -1) if (uploadAfter != -1)
@@ -801,7 +799,8 @@ int64_t Storage::UploadPreset(const BankFile&bankFile,int64_t uploadAfter)
lastPreset = uploadAfter; lastPreset = uploadAfter;
} }
if (bankFile.presets().size() == 0) { if (bankFile.presets().size() == 0)
{
throw PiPedalException("Invalid preset."); throw PiPedalException("Invalid preset.");
} }
for (size_t i = 0; i < bankFile.presets().size(); ++i) for (size_t i = 0; i < bankFile.presets().size(); ++i)
@@ -817,12 +816,12 @@ int64_t Storage::UploadPreset(const BankFile&bankFile,int64_t uploadAfter)
preset.name(s.str()); preset.name(s.str());
} }
lastPreset = this->currentBank.addPreset(preset,lastPreset); lastPreset = this->currentBank.addPreset(preset, lastPreset);
} }
this->SaveCurrentBank(); this->SaveCurrentBank();
return lastPreset; return lastPreset;
} }
int64_t Storage::UploadBank(BankFile&bankFile,int64_t uploadAfter) int64_t Storage::UploadBank(BankFile &bankFile, int64_t uploadAfter)
{ {
int64_t lastBank = this->bankIndex.selectedBank(); int64_t lastBank = this->bankIndex.selectedBank();
if (uploadAfter != -1) if (uploadAfter != -1)
@@ -830,7 +829,8 @@ int64_t Storage::UploadBank(BankFile&bankFile,int64_t uploadAfter)
lastBank = uploadAfter; lastBank = uploadAfter;
} }
if (bankFile.presets().size() == 0) { if (bankFile.presets().size() == 0)
{
throw PiPedalException("Invalid bank."); throw PiPedalException("Invalid bank.");
} }
bankFile.updateNextIndex(); bankFile.updateNextIndex();
@@ -845,24 +845,25 @@ int64_t Storage::UploadBank(BankFile&bankFile,int64_t uploadAfter)
} }
std::filesystem::path path = this->GetBankFileName(bankFile.name()); std::filesystem::path path = this->GetBankFileName(bankFile.name());
std::ofstream f(path); std::ofstream f(path);
if (!f.is_open()) { if (!f.is_open())
{
throw PiPedalException("Can't write to bank file."); throw PiPedalException("Can't write to bank file.");
} }
json_writer writer(f); json_writer writer(f);
writer.write(bankFile); writer.write(bankFile);
lastBank = this->bankIndex.addBank(lastBank,bankFile.name()); lastBank = this->bankIndex.addBank(lastBank, bankFile.name());
this->SaveBankIndex(); this->SaveBankIndex();
return lastBank; return lastBank;
} }
void Storage::SetGovernorSettings(const std::string &governor)
void Storage::SetGovernorSettings(const std::string & governor)
{ {
userSettings.governor_ = governor; userSettings.governor_ = governor;
SaveUserSettings(); SaveUserSettings();
} }
void Storage::SaveUserSettings() { void Storage::SaveUserSettings()
{
std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME; std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME;
{ {
std::ofstream f(path); std::ofstream f(path);
@@ -889,8 +890,7 @@ std::string Storage::GetGovernorSettings() const
return this->userSettings.governor_; return this->userSettings.governor_;
} }
void Storage::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings)
void Storage::SetWifiConfigSettings(const WifiConfigSettings & wifiConfigSettings)
{ {
WifiConfigSettings copyToSave = wifiConfigSettings; WifiConfigSettings copyToSave = wifiConfigSettings;
copyToSave.rebootRequired_ = false; copyToSave.rebootRequired_ = false;
@@ -915,19 +915,21 @@ void Storage::SetWifiConfigSettings(const WifiConfigSettings & wifiConfigSetting
if (copyToStore.enable_) if (copyToStore.enable_)
{ {
copyToStore.hasPassword_ = copyToStore.password_.length() != 0 || this->wifiConfigSettings.hasPassword_; copyToStore.hasPassword_ = copyToStore.password_.length() != 0 || this->wifiConfigSettings.hasPassword_;
} else { }
else
{
copyToStore.hasPassword_ = false; copyToStore.hasPassword_ = false;
} }
copyToStore.password_ = ""; copyToStore.password_ = "";
copyToStore.rebootRequired_ = false; copyToStore.rebootRequired_ = false;
if (copyToStore.enable_ && !copyToStore.hasPassword_) if (copyToStore.enable_ && !copyToStore.hasPassword_)
{ {
copyToStore.hasPassword_ = this->wifiConfigSettings.hasPassword_; copyToStore.hasPassword_ = this->wifiConfigSettings.hasPassword_;
} }
this->wifiConfigSettings = copyToStore; this->wifiConfigSettings = copyToStore;
} }
void Storage::SetWifiDirectConfigSettings(const WifiDirectConfigSettings & wifiDirectConfigSettings) void Storage::SetWifiDirectConfigSettings(const WifiDirectConfigSettings &wifiDirectConfigSettings)
{ {
WifiDirectConfigSettings copyToSave = wifiDirectConfigSettings; WifiDirectConfigSettings copyToSave = wifiDirectConfigSettings;
copyToSave.rebootRequired_ = false; copyToSave.rebootRequired_ = false;
@@ -945,7 +947,8 @@ void Storage::LoadUserSettings()
{ {
std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME; std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME;
try { try
{
if (std::filesystem::is_regular_file(path)) if (std::filesystem::is_regular_file(path))
{ {
std::ifstream f(path); std::ifstream f(path);
@@ -956,17 +959,18 @@ void Storage::LoadUserSettings()
json_reader reader(f); json_reader reader(f);
reader.read(&userSettings); reader.read(&userSettings);
} }
} catch (const std::exception&) }
catch (const std::exception &)
{ {
} }
this->wifiConfigSettings.valid_ = true; this->wifiConfigSettings.valid_ = true;
} }
void Storage::LoadWifiConfigSettings() void Storage::LoadWifiConfigSettings()
{ {
std::filesystem::path path = this->dataRoot / WIFI_CONFIG_SETTINGS_FILENAME; std::filesystem::path path = this->dataRoot / WIFI_CONFIG_SETTINGS_FILENAME;
try { try
{
if (std::filesystem::is_regular_file(path)) if (std::filesystem::is_regular_file(path))
{ {
std::ifstream f(path); std::ifstream f(path);
@@ -979,9 +983,9 @@ void Storage::LoadWifiConfigSettings()
reader.read(&wifiConfigSettings); reader.read(&wifiConfigSettings);
this->wifiConfigSettings = wifiConfigSettings; this->wifiConfigSettings = wifiConfigSettings;
} }
} catch (const std::exception&) }
catch (const std::exception &)
{ {
} }
this->wifiConfigSettings.valid_ = true; this->wifiConfigSettings.valid_ = true;
} }
@@ -996,8 +1000,6 @@ void Storage::LoadWifiDirectConfigSettings()
this->wifiDirectConfigSettings = settings; this->wifiDirectConfigSettings = settings;
} }
WifiConfigSettings Storage::GetWifiConfigSettings() WifiConfigSettings Storage::GetWifiConfigSettings()
{ {
return this->wifiConfigSettings; return this->wifiConfigSettings;
@@ -1007,43 +1009,47 @@ WifiDirectConfigSettings Storage::GetWifiDirectConfigSettings()
return this->wifiDirectConfigSettings; return this->wifiDirectConfigSettings;
} }
void Storage::SaveCurrentPreset(const CurrentPreset &currentPreset) void Storage::SaveCurrentPreset(const CurrentPreset &currentPreset)
{ {
try { try
{
std::filesystem::path path = GetCurrentPresetPath(); std::filesystem::path path = GetCurrentPresetPath();
std::ofstream f(path); std::ofstream f(path);
json_writer writer(f); json_writer writer(f);
writer.write(currentPreset); writer.write(currentPreset);
} catch (std::exception&) }
catch (std::exception &)
{ {
// called from destructor. Must be nothrow(). // called from destructor. Must be nothrow().
} }
} }
bool Storage::RestoreCurrentPreset(CurrentPreset*pResult) bool Storage::RestoreCurrentPreset(CurrentPreset *pResult)
{ {
std::filesystem::path path = GetCurrentPresetPath(); std::filesystem::path path = GetCurrentPresetPath();
if (std::filesystem::exists(path)) { if (std::filesystem::exists(path))
try { {
try
{
std::ifstream f(path); std::ifstream f(path);
json_reader reader(f); json_reader reader(f);
reader.read(pResult); reader.read(pResult);
std::filesystem::remove(path); // one-shot only, restore the state from the last *orderly* shutdown. std::filesystem::remove(path); // one-shot only, restore the state from the last *orderly* shutdown.
} catch (const std::exception&) }
catch (const std::exception &)
{ {
return false; return false;
} }
return true; return true;
} else { }
else
{
return false; return false;
} }
} }
bool Storage::HasPluginPresets(const std::string &pluginUri) const bool Storage::HasPluginPresets(const std::string &pluginUri) const
{ {
for (const auto &entry: this->pluginPresetIndex.entries_) for (const auto &entry : this->pluginPresetIndex.entries_)
{ {
if (entry.pluginUri_ == pluginUri) if (entry.pluginUri_ == pluginUri)
{ {
@@ -1055,7 +1061,7 @@ bool Storage::HasPluginPresets(const std::string &pluginUri) const
std::filesystem::path Storage::GetPluginPresetPath(const std::string &pluginUri) const std::filesystem::path Storage::GetPluginPresetPath(const std::string &pluginUri) const
{ {
for (const auto &entry: this->pluginPresetIndex.entries_) for (const auto &entry : this->pluginPresetIndex.entries_)
{ {
if (entry.pluginUri_ == pluginUri) if (entry.pluginUri_ == pluginUri)
{ {
@@ -1063,9 +1069,8 @@ std::filesystem::path Storage::GetPluginPresetPath(const std::string &pluginUri)
} }
} }
throw PiPedalArgumentException("Plugin preset file not found."); throw PiPedalArgumentException("Plugin preset file not found.");
} }
void Storage::SavePluginPresets(const std::string&pluginUri, const PluginPresets&presets) void Storage::SavePluginPresets(const std::string &pluginUri, const PluginPresets &presets)
{ {
std::string name; std::string name;
std::filesystem::path path; std::filesystem::path path;
@@ -1075,10 +1080,12 @@ void Storage::SavePluginPresets(const std::string&pluginUri, const PluginPresets
name = SS(pluginPresetIndex.nextInstanceId_ << ".json"); name = SS(pluginPresetIndex.nextInstanceId_ << ".json");
path = GetPluginPresetsDirectory() / name; path = GetPluginPresetsDirectory() / name;
presetAdded = true; presetAdded = true;
} else { }
else
{
path = GetPluginPresetPath(pluginUri); path = GetPluginPresetPath(pluginUri);
} }
auto tempPath = path.string()+".$$$"; auto tempPath = path.string() + ".$$$";
{ {
std::ofstream os; std::ofstream os;
os.open(tempPath, std::ios_base::trunc); os.open(tempPath, std::ios_base::trunc);
@@ -1093,22 +1100,20 @@ void Storage::SavePluginPresets(const std::string&pluginUri, const PluginPresets
{ {
std::filesystem::remove(path); std::filesystem::remove(path);
} }
std::filesystem::rename(tempPath,path); std::filesystem::rename(tempPath, path);
if (presetAdded) if (presetAdded)
{ {
pluginPresetIndex.entries_.push_back( pluginPresetIndex.entries_.push_back(
PluginPresetIndexEntry( PluginPresetIndexEntry(
pluginUri,name pluginUri, name));
)
);
pluginPresetIndex.nextInstanceId_++; pluginPresetIndex.nextInstanceId_++;
this->pluginPresetIndexChanged = true; this->pluginPresetIndexChanged = true;
SavePluginPresetIndex(); SavePluginPresetIndex();
} }
} }
PluginPresets Storage::GetPluginPresets(const std::string&pluginUri) const PluginPresets Storage::GetPluginPresets(const std::string &pluginUri) const
{ {
PluginPresets result; PluginPresets result;
if (!HasPluginPresets(pluginUri)) if (!HasPluginPresets(pluginUri))
@@ -1127,36 +1132,33 @@ PluginPresets Storage::GetPluginPresets(const std::string&pluginUri) const
reader.read(&result); reader.read(&result);
return result; return result;
} }
PluginUiPresets Storage::GetPluginUiPresets(const std::string&pluginUri) const PluginUiPresets Storage::GetPluginUiPresets(const std::string &pluginUri) const
{ {
PluginPresets presets = GetPluginPresets(pluginUri); PluginPresets presets = GetPluginPresets(pluginUri);
PluginUiPresets result; PluginUiPresets result;
result.pluginUri_ = presets.pluginUri_; result.pluginUri_ = presets.pluginUri_;
for (size_t i = 0; i < presets.presets_.size(); ++i) for (size_t i = 0; i < presets.presets_.size(); ++i)
{ {
const auto& preset = presets.presets_[i]; const auto &preset = presets.presets_[i];
result.presets_.push_back( result.presets_.push_back(
PluginUiPreset PluginUiPreset{
{
preset.label_, preset.label_,
preset.instanceId_ preset.instanceId_});
}
);
} }
return result; return result;
} }
std::vector<ControlValue> Storage::GetPluginPresetValues(const std::string&pluginUri, uint64_t instanceId) std::vector<ControlValue> Storage::GetPluginPresetValues(const std::string &pluginUri, uint64_t instanceId)
{ {
auto presets = GetPluginPresets(pluginUri); auto presets = GetPluginPresets(pluginUri);
for (const auto & preset: presets.presets_) for (const auto &preset : presets.presets_)
{ {
if (preset.instanceId_ == instanceId) if (preset.instanceId_ == instanceId)
{ {
std::vector<ControlValue> result; std::vector<ControlValue> result;
for (const auto &valuePair: preset.controlValues_) for (const auto &valuePair : preset.controlValues_)
{ {
result.push_back(ControlValue(valuePair.first.c_str(),valuePair.second)); result.push_back(ControlValue(valuePair.first.c_str(), valuePair.second));
} }
return result; return result;
} }
@@ -1165,16 +1167,16 @@ std::vector<ControlValue> Storage::GetPluginPresetValues(const std::string&plugi
} }
uint64_t Storage::SavePluginPreset( uint64_t Storage::SavePluginPreset(
const std::string&pluginUri, const std::string &pluginUri,
const std::string&name, const std::string &name,
const std::map<std::string,float> & values) const std::map<std::string, float> &values)
{ {
auto presets = GetPluginPresets(pluginUri); auto presets = GetPluginPresets(pluginUri);
uint64_t result = -1; uint64_t result = -1;
bool existing = false; bool existing = false;
for (size_t i = 0; i < presets.presets_.size(); ++i) for (size_t i = 0; i < presets.presets_.size(); ++i)
{ {
auto & preset = presets.presets_[i]; auto &preset = presets.presets_[i];
if (preset.label_ == name) if (preset.label_ == name)
{ {
preset.controlValues_ = values; preset.controlValues_ = values;
@@ -1190,10 +1192,9 @@ uint64_t Storage::SavePluginPreset(
PluginPreset( PluginPreset(
result, result,
name, name,
values values));
));
} }
this->SavePluginPresets(pluginUri,presets); this->SavePluginPresets(pluginUri, presets);
return result; return result;
} }
@@ -1201,26 +1202,24 @@ void Storage::UpdatePluginPresets(const PluginUiPresets &pluginPresets)
{ {
// handles deletions, renaming, and reordering only. // handles deletions, renaming, and reordering only.
// If you need to add a preset, you neet to call SavePluginPreset or DulicatePluginPreset instead. // If you need to add a preset, you neet to call SavePluginPreset or DulicatePluginPreset instead.
PluginPresets presets = this->GetPluginPresets(pluginPresets.pluginUri_); PluginPresets presets = this->GetPluginPresets(pluginPresets.pluginUri_);
PluginPresets newPresets; PluginPresets newPresets;
newPresets.pluginUri_ = pluginPresets.pluginUri_; newPresets.pluginUri_ = pluginPresets.pluginUri_;
newPresets.nextInstanceId_ = presets.nextInstanceId_; newPresets.nextInstanceId_ = presets.nextInstanceId_;
for (const auto&preset: pluginPresets.presets_) for (const auto &preset : pluginPresets.presets_)
{ {
PluginPreset newPreset = presets.GetPreset(preset.instanceId_); PluginPreset newPreset = presets.GetPreset(preset.instanceId_);
newPreset.label_ = preset.label_; newPreset.label_ = preset.label_;
newPresets.presets_.push_back(std::move(newPreset)); newPresets.presets_.push_back(std::move(newPreset));
} }
SavePluginPresets(newPresets.pluginUri_,newPresets); SavePluginPresets(newPresets.pluginUri_, newPresets);
} }
static std::string stripCopySuffix(const std::string &s) static std::string stripCopySuffix(const std::string &s)
{ {
int pos = s.length()-1; int pos = s.length() - 1;
if (pos >= 0 && s[pos] == ')') if (pos >= 0 && s[pos] == ')')
{ {
--pos; --pos;
@@ -1228,7 +1227,7 @@ static std::string stripCopySuffix(const std::string &s)
{ {
--pos; --pos;
} }
if (pos >= 0 && s[pos] == '(') if (pos >= 0 && s[pos] == '(')
{ {
--pos; --pos;
} }
@@ -1236,11 +1235,11 @@ static std::string stripCopySuffix(const std::string &s)
{ {
--pos; --pos;
} }
return s.substr(0,pos+1); return s.substr(0, pos + 1);
} }
return s; return s;
} }
uint64_t Storage::CopyPluginPreset(const std::string&pluginUri,uint64_t presetId) uint64_t Storage::CopyPluginPreset(const std::string &pluginUri, uint64_t presetId)
{ {
PluginPresets presets = this->GetPluginPresets(pluginUri); PluginPresets presets = this->GetPluginPresets(pluginUri);
size_t pos = presets.Find(presetId); size_t pos = presets.Find(presetId);
@@ -1266,17 +1265,14 @@ uint64_t Storage::CopyPluginPreset(const std::string&pluginUri,uint64_t presetId
} }
} }
t.label_ = name; t.label_ = name;
presets.presets_.insert(presets.presets_.begin()+pos+1,t); presets.presets_.insert(presets.presets_.begin() + pos + 1, t);
SavePluginPresets(presets.pluginUri_,presets); SavePluginPresets(presets.pluginUri_, presets);
return t.instanceId_; return t.instanceId_;
} }
std::map<std::string,bool> Storage::GetFavorites() const std::map<std::string, bool> Storage::GetFavorites() const
{ {
std::map<std::string,bool> result; std::map<std::string, bool> result;
std::filesystem::path fileName = this->dataRoot / "favorites.json"; std::filesystem::path fileName = this->dataRoot / "favorites.json";
if (!std::filesystem::exists(fileName)) if (!std::filesystem::exists(fileName))
@@ -1291,10 +1287,9 @@ std::map<std::string,bool> Storage::GetFavorites() const
reader.read(&result); reader.read(&result);
} }
return result; return result;
} }
void Storage::SetFavorites(const std::map<std::string,bool>&favorites) { void Storage::SetFavorites(const std::map<std::string, bool> &favorites)
{
std::filesystem::path fileName = this->dataRoot / "favorites.json"; std::filesystem::path fileName = this->dataRoot / "favorites.json";
std::ofstream f; std::ofstream f;
f.open(fileName); f.open(fileName);
@@ -1305,15 +1300,45 @@ void Storage::SetFavorites(const std::map<std::string,bool>&favorites) {
} }
} }
pipedal::JackServerSettings Storage::GetJackServerSettings()
{
JackServerSettings result;
#if JACK_HOST
result.Initialize();
#else
std::filesystem::path fileName = this->dataRoot / "AudioConfig.json";
std::ifstream f;
f.open(fileName);
if (f.is_open())
{
json_reader reader(f);
reader.read(&result);
}
#endif
return result;
}
void Storage::SetJackServerSettings(const pipedal::JackServerSettings &jackConfiguration)
{
#if JACK_HOST
#error IMPLEMENT ME
#else
std::filesystem::path fileName = this->dataRoot / "AudioConfig.json";
std::ofstream f;
f.open(fileName);
if (f.is_open())
{
json_writer writer(f);
writer.write(jackConfiguration);
}
#endif
}
JSON_MAP_BEGIN(UserSettings) JSON_MAP_BEGIN(UserSettings)
JSON_MAP_REFERENCE(UserSettings,governor) JSON_MAP_REFERENCE(UserSettings, governor)
JSON_MAP_REFERENCE(UserSettings,showStatusMonitor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
JSON_MAP_END() JSON_MAP_END()
JSON_MAP_BEGIN(CurrentPreset) JSON_MAP_BEGIN(CurrentPreset)
JSON_MAP_REFERENCE(CurrentPreset,modified) JSON_MAP_REFERENCE(CurrentPreset, modified)
JSON_MAP_REFERENCE(CurrentPreset,preset) JSON_MAP_REFERENCE(CurrentPreset, preset)
JSON_MAP_END() JSON_MAP_END()
+3
View File
@@ -25,6 +25,7 @@
#include "PluginPreset.hpp" #include "PluginPreset.hpp"
#include "Banks.hpp" #include "Banks.hpp"
#include "JackConfiguration.hpp" #include "JackConfiguration.hpp"
#include "JackServerSettings.hpp"
#include "WifiConfigSettings.hpp" #include "WifiConfigSettings.hpp"
#include "WifiDirectConfigSettings.hpp" #include "WifiDirectConfigSettings.hpp"
#include <map> #include <map>
@@ -100,6 +101,8 @@ public:
const BankIndex & GetBanks() const { return bankIndex; } const BankIndex & GetBanks() const { return bankIndex; }
JackServerSettings GetJackServerSettings();
void SetJackServerSettings(const pipedal::JackServerSettings&jackServerSettings);
void LoadWifiConfigSettings(); void LoadWifiConfigSettings();
void LoadWifiDirectConfigSettings(); void LoadWifiDirectConfigSettings();
+79 -56
View File
@@ -18,6 +18,8 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include "pch.h" #include "pch.h"
#include "AudioConfig.hpp"
#include "BeastServer.hpp" #include "BeastServer.hpp"
#include <iostream> #include <iostream>
#include "Lv2Log.hpp" #include "Lv2Log.hpp"
@@ -404,7 +406,7 @@ public:
} }
}; };
/* When hosting a react app, replace /var/config.json with /* When hosting a react app, replace /var/config.json with
data that will connect the react app with our socket server. data that will connect the react app with our socket server.
*/ */
@@ -488,7 +490,6 @@ static bool isJackServiceRunning()
return std::filesystem::exists(path); return std::filesystem::exists(path);
} }
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
@@ -621,39 +622,10 @@ int main(int argc, char *argv[])
{ {
PiPedalModel model; PiPedalModel model;
// Pre-populate ALSA device info,wait for jackd service (daemon only) model.Init(configuration);
model.GetAlsaDevices(); // pre-cache ALSA devices.
if (systemd)
{
auto devices = model.GetAlsaDevices(); // pre-cache ALSA devices (most especially, the one which Jack will open, that we can no longer read)
auto serverSettings = model.GetJackServerSettings(); // Get heavy IO out of the way before letting dependent (Jack/ALSA) services run.
if (serverSettings.IsValid()) model.LoadLv2PluginInfo();
{
// wait up to 15 seconds for the midi device to come online.
if (!HasAlsaDevice(devices, serverSettings.GetAlsaDevice()))
{
Lv2Log::info("Waiting for ALSA device " + serverSettings.GetAlsaDevice());
for (int i = 0; i < 10; ++i)
{
sleep(1);
devices = model.GetAlsaDevices();
if (HasAlsaDevice(devices, serverSettings.GetAlsaDevice()))
{
break;
}
if (g_SigBreak)
break;
}
}
}
// pre-cache device info before we let audio services run.
model.GetAlsaDevices();
}
// Get heavy IO out of the way before letting dependent (Jack) services run.
model.LoadLv2PluginInfo(configuration);
if (systemd) if (systemd)
{ {
// Tell systemd we're done. // Tell systemd we're done.
@@ -662,34 +634,85 @@ int main(int argc, char *argv[])
(unsigned long)getpid()); (unsigned long)getpid());
} }
if (!isJackServiceRunning())
{
Lv2Log::info("Waiting for Jack service.");
// wait up to 15 seconds for the jack service to come online.
for (int i = 0; i < 15; ++i)
{
// use the time to prepopulate ALSA device cache before jack
// opens the device and we can't read properties.
model.GetAlsaDevices();
sleep(1); auto serverSettings = model.GetJackServerSettings();
if (isJackServiceRunning())
if (true || systemd)
{
// IF running as a service, wait for selected audio device to be initialized.
// It may take some time for ALSA to publish all available devices.
if (serverSettings.IsValid())
{
// wait up to 15 seconds for the midi device to come online.
auto devices = model.GetAlsaDevices();
bool firstMessage = true;
bool found = false;
if (!HasAlsaDevice(devices, serverSettings.GetAlsaDevice()))
{ {
break; if (firstMessage) Lv2Log::info(SS("Waiting for ALSA device " << serverSettings.GetAlsaDevice() << " to come online..."));
firstMessage = false;
for (int i = 0; i < 5; ++i)
{
sleep(3);
devices = model.GetAlsaDevices();
if (HasAlsaDevice(devices, serverSettings.GetAlsaDevice()))
{
found = true;
break;
}
if (g_SigBreak)
exit(1);
}
if (found)
{
Lv2Log::info(SS("Found ALSA device " << serverSettings.GetAlsaDevice() << "."));
} else {
Lv2Log::info(SS("ALSA device " << serverSettings.GetAlsaDevice() << " not found."));
}
} }
} }
}
if (isJackServiceRunning()) // pre-cache device info before we let audio services run.
{ model.GetAlsaDevices();
Lv2Log::info("Found Jack service.");
sleep(3); // jack needs a little time to get up to speed.
}
else
{
Lv2Log::info("Jack service not started.");
} }
model.Load(configuration); #if JACK_HOST
if (systemd)
{
if (!isJackServiceRunning())
{
Lv2Log::info("Waiting for Jack service.");
// wait up to 15 seconds for the jack service to come online.
for (int i = 0; i < 15; ++i)
{
// use the time to prepopulate ALSA device cache before jack
// opens the device and we can't read properties.
model.GetAlsaDevices();
sleep(1);
if (isJackServiceRunning())
{
break;
}
}
}
if (isJackServiceRunning())
{
Lv2Log::info("Found Jack service.");
sleep(3); // jack needs a little time to get up to speed.
}
else
{
Lv2Log::info("Jack service not started.");
}
model.GetAlsaDevices();
}
#endif
model.Load();
auto pipedalSocketFactory = MakePiPedalSocketFactory(model); auto pipedalSocketFactory = MakePiPedalSocketFactory(model);
+1
View File
@@ -3,6 +3,7 @@ Description=Jack Audio Daemon
After=sound.target After=sound.target
After=local-fs.target After=local-fs.target
After=dbus.socket After=dbus.socket
After=pipedald
[Service] [Service]
-1
View File
@@ -1,6 +1,5 @@
[Unit] [Unit]
Description=${DESCRIPTION} Description=${DESCRIPTION}
Before=jack.service
After=network.target After=network.target
After=sound.target After=sound.target
After=local-fs.target After=local-fs.target
+11
View File
@@ -1,3 +1,14 @@
JackConfiguration Input midi ports.
JackSettings Input midi ports.
Midi Port selection UI.
CPU Use should be % of AVAILABLE time, not % of total.
MIDI device selection.
raw Alsa midi.
Pri Description Pri Description
----------------- -----------------