0.9.11-rc-2
Switch from JACK to ALSA audio.
This commit is contained in:
Vendored
+39
-1
@@ -5,6 +5,7 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
|
||||
{
|
||||
"name": "(gdb) generic",
|
||||
"type": "cppdbg",
|
||||
@@ -77,7 +78,9 @@
|
||||
"request": "launch",
|
||||
// Resolved by CMake Tools:
|
||||
"program": "${command:cmake.launchTargetPath}",
|
||||
"args": [ "[avahi_service]" ],
|
||||
|
||||
"args": [ "[alsa_midi_test]" ],
|
||||
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"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",
|
||||
"type": "cppdbg",
|
||||
|
||||
+5
-3
@@ -1,10 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.16.0)
|
||||
project(pipedal
|
||||
VERSION 0.9.10
|
||||
VERSION 0.9.11
|
||||
DESCRIPTION "PiPedal Guitar Effect Pedal For Raspberry Pi"
|
||||
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/")
|
||||
|
||||
@@ -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_SHLIBDEPS ON)
|
||||
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_PACKAGING_INSTALL_PREFIX /usr)
|
||||
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
<img src='docs/GithubBanner.png' width="100%" />
|
||||
|
||||
Download: <a href='docs/download.md'>v0.9.10-rc-1</a> Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal).
|
||||
Download: <a href='docs/download.md'>v0.9.11-rc-2</a> 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.
|
||||
|
||||
+2
-2
@@ -4,14 +4,14 @@
|
||||
|
||||
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
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
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).
|
||||
|
||||
+2
-2
@@ -4,14 +4,14 @@
|
||||
|
||||
Download the most recent Debian (.deb) package for your platform:
|
||||
|
||||
- [Ubuntu or Raspberry Pi OS (64-bit)](https://github.com/rerdavies/pipedal/releases/download/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
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
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.
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
<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.
|
||||
|
||||
@@ -21,6 +21,7 @@ add_custom_command(
|
||||
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/react
|
||||
|
||||
DEPENDS
|
||||
src/AlsaMidiDeviceInfo.tsx
|
||||
src/SearchControl.tsx
|
||||
src/SplitControlView.tsx
|
||||
src/WifiConfigSettings.tsx
|
||||
|
||||
@@ -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
@@ -18,13 +18,14 @@
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import {PiPedalArgumentError} from './PiPedalError';
|
||||
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
|
||||
|
||||
export class JackChannelSelection {
|
||||
deserialize(input: any): JackChannelSelection
|
||||
{
|
||||
this.inputAudioPorts = input.inputAudioPorts.slice();
|
||||
this.outputAudioPorts = input.outputAudioPorts.slice();
|
||||
this.inputMidiPorts = input.inputMidiPorts.slice();
|
||||
this.inputMidiDevices = AlsaMidiDeviceInfo.deserializeArray(input.inputMidiDevices);
|
||||
this.sampleRate = input.sampleRate;
|
||||
this.bufferSize = input.bufferSize;
|
||||
this.numberOfBuffers = input.numberOfBuffers;
|
||||
@@ -35,7 +36,7 @@ export class JackChannelSelection {
|
||||
}
|
||||
inputAudioPorts: string[] = [];
|
||||
outputAudioPorts: string[] = [];
|
||||
inputMidiPorts: string[] = [];
|
||||
inputMidiDevices: AlsaMidiDeviceInfo[] = [];
|
||||
|
||||
sampleRate: number = 48000;
|
||||
bufferSize: number = 64;
|
||||
@@ -45,6 +46,7 @@ export class JackChannelSelection {
|
||||
let result = new JackChannelSelection();
|
||||
result.inputAudioPorts = jackConfiguration.inputAudioPorts.slice(0,2);
|
||||
result.outputAudioPorts = jackConfiguration.inputAudioPorts.slice(0,2);
|
||||
result.inputMidiDevices = [];
|
||||
return result;
|
||||
}
|
||||
getChannelDisplayValue(selectedChannels: string[], availableChannels: string[],isConfigValid: boolean): string
|
||||
@@ -97,8 +99,7 @@ export class JackConfiguration {
|
||||
this.maxAllowedMidiDelta = input.maxAllowedMidiDelta;
|
||||
this.inputAudioPorts = input.inputAudioPorts;
|
||||
this.outputAudioPorts = input.outputAudioPorts;
|
||||
this.inputMidiPorts = input.inputMidiPorts;
|
||||
this.outputMidiPorts = input.outputMidiPorts;
|
||||
this.inputMidiDevices = AlsaMidiDeviceInfo.deserializeArray(input.inputMidiDevices);
|
||||
return this;
|
||||
}
|
||||
isValid: boolean = false;
|
||||
@@ -111,8 +112,8 @@ export class JackConfiguration {
|
||||
|
||||
inputAudioPorts: string[] = [];
|
||||
outputAudioPorts: string[] = [];
|
||||
inputMidiPorts: string[] = [];
|
||||
outputMidiPorts: string[] = [];
|
||||
inputMidiDevices: AlsaMidiDeviceInfo[] = [];
|
||||
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ export default class JackHostStatus {
|
||||
deserialize(input: any): JackHostStatus {
|
||||
this.active = input.active;
|
||||
this.restarting = input.restarting;
|
||||
this.errorMessage = input.errorMessage;
|
||||
this.underruns = input.underruns;
|
||||
this.cpuUsage = input.cpuUsage;
|
||||
this.msSinceLastUnderrun = input.msSinceLastUnderrun;
|
||||
@@ -71,6 +72,7 @@ export default class JackHostStatus {
|
||||
return this.temperaturemC >= -100000;
|
||||
}
|
||||
active: boolean = false;
|
||||
errorMessage: string = "";
|
||||
restarting: boolean = false;
|
||||
underruns: number = 0;
|
||||
cpuUsage: number = 0;
|
||||
@@ -136,7 +138,7 @@ export default class JackHostStatus {
|
||||
<Typography variant="caption" color="textSecondary">{label}</Typography>
|
||||
|
||||
<span style={{ color: RED_COLOR }}>
|
||||
<Typography variant="caption" color="inherit">Stopped </Typography>
|
||||
<Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Stopped" : status.errorMessage} </Typography>
|
||||
</span>
|
||||
{
|
||||
status.temperaturemC > -100000 &&
|
||||
|
||||
@@ -135,7 +135,7 @@ const JackServerSettingsDialog = withStyles(styles)(
|
||||
}
|
||||
if (result.sampleRate === 0) result.sampleRate = 48000;
|
||||
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.bufferSize = selectedDevice.closestBufferSize(result.bufferSize);
|
||||
result.valid = true;
|
||||
|
||||
@@ -96,6 +96,7 @@ export const MidiBindingDialog =
|
||||
hasHooks: boolean = false;
|
||||
|
||||
handleClose() {
|
||||
this.cancelListenForControl();
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
@@ -120,7 +121,6 @@ export const MidiBindingDialog =
|
||||
|
||||
handleListenSucceeded(instanceId: number, symbol: string, isNote: boolean, noteOrControl: number)
|
||||
{
|
||||
this.listenHandle = undefined; // (one-shot event)
|
||||
this.cancelListenForControl();
|
||||
|
||||
let pedalBoard = this.model.pedalBoard.get();
|
||||
|
||||
@@ -1826,12 +1826,18 @@ class PiPedalModelImpl implements PiPedalModel {
|
||||
}
|
||||
}
|
||||
cancelListenForMidiEvent(listenHandle: ListenHandle): void {
|
||||
let found = false;
|
||||
for (let i = 0; i < this.midiListeners.length; ++i) {
|
||||
if (this.midiListeners[i].handle === listenHandle._handle) {
|
||||
this.midiListeners.splice(i, 1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
console.log('cancelListenForMidiEvent: event not found.');
|
||||
}
|
||||
this.webSocket?.send("cancelListenForMidiEvent", listenHandle._handle);
|
||||
}
|
||||
cancelListenForAtomOutput(listenHandle: ListenHandle): void {
|
||||
|
||||
@@ -27,38 +27,43 @@ import Dialog from '@mui/material/Dialog';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
|
||||
|
||||
|
||||
|
||||
|
||||
export interface SelectMidiChannelsDialogProps {
|
||||
open: boolean;
|
||||
selectedChannels: string[];
|
||||
availableChannels: string[];
|
||||
onClose: (selectedChannels: string[] | null) => void;
|
||||
selectedChannels: AlsaMidiDeviceInfo[];
|
||||
availableChannels: AlsaMidiDeviceInfo[];
|
||||
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) {
|
||||
if (selectedChannels[i] === channel) return true;
|
||||
if (selectedChannels[i].equals(channel)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function addPort(availableChannels: string[], selectedChannels: string[], newChannel: string) {
|
||||
let result: string[] = [];
|
||||
function addPort(availableChannels: AlsaMidiDeviceInfo[], selectedChannels: AlsaMidiDeviceInfo[], newChannel: AlsaMidiDeviceInfo)
|
||||
:AlsaMidiDeviceInfo[]
|
||||
{
|
||||
let result: AlsaMidiDeviceInfo[] = [];
|
||||
for (let i = 0; i < availableChannels.length; ++i) {
|
||||
let channel = availableChannels[i];
|
||||
if (isChecked(selectedChannels, channel) || channel === newChannel) {
|
||||
if (isChecked(selectedChannels, channel) || channel.equals(newChannel)) {
|
||||
result.push(channel);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function removePort(selectedChannels: string[], channel: string) {
|
||||
let result: string[] = [];
|
||||
function removePort(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo)
|
||||
:AlsaMidiDeviceInfo[]
|
||||
{
|
||||
let result: AlsaMidiDeviceInfo[] = [];
|
||||
for (let i = 0; i < selectedChannels.length; ++i) {
|
||||
if (selectedChannels[i] !== channel) {
|
||||
if (!selectedChannels[i].equals(channel)) {
|
||||
result.push(selectedChannels[i]);
|
||||
}
|
||||
}
|
||||
@@ -72,7 +77,7 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
|
||||
|
||||
|
||||
|
||||
let toggleSelect = (value: string) => {
|
||||
let toggleSelect = (value: AlsaMidiDeviceInfo) => {
|
||||
if (!isChecked(currentSelection, value)) {
|
||||
setCurrentSelection(addPort(availableChannels, currentSelection, value));
|
||||
} else {
|
||||
@@ -90,15 +95,15 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) {
|
||||
|
||||
return (
|
||||
<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>
|
||||
{availableChannels.map((channel) => (
|
||||
<ListItem button >
|
||||
<FormControlLabel
|
||||
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>
|
||||
)
|
||||
|
||||
@@ -42,6 +42,7 @@ import WifiConfigDialog from './WifiConfigDialog';
|
||||
import WifiDirectConfigDialog from './WifiDirectConfigDialog';
|
||||
import DialogEx from './DialogEx'
|
||||
import GovernorSettings from './GovernorSettings';
|
||||
import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo';
|
||||
|
||||
import Slide, { SlideProps } from '@mui/material/Slide';
|
||||
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) {
|
||||
let newSelection = this.state.jackSettings.clone();
|
||||
newSelection.inputMidiPorts = channels;
|
||||
let newSelection: JackChannelSelection = this.state.jackSettings.clone();
|
||||
newSelection.inputMidiDevices = channels;
|
||||
this.model.setJackSettings(newSelection);
|
||||
|
||||
}
|
||||
@@ -430,9 +431,9 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
||||
}
|
||||
|
||||
midiSummary(): string {
|
||||
let ports = this.state.jackSettings.inputMidiPorts;
|
||||
let ports = this.state.jackSettings.inputMidiDevices;
|
||||
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";
|
||||
}
|
||||
handleShowWifiConfigDialog() {
|
||||
@@ -592,7 +593,7 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
||||
<ButtonBase className={classes.setting} disabled={!isConfigValid} onClick={() => this.handleMidiSelection()} >
|
||||
<SelectHoverBackground selected={false} showHover={true} />
|
||||
<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>
|
||||
</div>
|
||||
@@ -767,9 +768,9 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
|
||||
(this.state.showMidiSelectDialog) &&
|
||||
(
|
||||
<SelectMidiChannelsDialog open={this.state.showMidiSelectDialog}
|
||||
onClose={(selectedChannels: string[] | null) => this.handleSelectMidiDialogResult(selectedChannels)}
|
||||
selectedChannels={this.state.jackSettings.inputMidiPorts}
|
||||
availableChannels={this.state.jackConfiguration.inputMidiPorts}
|
||||
onClose={(selectedChannels: AlsaMidiDeviceInfo[] | null) => this.handleSelectMidiDialogResult(selectedChannels)}
|
||||
selectedChannels={this.state.jackSettings.inputMidiDevices}
|
||||
availableChannels={this.state.jackConfiguration.inputMidiDevices}
|
||||
/>
|
||||
|
||||
)
|
||||
|
||||
@@ -204,10 +204,16 @@ bool setJackConfiguration(JackServerSettings serverSettings)
|
||||
{
|
||||
bool success = true;
|
||||
|
||||
#if JACK_HOST
|
||||
|
||||
serverSettings.Write();
|
||||
|
||||
silentSysExec("/usr/bin/systemctl unmask 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);
|
||||
delayedRestartThread.detach();
|
||||
|
||||
+1686
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -94,6 +94,7 @@ add_definitions(-DBOOST_ERROR_CODE_HEADER_ONLY -DBOOST_BIND_GLOBAL_PLACEHOLDERS
|
||||
|
||||
|
||||
set (PIPEDAL_SOURCES
|
||||
CpuUse.hpp CpuUse.cpp
|
||||
P2pConfigFiles.hpp
|
||||
AvahiService.cpp AvahiService.hpp
|
||||
DeviceIdFile.cpp DeviceIdFile.hpp
|
||||
@@ -153,6 +154,11 @@ set (PIPEDAL_SOURCES
|
||||
InheritPriorityMutex.hpp InheritPriorityMutex.cpp
|
||||
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
|
||||
${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
|
||||
jsonTest.cpp
|
||||
AlsaDriverTest.cpp
|
||||
AvahiServiceTest.cpp
|
||||
WifiChannelsTest.cpp
|
||||
PiPedalAlsaTest.cpp
|
||||
@@ -259,6 +268,27 @@ add_executable(pipedalconfig
|
||||
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
|
||||
UnixSocket.cpp UnixSocket.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)
|
||||
|
||||
|
||||
add_executable(processcopyrights copyrightMain.cpp
|
||||
CommandLineParser.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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
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 &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)
|
||||
{
|
||||
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>
|
||||
void AddOption(const std::string &option, T *pResult)
|
||||
{
|
||||
|
||||
+36
-12
@@ -36,6 +36,17 @@
|
||||
#include "DeviceIdFile.hpp"
|
||||
#include <uuid/uuid.h>
|
||||
#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())
|
||||
|
||||
@@ -110,6 +121,14 @@ void EnableService()
|
||||
}
|
||||
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)
|
||||
{
|
||||
cout << "Error: Failed to disable the " NATIVE_SERVICE " service.";
|
||||
@@ -145,10 +164,14 @@ void StopService(bool excludeShutdownService = false)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if INSTALL_JACK_SERVICE || UNINSTALL_JACK_SERVICE
|
||||
if (sysExec(SYSTEMCTL_BIN " stop " JACK_SERVICE ".service") != EXIT_SUCCESS)
|
||||
{
|
||||
#if INSTALL_JACK_SERVICE
|
||||
throw PiPedalException("Failed to stop the " JACK_SERVICE " service.");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void StartService(bool excludeShutdownService = false)
|
||||
@@ -166,6 +189,7 @@ void StartService(bool excludeShutdownService = false)
|
||||
{
|
||||
throw PiPedalException("Failed to start the " NATIVE_SERVICE " service.");
|
||||
}
|
||||
#if INSTALL_JACK_SERVICE
|
||||
JackServerSettings serverSettings;
|
||||
serverSettings.ReadJackConfiguration();
|
||||
if (serverSettings.IsValid())
|
||||
@@ -175,6 +199,7 @@ void StartService(bool excludeShutdownService = false)
|
||||
throw PiPedalException("Failed to start the " JACK_SERVICE " service.");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
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;
|
||||
void UninstallPamEnv()
|
||||
{
|
||||
#if UNINSTALL_JACK_SERVICE
|
||||
RemoveLine(
|
||||
"/etc/security/pam_env.conf",
|
||||
PAM_LINE);
|
||||
#endif
|
||||
}
|
||||
void InstallPamEnv()
|
||||
{
|
||||
#if INSTALL_JACK_SERVICE
|
||||
std::string newLine = PAM_LINE;
|
||||
std::vector<std::string> lines;
|
||||
std::filesystem::path path = "/etc/security/pam_env.conf";
|
||||
@@ -313,6 +341,7 @@ void InstallPamEnv()
|
||||
{
|
||||
cout << "Failed to update " << path << ". " << e.what();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void InstallLimits()
|
||||
@@ -322,12 +351,12 @@ void InstallLimits()
|
||||
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))
|
||||
{
|
||||
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";
|
||||
output << "@audio - rtprio 95"
|
||||
<< "\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()
|
||||
{
|
||||
@@ -363,6 +383,8 @@ void InstallJackService()
|
||||
{
|
||||
InstallLimits();
|
||||
InstallPamEnv();
|
||||
|
||||
#if INSTALL_JACK_SERVICE
|
||||
if (sysExec(GROUPADD_BIN " -f " JACK_SERVICE_GROUP_NAME) != EXIT_SUCCESS)
|
||||
{
|
||||
throw PiPedalException("Failed to create jack service group.");
|
||||
@@ -391,6 +413,8 @@ void InstallJackService()
|
||||
WriteTemplateFile(map, GetServiceFileName(JACK_SERVICE));
|
||||
|
||||
MaybeStartJackService();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
int SudoExec(char **argv)
|
||||
@@ -463,7 +487,7 @@ void Uninstall()
|
||||
{
|
||||
}
|
||||
UninstallPamEnv();
|
||||
UninstallLimits();
|
||||
// UninstallLimits();
|
||||
sysExec(SYSTEMCTL_BIN " daemon-reload");
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
|
||||
@@ -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
@@ -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
@@ -19,50 +19,24 @@
|
||||
|
||||
#include "pch.h"
|
||||
#include "JackConfiguration.hpp"
|
||||
#include "AudioConfig.hpp"
|
||||
#include "AlsaDriver.hpp"
|
||||
#include "Lv2Log.hpp"
|
||||
#include "PiPedalException.hpp"
|
||||
|
||||
|
||||
#if JACK_HOST
|
||||
#include <jack/jack.h>
|
||||
#include <jack/types.h>
|
||||
#include <jack/session.h>
|
||||
#endif
|
||||
|
||||
#include <string.h>
|
||||
#include "defer.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
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()
|
||||
{
|
||||
@@ -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,
|
||||
const char *type_name_pattern,
|
||||
unsigned long flags)
|
||||
{
|
||||
#if JACK_HOST
|
||||
const char**ports = jack_get_ports(client, port_name_pattern, type_name_pattern, flags);
|
||||
if (ports != nullptr)
|
||||
{
|
||||
@@ -88,9 +64,14 @@ static void AddPorts(jack_client_t *client, std::vector<std::string> *output, co
|
||||
jack_free(ports);
|
||||
|
||||
}
|
||||
#else
|
||||
throw PiPedalStateException("JACK_HOST not enabled at compile time.");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace pipedal {
|
||||
#if JACK_HOST
|
||||
std::string GetJackErrorMessage(jack_status_t status)
|
||||
{
|
||||
std::stringstream s;
|
||||
@@ -122,9 +103,43 @@ namespace pipedal {
|
||||
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_client_t *client = nullptr;
|
||||
@@ -161,8 +176,6 @@ void JackConfiguration::Initialize()
|
||||
|
||||
AddPorts(client, &this->inputAudioPorts_, "system", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput);
|
||||
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;
|
||||
this->errorStatus_ = "";
|
||||
jack_client_close(client);
|
||||
@@ -172,6 +185,9 @@ void JackConfiguration::Initialize()
|
||||
jack_client_close(client);
|
||||
this->errorStatus_ = e.what();
|
||||
}
|
||||
#else
|
||||
throw PiPedalStateException("JACK_HOST not enabled at compile time.");
|
||||
#endif
|
||||
}
|
||||
|
||||
JackChannelSelection JackChannelSelection::MakeDefault(const JackConfiguration&config){
|
||||
@@ -211,12 +227,59 @@ static std::vector<std::string> makeValid(const std::vector<std::string> & selec
|
||||
}
|
||||
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 result;
|
||||
result.inputAudioPorts_ = makeValid(this->inputAudioPorts_,config.GetInputAudioPorts());
|
||||
result.outputAudioPorts_ = makeValid(this->outputAudioPorts_,config.GetOutputAudioPorts());
|
||||
result.inputMidiPorts_ = makeValid(this->inputMidiPorts_, config.GetInputMidiPorts());
|
||||
result.inputMidiDevices_ = makeValid(this->inputMidiDevices_, config.GetInputMidiDevices());
|
||||
if (!result.isValid())
|
||||
{
|
||||
return this->MakeDefault(config);
|
||||
@@ -226,7 +289,7 @@ JackChannelSelection JackChannelSelection::RemoveInvalidChannels(const JackConfi
|
||||
JSON_MAP_BEGIN(JackChannelSelection)
|
||||
JSON_MAP_REFERENCE(JackChannelSelection,inputAudioPorts)
|
||||
JSON_MAP_REFERENCE(JackChannelSelection,outputAudioPorts)
|
||||
JSON_MAP_REFERENCE(JackChannelSelection,inputMidiPorts)
|
||||
JSON_MAP_REFERENCE(JackChannelSelection,inputMidiDevices)
|
||||
|
||||
JSON_MAP_END()
|
||||
|
||||
@@ -240,7 +303,6 @@ JSON_MAP_BEGIN(JackConfiguration)
|
||||
JSON_MAP_REFERENCE(JackConfiguration,maxAllowedMidiDelta)
|
||||
JSON_MAP_REFERENCE(JackConfiguration,inputAudioPorts)
|
||||
JSON_MAP_REFERENCE(JackConfiguration,outputAudioPorts)
|
||||
JSON_MAP_REFERENCE(JackConfiguration,inputMidiPorts)
|
||||
JSON_MAP_REFERENCE(JackConfiguration,outputMidiPorts)
|
||||
JSON_MAP_REFERENCE(JackConfiguration,inputMidiDevices)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include "json.hpp"
|
||||
#include "JackServerSettings.hpp"
|
||||
#include "PiPedalAlsa.hpp"
|
||||
|
||||
|
||||
namespace pipedal
|
||||
{
|
||||
@@ -42,11 +45,13 @@ namespace pipedal
|
||||
|
||||
std::vector<std::string> inputAudioPorts_;
|
||||
std::vector<std::string> outputAudioPorts_;
|
||||
std::vector<std::string> inputMidiPorts_;
|
||||
std::vector<std::string> outputMidiPorts_;
|
||||
std::vector<AlsaMidiDeviceInfo> inputMidiDevices_;
|
||||
public:
|
||||
JackConfiguration();
|
||||
void Initialize();
|
||||
|
||||
void AlsaInitialize(const JackServerSettings &jackServerSettings); // from also config settings.
|
||||
|
||||
void JackInitialize(); // from jack server instance.
|
||||
~JackConfiguration();
|
||||
bool isValid() const { return isValid_;}
|
||||
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> &GetOutputAudioPorts() const { return outputAudioPorts_; }
|
||||
const std::vector<std::string> &GetInputMidiPorts() const { return inputMidiPorts_; }
|
||||
const std::vector<std::string> &GetOutputMidiPorts() const { return outputMidiPorts_; }
|
||||
const std::vector<AlsaMidiDeviceInfo> &GetInputMidiDevices() const { return inputMidiDevices_; }
|
||||
|
||||
DECLARE_JSON_MAP(JackConfiguration);
|
||||
|
||||
@@ -69,10 +73,20 @@ namespace pipedal
|
||||
private:
|
||||
std::vector<std::string> inputAudioPorts_;
|
||||
std::vector<std::string> outputAudioPorts_;
|
||||
std::vector<std::string> inputMidiPorts_;
|
||||
std::vector<AlsaMidiDeviceInfo> inputMidiDevices_;
|
||||
public:
|
||||
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 {
|
||||
@@ -83,14 +97,14 @@ namespace pipedal
|
||||
{
|
||||
return inputAudioPorts_;
|
||||
}
|
||||
const std::vector<std::string>& getOutputAudioPorts() const
|
||||
const std::vector<std::string>& GetOutputAudioPorts() const
|
||||
{
|
||||
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;
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -21,12 +21,15 @@
|
||||
|
||||
#include "Lv2Log.hpp"
|
||||
|
||||
#include "JackDriver.hpp"
|
||||
#include "AlsaDriver.hpp"
|
||||
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
#include <jack/jack.h>
|
||||
#include <jack/types.h>
|
||||
#include <jack/session.h>
|
||||
#include <jack/midiport.h>
|
||||
#include "AudioConfig.hpp"
|
||||
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <mutex>
|
||||
@@ -63,11 +66,6 @@ using namespace pipedal;
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -105,9 +103,10 @@ static std::string GetGovernor()
|
||||
|
||||
|
||||
|
||||
class JackHostImpl : public JackHost
|
||||
class JackHostImpl : public JackHost, private AudioDriverHost
|
||||
{
|
||||
private:
|
||||
AudioDriver*audioDriver = nullptr;
|
||||
|
||||
inherit_priority_recursive_mutex mutex;
|
||||
int64_t overrunGracePeriodSamples = 0;
|
||||
@@ -133,36 +132,26 @@ private:
|
||||
|
||||
JackChannelSelection channelSelection;
|
||||
bool active = false;
|
||||
jack_client_t *client = nullptr;
|
||||
|
||||
std::shared_ptr<Lv2PedalBoard> currentPedalBoard;
|
||||
std::vector<std::shared_ptr<Lv2PedalBoard>> activePedalBoards; // pedalboards that have been sent to the audio queue.
|
||||
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;
|
||||
|
||||
uint32_t sampleRate;
|
||||
uint32_t sampleRate = 0;
|
||||
uint64_t currentSample = 0;
|
||||
|
||||
std::atomic<uint64_t> underruns = 0;
|
||||
std::atomic<std::chrono::system_clock::time_point> lastUnderrunTime =
|
||||
std::chrono::system_clock::from_time_t(0);
|
||||
|
||||
int XrunCallback()
|
||||
virtual void OnUnderrun()
|
||||
{
|
||||
++this->underruns;
|
||||
this->lastUnderrunTime = std::chrono::system_clock ::now();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int xrun_callback_fn(void *arg)
|
||||
{
|
||||
return ((JackHostImpl *)arg)->XrunCallback();
|
||||
}
|
||||
|
||||
virtual void Close()
|
||||
{
|
||||
@@ -174,84 +163,13 @@ private:
|
||||
StopReaderThread();
|
||||
if (active)
|
||||
{
|
||||
// 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.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);
|
||||
audioDriver->Deactivate();
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
audioDriver->Close();
|
||||
|
||||
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.
|
||||
this->activePedalBoards.resize(0);
|
||||
this->realtimeActivePedalBoard = nullptr;
|
||||
@@ -268,20 +186,32 @@ private:
|
||||
delete realtimeMonitorPortSubscriptions;
|
||||
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;
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
ZeroBuffer(out, nframes);
|
||||
@@ -487,22 +417,26 @@ private:
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
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;
|
||||
if (jack_midi_event_get(&event, portBuffer, frame) == 0)
|
||||
MidiEvent event;
|
||||
if (audioDriver->GetMidiInputEvent(&event,portBuffer,frame))
|
||||
{
|
||||
eventBufferWriter.writeMidiEvent(iterator, 0, event.size, event.buffer);
|
||||
|
||||
@@ -528,11 +462,26 @@ private:
|
||||
|
||||
#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
|
||||
{
|
||||
jack_default_audio_sample_t *in, *out;
|
||||
float *in, *out;
|
||||
|
||||
pParameterRequests = nullptr;
|
||||
|
||||
@@ -547,9 +496,9 @@ private:
|
||||
float *inputBuffers[4];
|
||||
float *outputBuffers[4];
|
||||
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)
|
||||
{
|
||||
buffersValid = false;
|
||||
@@ -557,11 +506,11 @@ private:
|
||||
}
|
||||
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)
|
||||
{
|
||||
buffersValid = false;
|
||||
@@ -569,7 +518,7 @@ private:
|
||||
}
|
||||
outputBuffers[i] = output;
|
||||
}
|
||||
outputBuffers[outputPorts.size()] = nullptr;
|
||||
outputBuffers[audioDriver->OutputBufferCount()] = nullptr;
|
||||
|
||||
if (buffersValid)
|
||||
{
|
||||
@@ -626,61 +575,9 @@ private:
|
||||
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:
|
||||
JackHostImpl(IHost *pHost)
|
||||
: inputRingBuffer(RING_BUFFER_SIZE),
|
||||
@@ -691,22 +588,29 @@ public:
|
||||
hostWriter(&this->inputRingBuffer),
|
||||
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()
|
||||
{
|
||||
Close();
|
||||
CleanRestartThreads(true);
|
||||
jack_set_error_function(nullptr);
|
||||
jack_set_info_function(nullptr);
|
||||
|
||||
delete audioDriver;
|
||||
|
||||
}
|
||||
|
||||
virtual JackConfiguration GetServerConfiguration()
|
||||
{
|
||||
JackConfiguration result;
|
||||
|
||||
result.Initialize();
|
||||
result.JackInitialize();
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -980,14 +884,13 @@ public:
|
||||
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);
|
||||
if (channelSelection.GetInputAudioPorts().size() == 0
|
||||
|| channelSelection.getOutputAudioPorts().size() == 0)
|
||||
|| channelSelection.GetOutputAudioPorts().size() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1008,162 +911,32 @@ public:
|
||||
|
||||
StartReaderThread();
|
||||
|
||||
jack_status_t status;
|
||||
try
|
||||
{
|
||||
// need a unique instance name every timme.
|
||||
std::stringstream s;
|
||||
s << "PiPedal";
|
||||
if (jackInstanceId != 0) {
|
||||
s << jackInstanceId;
|
||||
}
|
||||
++jackInstanceId;
|
||||
try {
|
||||
|
||||
std::string instanceName = s.str();
|
||||
audioDriver->Open(jackServerSettings,this->channelSelection);
|
||||
|
||||
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);
|
||||
|
||||
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->sampleRate = audioDriver->GetSampleRate();
|
||||
|
||||
this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate)*OVERRUN_GRACE_PERIOD_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());
|
||||
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.GetInputMidiPorts();
|
||||
|
||||
midiLv2Buffers.resize(selectedMidiPorts.size());
|
||||
for (size_t i = 0; i < selectedMidiPorts.size(); ++i)
|
||||
midiLv2Buffers.resize(audioDriver->MidiInputBufferCount());
|
||||
for (size_t i = 0; i < audioDriver->MidiInputBufferCount(); ++i)
|
||||
{
|
||||
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);
|
||||
if (activateResult == 0)
|
||||
{
|
||||
active = true;
|
||||
active = true;
|
||||
audioDriver->Activate();
|
||||
Lv2Log::info("Audio started.");
|
||||
|
||||
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)
|
||||
{
|
||||
Close();
|
||||
active = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -1366,6 +1139,8 @@ private:
|
||||
AdminClient client;
|
||||
|
||||
client.SetJackServerConfiguration(jackServerSettings);
|
||||
|
||||
|
||||
//this_->Open(this_->channelSelection);
|
||||
this_->restarting = false;
|
||||
onComplete(true, "");
|
||||
@@ -1468,16 +1243,12 @@ public:
|
||||
|
||||
result.temperaturemC_ = GetRaspberryPiTemperature();
|
||||
|
||||
result.active_ = this->active;
|
||||
result.active_ = IsAudioActive();
|
||||
result.restarting_ = this->restarting;
|
||||
|
||||
if (client != nullptr)
|
||||
if (this->audioDriver != nullptr)
|
||||
{
|
||||
result.cpuUsage_ = jack_cpu_load(this->client);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.cpuUsage_ = 0;
|
||||
result.cpuUsage_ = audioDriver->CpuUse();
|
||||
}
|
||||
GetCpuFrequency(&result.cpuFreqMax_,&result.cpuFreqMin_);
|
||||
result.governor_ = GetGovernor();
|
||||
@@ -1497,7 +1268,6 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
int JackHostImpl::jackInstanceId = 0;
|
||||
|
||||
JackHost *JackHost::CreateInstance(IHost *pHost)
|
||||
{
|
||||
@@ -1506,6 +1276,7 @@ JackHost *JackHost::CreateInstance(IHost *pHost)
|
||||
|
||||
JSON_MAP_BEGIN(JackHostStatus)
|
||||
JSON_MAP_REFERENCE(JackHostStatus, active)
|
||||
JSON_MAP_REFERENCE(JackHostStatus, errorMessage)
|
||||
JSON_MAP_REFERENCE(JackHostStatus, restarting)
|
||||
JSON_MAP_REFERENCE(JackHostStatus, underruns)
|
||||
JSON_MAP_REFERENCE(JackHostStatus, cpuUsage)
|
||||
|
||||
+2
-1
@@ -106,6 +106,7 @@ public:
|
||||
class JackHostStatus {
|
||||
public:
|
||||
bool active_;
|
||||
std::string errorMessage_;
|
||||
bool restarting_;
|
||||
uint64_t underruns_;
|
||||
float cpuUsage_ = 0;
|
||||
@@ -139,7 +140,7 @@ public:
|
||||
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 uint32_t GetSampleRate() = 0;
|
||||
|
||||
@@ -157,6 +157,7 @@ void JackServerSettings::ReadJackConfiguration()
|
||||
|
||||
void JackServerSettings::Write()
|
||||
{
|
||||
#if JACK_HOST
|
||||
this->valid_ = false;
|
||||
|
||||
std::vector<std::string> precedingLines;
|
||||
@@ -232,6 +233,9 @@ void JackServerSettings::Write()
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
}
|
||||
#else
|
||||
throw PiPedalStateException("JACK_HOST not enabled at compile time.");
|
||||
#endif
|
||||
}
|
||||
|
||||
JSON_MAP_BEGIN(JackServerSettings)
|
||||
|
||||
@@ -33,6 +33,15 @@ namespace pipedal {
|
||||
|
||||
public:
|
||||
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_; }
|
||||
uint32_t GetBufferSize() const { return bufferSize_; }
|
||||
|
||||
+1
-1
@@ -266,7 +266,7 @@ void Lv2Host::OnConfigurationChanged(const JackConfiguration &configuration, con
|
||||
if (configuration.isValid())
|
||||
{
|
||||
this->numberOfAudioInputChannels = settings.GetInputAudioPorts().size();
|
||||
this->numberOfAudioOutputChannels = settings.getOutputAudioPorts().size();
|
||||
this->numberOfAudioOutputChannels = settings.GetOutputAudioPorts().size();
|
||||
this->maxBufferSize = configuration.GetBlockLength();
|
||||
optionsFeature.Prepare(this->mapFeature, configuration.GetSampleRate(), configuration.GetBlockLength(), GetAtomBufferSize());
|
||||
}
|
||||
|
||||
+8
-5
@@ -31,9 +31,12 @@ using namespace pipedal;
|
||||
|
||||
|
||||
static auto timeZero = std::chrono::system_clock::now();
|
||||
bool Lv2Log::show_time_ = true;
|
||||
|
||||
std::string timeTag()
|
||||
{
|
||||
if (!Lv2Log::show_time()) return "";
|
||||
|
||||
using namespace std::chrono;
|
||||
|
||||
auto t = std::chrono::system_clock::now()- timeZero;
|
||||
@@ -47,7 +50,7 @@ std::string timeTag()
|
||||
std::stringstream s;
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
@@ -61,23 +64,23 @@ class StdErrLogger : public Lv2Logger
|
||||
virtual void onError(const char* message)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
std::lock_guard lock(m);
|
||||
std::cerr << timeTag() << " INF: " << message << std::endl;
|
||||
std::cerr << timeTag() << "INF: " << message << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -47,8 +47,10 @@ namespace pipedal
|
||||
class Lv2Log
|
||||
{
|
||||
private:
|
||||
|
||||
static Lv2Logger *logger_;
|
||||
static LogLevel log_level_;
|
||||
static bool show_time_;
|
||||
|
||||
static void v_error(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_;
|
||||
}
|
||||
static void show_time(bool show)
|
||||
{
|
||||
show_time_ = show;
|
||||
}
|
||||
static bool show_time() {
|
||||
return show_time_;
|
||||
}
|
||||
|
||||
// static void error(const char* message) {
|
||||
// if (Lv2Log::log_level_ >= LogLevel::Error)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -167,9 +167,99 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
|
||||
}
|
||||
}
|
||||
snd_config_update_free_global();
|
||||
|
||||
Lv2Log::debug("GetAlsaDevices --");
|
||||
for (auto& device: result)
|
||||
{
|
||||
Lv2Log::debug(SS(" " << device.name_ << " " << device.longName_ << " " << device.cardId_));
|
||||
}
|
||||
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_REFERENCE(AlsaDeviceInfo, cardId)
|
||||
JSON_MAP_REFERENCE(AlsaDeviceInfo, id)
|
||||
@@ -179,3 +269,8 @@ JSON_MAP_REFERENCE(AlsaDeviceInfo, sampleRates)
|
||||
JSON_MAP_REFERENCE(AlsaDeviceInfo, minBufferSize)
|
||||
JSON_MAP_REFERENCE(AlsaDeviceInfo, maxBufferSize)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(AlsaMidiDeviceInfo)
|
||||
JSON_MAP_REFERENCE(AlsaMidiDeviceInfo, name)
|
||||
JSON_MAP_REFERENCE(AlsaMidiDeviceInfo, description)
|
||||
JSON_MAP_END()
|
||||
|
||||
@@ -33,6 +33,16 @@ namespace pipedal {
|
||||
|
||||
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 {
|
||||
@@ -45,4 +55,6 @@ namespace pipedal {
|
||||
|
||||
std::vector<AlsaDeviceInfo> GetAlsaDevices();
|
||||
};
|
||||
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiInputDevices();
|
||||
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiOutputDevices();
|
||||
}
|
||||
@@ -32,7 +32,14 @@ TEST_CASE( "ALSA Test", "[pipedal_alsa_test][Build][Dev]" ) {
|
||||
|
||||
PiPedalAlsaDevices devices;
|
||||
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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ namespace pipedal
|
||||
: PiPedalException(std::forward<std::string>(what))
|
||||
{
|
||||
}
|
||||
PiPedalStateException(const std::string &what)
|
||||
: PiPedalException(what)
|
||||
{
|
||||
}
|
||||
};
|
||||
class PiPedalLogicException : public PiPedalException
|
||||
{
|
||||
|
||||
+42
-10
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "pch.h"
|
||||
#include "DeviceIdFile.hpp"
|
||||
#include "AudioConfig.hpp"
|
||||
#include <sched.h>
|
||||
#include "PiPedalModel.hpp"
|
||||
#include "JackHost.hpp"
|
||||
@@ -49,7 +50,12 @@ std::string AtomToJson(int length, uint8_t *pData)
|
||||
PiPedalModel::PiPedalModel()
|
||||
{
|
||||
this->pedalBoard = PedalBoard::MakeDefault();
|
||||
#if JACK_HOST
|
||||
this->jackServerSettings.ReadJackConfiguration();
|
||||
#else
|
||||
this->jackServerSettings = this->storage.GetJackServerSettings();
|
||||
#endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -108,14 +114,19 @@ PiPedalModel::~PiPedalModel()
|
||||
|
||||
#include <fstream>
|
||||
|
||||
|
||||
void PiPedalModel::LoadLv2PluginInfo(const PiPedalConfiguration&configuration)
|
||||
void PiPedalModel::Init(const PiPedalConfiguration&configuration)
|
||||
{
|
||||
|
||||
this->configuration = configuration;
|
||||
storage.SetConfigRoot(configuration.GetDocRoot());
|
||||
storage.SetDataRoot(configuration.GetLocalStoragePath());
|
||||
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.
|
||||
std::filesystem::path pluginClassesPath = configuration.GetDocRoot() / "plugin_classes.json";
|
||||
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->webPort = (uint16_t)configuration.GetSocketServerPort();
|
||||
this->jackServerSettings.ReadJackConfiguration();
|
||||
|
||||
|
||||
adminClient.MonitorGovernor(storage.GetGovernorSettings());
|
||||
@@ -192,8 +202,12 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
|
||||
scheduler_params.sched_priority = 10;
|
||||
memset(&scheduler_params,0,sizeof(sched_param));
|
||||
sched_setscheduler(0,SCHED_RR,&scheduler_params);
|
||||
|
||||
this->jackConfiguration = jackHost->GetServerConfiguration();
|
||||
#if JACK_HOST
|
||||
this->jackConfiguration = this->jackConfiguration.JackInitialize();
|
||||
#else
|
||||
this->jackServerSettings = storage.GetJackServerSettings();
|
||||
this->jackConfiguration.AlsaInitialize(this->jackServerSettings);
|
||||
#endif
|
||||
if (this->jackConfiguration.isValid())
|
||||
{
|
||||
JackChannelSelection selection = storage.GetJackChannelSelection(jackConfiguration);
|
||||
@@ -202,7 +216,7 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
|
||||
this->lv2Host.OnConfigurationChanged(jackConfiguration, selection);
|
||||
try
|
||||
{
|
||||
jackHost->Open(selection);
|
||||
jackHost->Open(this->jackServerSettings,selection);
|
||||
|
||||
std::shared_ptr<Lv2PedalBoard> lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)};
|
||||
this->lv2PedalBoard = lv2PedalBoard;
|
||||
@@ -212,6 +226,8 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration)
|
||||
{
|
||||
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);
|
||||
if (this->jackHost->IsOpen())
|
||||
{
|
||||
// do a complete reload.
|
||||
|
||||
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->Open(channelSelection);
|
||||
this->jackHost->Open(this->jackServerSettings,channelSelection);
|
||||
std::shared_ptr<Lv2PedalBoard> lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)};
|
||||
this->lv2PedalBoard = lv2PedalBoard;
|
||||
jackHost->SetPedalBoard(lv2PedalBoard);
|
||||
this->UpdateRealtimeVuSubscriptions();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1127,6 +1155,10 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet
|
||||
if (adminClient.CanUseShutdownClient())
|
||||
{
|
||||
|
||||
#if ALSA_HOST
|
||||
storage.SetJackServerSettings(jackServerSettings);
|
||||
#endif
|
||||
|
||||
// save the current (edited) preset now in case the service shutdown isn't clean.
|
||||
CurrentPreset currentPreset;
|
||||
currentPreset.modified_ = this->hasPresetChanged;
|
||||
|
||||
@@ -153,6 +153,8 @@ private: // IJackHostCallbacks
|
||||
virtual void OnNotifyMidiListen(bool isNote, uint8_t noteOrControl);
|
||||
virtual void OnNotifyAtomOutput(uint64_t instanceId, const std::string&atomType,const std::string&atomJson);
|
||||
|
||||
|
||||
PiPedalConfiguration configuration;
|
||||
public:
|
||||
PiPedalModel();
|
||||
virtual ~PiPedalModel();
|
||||
@@ -164,9 +166,10 @@ public:
|
||||
|
||||
AdminClient&GetAdminClient() { return adminClient; }
|
||||
|
||||
void Init(const PiPedalConfiguration&configuration);
|
||||
|
||||
void LoadLv2PluginInfo(const PiPedalConfiguration&configuration);
|
||||
void Load(const PiPedalConfiguration&configuration);
|
||||
void LoadLv2PluginInfo();
|
||||
void Load();
|
||||
|
||||
const Lv2Host& GetLv2Host() const { return lv2Host; }
|
||||
PedalBoard GetCurrentPedalBoardCopy()
|
||||
|
||||
@@ -52,6 +52,10 @@ namespace pipedal
|
||||
{
|
||||
lineBuffer.reserve(120);
|
||||
}
|
||||
|
||||
size_t width() const { return lineWidth; }
|
||||
void width(size_t width) { lineWidth =width; }
|
||||
|
||||
std::ostream&Stream() const { return s;}
|
||||
|
||||
PrettyPrinter& Indent(size_t indent)
|
||||
@@ -67,6 +71,21 @@ namespace pipedal
|
||||
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()
|
||||
{
|
||||
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
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "pch.h"
|
||||
#include "Storage.hpp"
|
||||
#include "AudioConfig.hpp"
|
||||
#include "PiPedalException.hpp"
|
||||
#include <sstream>
|
||||
#include "json.hpp"
|
||||
@@ -35,8 +36,6 @@ const char *BANKS_FILENAME = "index.banks";
|
||||
#define WIFI_CONFIG_SETTINGS_FILENAME "wifiConfigSettings.json";
|
||||
#define USER_SETTINGS_FILENAME "userSettings.json";
|
||||
|
||||
|
||||
|
||||
Storage::Storage()
|
||||
{
|
||||
SetConfigRoot("~/var/Config");
|
||||
@@ -131,7 +130,7 @@ std::string Storage::SafeEncodeName(const std::string &name)
|
||||
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())
|
||||
return path;
|
||||
@@ -161,28 +160,27 @@ std::filesystem::path ResolveHomePath(const std::filesystem::path& path)
|
||||
return result;
|
||||
}
|
||||
|
||||
void Storage::SetConfigRoot(const std::filesystem::path& path)
|
||||
void Storage::SetConfigRoot(const std::filesystem::path &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);
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
std::filesystem::path sourceFile = directoryEntry.path();
|
||||
std::filesystem::path destFile = destination / sourceFile.filename();
|
||||
|
||||
std::filesystem::copy_file(sourceFile,destFile);
|
||||
std::ignore = chmod(destFile.c_str(),S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
|
||||
|
||||
}
|
||||
std::filesystem::copy_file(sourceFile, destFile);
|
||||
std::ignore = chmod(destFile.c_str(), S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
void Storage::MaybeCopyDefaultPresets()
|
||||
@@ -192,18 +190,15 @@ void Storage::MaybeCopyDefaultPresets()
|
||||
if (!std::filesystem::exists(presetsDirectory / "index.banks"))
|
||||
{
|
||||
CopyDirectory(this->configRoot / "default_presets" / "presets",
|
||||
presetsDirectory
|
||||
);
|
||||
presetsDirectory);
|
||||
}
|
||||
|
||||
auto pluginDirectory = this->GetPluginPresetsDirectory();
|
||||
if (!std::filesystem::exists(pluginDirectory / "index.json"))
|
||||
{
|
||||
CopyDirectory(this->configRoot / "default_presets" / "plugin_presets",
|
||||
pluginDirectory
|
||||
);
|
||||
pluginDirectory);
|
||||
}
|
||||
|
||||
}
|
||||
void Storage::Initialize()
|
||||
{
|
||||
@@ -233,7 +228,6 @@ void Storage::Initialize()
|
||||
LoadUserSettings();
|
||||
}
|
||||
|
||||
|
||||
void Storage::LoadBank(int64_t instanceId)
|
||||
{
|
||||
auto indexEntry = this->bankIndex.getBankIndexEntry(instanceId);
|
||||
@@ -265,7 +259,6 @@ std::filesystem::path Storage::GetCurrentPresetPath() const
|
||||
return this->dataRoot / "currentPreset.json";
|
||||
}
|
||||
|
||||
|
||||
std::filesystem::path Storage::GetChannelSelectionFileName()
|
||||
{
|
||||
return this->dataRoot / "JackChannelSelection.json";
|
||||
@@ -304,7 +297,7 @@ void Storage::LoadBankIndex()
|
||||
|
||||
std::string name = "Default Bank";
|
||||
SaveBankFile(name, currentBank);
|
||||
int64_t selectedBank = bankIndex.addBank(-1,name);
|
||||
int64_t selectedBank = bankIndex.addBank(-1, name);
|
||||
bankIndex.selectedBank(selectedBank);
|
||||
}
|
||||
SaveBankIndex();
|
||||
@@ -365,7 +358,7 @@ void Storage::ReIndex()
|
||||
if (path.extension() == BANK_EXTENSION)
|
||||
{
|
||||
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();
|
||||
bankFile.selectedPreset(instanceId);
|
||||
SaveBankFile(name, bankFile);
|
||||
this->bankIndex.addBank(-1,name);
|
||||
this->bankIndex.addBank(-1, name);
|
||||
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 name = indexEntry.name();
|
||||
std::filesystem::path fileName = GetBankFileName(name);
|
||||
@@ -407,7 +401,6 @@ void Storage::LoadBankFile(const std::string &name, BankFile *pBank)
|
||||
std::ifstream is(fileName);
|
||||
json_reader reader(is);
|
||||
reader.read(pBank);
|
||||
|
||||
}
|
||||
|
||||
void Storage::SaveBankFile(const std::string &name, const BankFile &bankFile)
|
||||
@@ -459,8 +452,9 @@ const PedalBoard &Storage::GetCurrentPreset()
|
||||
bool Storage::LoadPreset(int64_t instanceId)
|
||||
{
|
||||
if (!currentBank.hasItem(instanceId))
|
||||
return false;
|
||||
if (instanceId != currentBank.selectedPreset()) {
|
||||
return false;
|
||||
if (instanceId != currentBank.selectedPreset())
|
||||
{
|
||||
currentBank.selectedPreset(instanceId);
|
||||
SaveCurrentBank();
|
||||
}
|
||||
@@ -489,13 +483,13 @@ void Storage::SetPresetIndex(const PresetIndex &presets)
|
||||
std::map<int64_t, std::unique_ptr<BankFileEntry> *> entries;
|
||||
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]);
|
||||
}
|
||||
std::vector<std::unique_ptr<BankFileEntry>*> newPresets;
|
||||
std::vector<std::unique_ptr<BankFileEntry> *> newPresets;
|
||||
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)
|
||||
{
|
||||
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 newSelection = currentBank.deletePreset(presetId);
|
||||
int64_t newSelection = currentBank.deletePreset(presetId);
|
||||
SaveCurrentBank();
|
||||
return newSelection;
|
||||
}
|
||||
@@ -724,7 +718,7 @@ int64_t Storage::SaveBankAs(int64_t bankId, const std::string &newName)
|
||||
std::filesystem::path newPath = this->GetBankFileName(newName);
|
||||
try
|
||||
{
|
||||
std::filesystem::copy(oldPath,newPath);
|
||||
std::filesystem::copy(oldPath, newPath);
|
||||
}
|
||||
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() << ")";
|
||||
throw PiPedalException(s.str());
|
||||
}
|
||||
int64_t newId = this->bankIndex.addBank(bankId,newName);
|
||||
int64_t newId = this->bankIndex.addBank(bankId, newName);
|
||||
SaveBankIndex();
|
||||
return newId;
|
||||
}
|
||||
|
||||
void Storage::MoveBank(int from, int to)
|
||||
{
|
||||
this->bankIndex.move(from,to);
|
||||
this->bankIndex.move(from, to);
|
||||
this->SaveBankIndex();
|
||||
}
|
||||
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)
|
||||
{
|
||||
auto & entry = entries[i];
|
||||
auto &entry = entries[i];
|
||||
|
||||
if (entry.instanceId() == bankId) {
|
||||
if (entry.instanceId() == bankId)
|
||||
{
|
||||
std::filesystem::path fileName = this->GetBankFileName(entry.name());
|
||||
entries.erase(entries.begin()+i);
|
||||
entries.erase(entries.begin() + i);
|
||||
|
||||
int64_t newSelection;
|
||||
if (i < entries.size())
|
||||
{
|
||||
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?
|
||||
// Create a default empty bank.
|
||||
BankIndexEntry newEntry;
|
||||
@@ -773,7 +771,7 @@ int64_t Storage::DeleteBank(int64_t bankId)
|
||||
|
||||
std::string name = "Default Bank";
|
||||
SaveBankFile(name, defaultBank);
|
||||
int64_t selectedBank = bankIndex.addBank(-1,name);
|
||||
int64_t selectedBank = bankIndex.addBank(-1, name);
|
||||
newSelection = selectedBank;
|
||||
}
|
||||
if (this->bankIndex.selectedBank() == bankId)
|
||||
@@ -793,7 +791,7 @@ int64_t Storage::GetCurrentPresetId() const
|
||||
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();
|
||||
if (uploadAfter != -1)
|
||||
@@ -801,7 +799,8 @@ int64_t Storage::UploadPreset(const BankFile&bankFile,int64_t uploadAfter)
|
||||
lastPreset = uploadAfter;
|
||||
}
|
||||
|
||||
if (bankFile.presets().size() == 0) {
|
||||
if (bankFile.presets().size() == 0)
|
||||
{
|
||||
throw PiPedalException("Invalid preset.");
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
lastPreset = this->currentBank.addPreset(preset,lastPreset);
|
||||
lastPreset = this->currentBank.addPreset(preset, lastPreset);
|
||||
}
|
||||
this->SaveCurrentBank();
|
||||
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();
|
||||
if (uploadAfter != -1)
|
||||
@@ -830,7 +829,8 @@ int64_t Storage::UploadBank(BankFile&bankFile,int64_t uploadAfter)
|
||||
lastBank = uploadAfter;
|
||||
}
|
||||
|
||||
if (bankFile.presets().size() == 0) {
|
||||
if (bankFile.presets().size() == 0)
|
||||
{
|
||||
throw PiPedalException("Invalid bank.");
|
||||
}
|
||||
bankFile.updateNextIndex();
|
||||
@@ -845,24 +845,25 @@ int64_t Storage::UploadBank(BankFile&bankFile,int64_t uploadAfter)
|
||||
}
|
||||
std::filesystem::path path = this->GetBankFileName(bankFile.name());
|
||||
std::ofstream f(path);
|
||||
if (!f.is_open()) {
|
||||
if (!f.is_open())
|
||||
{
|
||||
throw PiPedalException("Can't write to bank file.");
|
||||
}
|
||||
json_writer writer(f);
|
||||
writer.write(bankFile);
|
||||
|
||||
lastBank = this->bankIndex.addBank(lastBank,bankFile.name());
|
||||
lastBank = this->bankIndex.addBank(lastBank, bankFile.name());
|
||||
this->SaveBankIndex();
|
||||
return lastBank;
|
||||
}
|
||||
|
||||
|
||||
void Storage::SetGovernorSettings(const std::string & governor)
|
||||
void Storage::SetGovernorSettings(const std::string &governor)
|
||||
{
|
||||
userSettings.governor_ = governor;
|
||||
SaveUserSettings();
|
||||
}
|
||||
void Storage::SaveUserSettings() {
|
||||
void Storage::SaveUserSettings()
|
||||
{
|
||||
std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME;
|
||||
{
|
||||
std::ofstream f(path);
|
||||
@@ -889,8 +890,7 @@ std::string Storage::GetGovernorSettings() const
|
||||
return this->userSettings.governor_;
|
||||
}
|
||||
|
||||
|
||||
void Storage::SetWifiConfigSettings(const WifiConfigSettings & wifiConfigSettings)
|
||||
void Storage::SetWifiConfigSettings(const WifiConfigSettings &wifiConfigSettings)
|
||||
{
|
||||
WifiConfigSettings copyToSave = wifiConfigSettings;
|
||||
copyToSave.rebootRequired_ = false;
|
||||
@@ -915,19 +915,21 @@ void Storage::SetWifiConfigSettings(const WifiConfigSettings & wifiConfigSetting
|
||||
if (copyToStore.enable_)
|
||||
{
|
||||
copyToStore.hasPassword_ = copyToStore.password_.length() != 0 || this->wifiConfigSettings.hasPassword_;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
copyToStore.hasPassword_ = false;
|
||||
}
|
||||
copyToStore.password_ = "";
|
||||
copyToStore.rebootRequired_ = false;
|
||||
if (copyToStore.enable_ && !copyToStore.hasPassword_)
|
||||
if (copyToStore.enable_ && !copyToStore.hasPassword_)
|
||||
{
|
||||
copyToStore.hasPassword_ = this->wifiConfigSettings.hasPassword_;
|
||||
}
|
||||
this->wifiConfigSettings = copyToStore;
|
||||
}
|
||||
|
||||
void Storage::SetWifiDirectConfigSettings(const WifiDirectConfigSettings & wifiDirectConfigSettings)
|
||||
void Storage::SetWifiDirectConfigSettings(const WifiDirectConfigSettings &wifiDirectConfigSettings)
|
||||
{
|
||||
WifiDirectConfigSettings copyToSave = wifiDirectConfigSettings;
|
||||
copyToSave.rebootRequired_ = false;
|
||||
@@ -945,7 +947,8 @@ void Storage::LoadUserSettings()
|
||||
{
|
||||
std::filesystem::path path = this->dataRoot / USER_SETTINGS_FILENAME;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
if (std::filesystem::is_regular_file(path))
|
||||
{
|
||||
std::ifstream f(path);
|
||||
@@ -956,17 +959,18 @@ void Storage::LoadUserSettings()
|
||||
json_reader reader(f);
|
||||
reader.read(&userSettings);
|
||||
}
|
||||
} catch (const std::exception&)
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
}
|
||||
this->wifiConfigSettings.valid_ = true;
|
||||
|
||||
}
|
||||
void Storage::LoadWifiConfigSettings()
|
||||
{
|
||||
std::filesystem::path path = this->dataRoot / WIFI_CONFIG_SETTINGS_FILENAME;
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
if (std::filesystem::is_regular_file(path))
|
||||
{
|
||||
std::ifstream f(path);
|
||||
@@ -979,9 +983,9 @@ void Storage::LoadWifiConfigSettings()
|
||||
reader.read(&wifiConfigSettings);
|
||||
this->wifiConfigSettings = wifiConfigSettings;
|
||||
}
|
||||
} catch (const std::exception&)
|
||||
}
|
||||
catch (const std::exception &)
|
||||
{
|
||||
|
||||
}
|
||||
this->wifiConfigSettings.valid_ = true;
|
||||
}
|
||||
@@ -996,8 +1000,6 @@ void Storage::LoadWifiDirectConfigSettings()
|
||||
this->wifiDirectConfigSettings = settings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
WifiConfigSettings Storage::GetWifiConfigSettings()
|
||||
{
|
||||
return this->wifiConfigSettings;
|
||||
@@ -1007,43 +1009,47 @@ WifiDirectConfigSettings Storage::GetWifiDirectConfigSettings()
|
||||
return this->wifiDirectConfigSettings;
|
||||
}
|
||||
|
||||
|
||||
void Storage::SaveCurrentPreset(const CurrentPreset ¤tPreset)
|
||||
{
|
||||
try {
|
||||
try
|
||||
{
|
||||
std::filesystem::path path = GetCurrentPresetPath();
|
||||
|
||||
std::ofstream f(path);
|
||||
json_writer writer(f);
|
||||
writer.write(currentPreset);
|
||||
} catch (std::exception&)
|
||||
}
|
||||
catch (std::exception &)
|
||||
{
|
||||
// called from destructor. Must be nothrow().
|
||||
}
|
||||
|
||||
}
|
||||
bool Storage::RestoreCurrentPreset(CurrentPreset*pResult)
|
||||
bool Storage::RestoreCurrentPreset(CurrentPreset *pResult)
|
||||
{
|
||||
std::filesystem::path path = GetCurrentPresetPath();
|
||||
if (std::filesystem::exists(path)) {
|
||||
try {
|
||||
if (std::filesystem::exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
std::ifstream f(path);
|
||||
json_reader reader(f);
|
||||
reader.read(pResult);
|
||||
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 true;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
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)
|
||||
{
|
||||
@@ -1055,7 +1061,7 @@ bool Storage::HasPluginPresets(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)
|
||||
{
|
||||
@@ -1063,9 +1069,8 @@ std::filesystem::path Storage::GetPluginPresetPath(const std::string &pluginUri)
|
||||
}
|
||||
}
|
||||
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::filesystem::path path;
|
||||
@@ -1075,10 +1080,12 @@ void Storage::SavePluginPresets(const std::string&pluginUri, const PluginPresets
|
||||
name = SS(pluginPresetIndex.nextInstanceId_ << ".json");
|
||||
path = GetPluginPresetsDirectory() / name;
|
||||
presetAdded = true;
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
path = GetPluginPresetPath(pluginUri);
|
||||
}
|
||||
auto tempPath = path.string()+".$$$";
|
||||
auto tempPath = path.string() + ".$$$";
|
||||
{
|
||||
std::ofstream os;
|
||||
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::rename(tempPath,path);
|
||||
std::filesystem::rename(tempPath, path);
|
||||
|
||||
if (presetAdded)
|
||||
{
|
||||
pluginPresetIndex.entries_.push_back(
|
||||
PluginPresetIndexEntry(
|
||||
pluginUri,name
|
||||
)
|
||||
);
|
||||
pluginUri, name));
|
||||
pluginPresetIndex.nextInstanceId_++;
|
||||
this->pluginPresetIndexChanged = true;
|
||||
SavePluginPresetIndex();
|
||||
}
|
||||
}
|
||||
|
||||
PluginPresets Storage::GetPluginPresets(const std::string&pluginUri) const
|
||||
PluginPresets Storage::GetPluginPresets(const std::string &pluginUri) const
|
||||
{
|
||||
PluginPresets result;
|
||||
if (!HasPluginPresets(pluginUri))
|
||||
@@ -1127,36 +1132,33 @@ PluginPresets Storage::GetPluginPresets(const std::string&pluginUri) const
|
||||
reader.read(&result);
|
||||
return result;
|
||||
}
|
||||
PluginUiPresets Storage::GetPluginUiPresets(const std::string&pluginUri) const
|
||||
PluginUiPresets Storage::GetPluginUiPresets(const std::string &pluginUri) const
|
||||
{
|
||||
PluginPresets presets = GetPluginPresets(pluginUri);
|
||||
PluginUiPresets result;
|
||||
result.pluginUri_ = presets.pluginUri_;
|
||||
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(
|
||||
PluginUiPreset
|
||||
{
|
||||
PluginUiPreset{
|
||||
preset.label_,
|
||||
preset.instanceId_
|
||||
}
|
||||
);
|
||||
preset.instanceId_});
|
||||
}
|
||||
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);
|
||||
for (const auto & preset: presets.presets_)
|
||||
for (const auto &preset : presets.presets_)
|
||||
{
|
||||
if (preset.instanceId_ == instanceId)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -1165,16 +1167,16 @@ std::vector<ControlValue> Storage::GetPluginPresetValues(const std::string&plugi
|
||||
}
|
||||
|
||||
uint64_t Storage::SavePluginPreset(
|
||||
const std::string&pluginUri,
|
||||
const std::string&name,
|
||||
const std::map<std::string,float> & values)
|
||||
const std::string &pluginUri,
|
||||
const std::string &name,
|
||||
const std::map<std::string, float> &values)
|
||||
{
|
||||
auto presets = GetPluginPresets(pluginUri);
|
||||
uint64_t result = -1;
|
||||
bool existing = false;
|
||||
for (size_t i = 0; i < presets.presets_.size(); ++i)
|
||||
{
|
||||
auto & preset = presets.presets_[i];
|
||||
auto &preset = presets.presets_[i];
|
||||
if (preset.label_ == name)
|
||||
{
|
||||
preset.controlValues_ = values;
|
||||
@@ -1190,10 +1192,9 @@ uint64_t Storage::SavePluginPreset(
|
||||
PluginPreset(
|
||||
result,
|
||||
name,
|
||||
values
|
||||
));
|
||||
values));
|
||||
}
|
||||
this->SavePluginPresets(pluginUri,presets);
|
||||
this->SavePluginPresets(pluginUri, presets);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1201,26 +1202,24 @@ void Storage::UpdatePluginPresets(const PluginUiPresets &pluginPresets)
|
||||
{
|
||||
// handles deletions, renaming, and reordering only.
|
||||
// If you need to add a preset, you neet to call SavePluginPreset or DulicatePluginPreset instead.
|
||||
|
||||
|
||||
PluginPresets presets = this->GetPluginPresets(pluginPresets.pluginUri_);
|
||||
PluginPresets newPresets;
|
||||
newPresets.pluginUri_ = pluginPresets.pluginUri_;
|
||||
newPresets.nextInstanceId_ = presets.nextInstanceId_;
|
||||
|
||||
for (const auto&preset: pluginPresets.presets_)
|
||||
for (const auto &preset : pluginPresets.presets_)
|
||||
{
|
||||
PluginPreset newPreset = presets.GetPreset(preset.instanceId_);
|
||||
newPreset.label_ = preset.label_;
|
||||
newPresets.presets_.push_back(std::move(newPreset));
|
||||
}
|
||||
SavePluginPresets(newPresets.pluginUri_,newPresets);
|
||||
|
||||
|
||||
SavePluginPresets(newPresets.pluginUri_, newPresets);
|
||||
}
|
||||
|
||||
static std::string stripCopySuffix(const std::string &s)
|
||||
{
|
||||
int pos = s.length()-1;
|
||||
int pos = s.length() - 1;
|
||||
if (pos >= 0 && s[pos] == ')')
|
||||
{
|
||||
--pos;
|
||||
@@ -1228,7 +1227,7 @@ static std::string stripCopySuffix(const std::string &s)
|
||||
{
|
||||
--pos;
|
||||
}
|
||||
if (pos >= 0 && s[pos] == '(')
|
||||
if (pos >= 0 && s[pos] == '(')
|
||||
{
|
||||
--pos;
|
||||
}
|
||||
@@ -1236,11 +1235,11 @@ static std::string stripCopySuffix(const std::string &s)
|
||||
{
|
||||
--pos;
|
||||
}
|
||||
return s.substr(0,pos+1);
|
||||
return s.substr(0, pos + 1);
|
||||
}
|
||||
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);
|
||||
size_t pos = presets.Find(presetId);
|
||||
@@ -1266,17 +1265,14 @@ uint64_t Storage::CopyPluginPreset(const std::string&pluginUri,uint64_t presetId
|
||||
}
|
||||
}
|
||||
t.label_ = name;
|
||||
presets.presets_.insert(presets.presets_.begin()+pos+1,t);
|
||||
SavePluginPresets(presets.pluginUri_,presets);
|
||||
presets.presets_.insert(presets.presets_.begin() + pos + 1, t);
|
||||
SavePluginPresets(presets.pluginUri_, presets);
|
||||
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";
|
||||
if (!std::filesystem::exists(fileName))
|
||||
@@ -1291,10 +1287,9 @@ std::map<std::string,bool> Storage::GetFavorites() const
|
||||
reader.read(&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::ofstream f;
|
||||
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_REFERENCE(UserSettings,governor)
|
||||
JSON_MAP_REFERENCE(UserSettings,showStatusMonitor)
|
||||
JSON_MAP_REFERENCE(UserSettings, governor)
|
||||
JSON_MAP_REFERENCE(UserSettings, showStatusMonitor)
|
||||
JSON_MAP_END()
|
||||
|
||||
JSON_MAP_BEGIN(CurrentPreset)
|
||||
JSON_MAP_REFERENCE(CurrentPreset,modified)
|
||||
JSON_MAP_REFERENCE(CurrentPreset,preset)
|
||||
JSON_MAP_REFERENCE(CurrentPreset, modified)
|
||||
JSON_MAP_REFERENCE(CurrentPreset, preset)
|
||||
JSON_MAP_END()
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "PluginPreset.hpp"
|
||||
#include "Banks.hpp"
|
||||
#include "JackConfiguration.hpp"
|
||||
#include "JackServerSettings.hpp"
|
||||
#include "WifiConfigSettings.hpp"
|
||||
#include "WifiDirectConfigSettings.hpp"
|
||||
#include <map>
|
||||
@@ -100,6 +101,8 @@ public:
|
||||
|
||||
const BankIndex & GetBanks() const { return bankIndex; }
|
||||
|
||||
JackServerSettings GetJackServerSettings();
|
||||
void SetJackServerSettings(const pipedal::JackServerSettings&jackServerSettings);
|
||||
|
||||
void LoadWifiConfigSettings();
|
||||
void LoadWifiDirectConfigSettings();
|
||||
|
||||
+79
-56
@@ -18,6 +18,8 @@
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
#include "pch.h"
|
||||
|
||||
#include "AudioConfig.hpp"
|
||||
#include "BeastServer.hpp"
|
||||
#include <iostream>
|
||||
#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.
|
||||
*/
|
||||
|
||||
@@ -488,7 +490,6 @@ static bool isJackServiceRunning()
|
||||
return std::filesystem::exists(path);
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
|
||||
@@ -621,39 +622,10 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
PiPedalModel model;
|
||||
|
||||
// Pre-populate ALSA device info,wait for jackd service (daemon only)
|
||||
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)
|
||||
model.Init(configuration);
|
||||
|
||||
auto serverSettings = model.GetJackServerSettings();
|
||||
if (serverSettings.IsValid())
|
||||
{
|
||||
// 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);
|
||||
// Get heavy IO out of the way before letting dependent (Jack/ALSA) services run.
|
||||
model.LoadLv2PluginInfo();
|
||||
if (systemd)
|
||||
{
|
||||
// Tell systemd we're done.
|
||||
@@ -662,34 +634,85 @@ int main(int argc, char *argv[])
|
||||
(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);
|
||||
if (isJackServiceRunning())
|
||||
auto serverSettings = model.GetJackServerSettings();
|
||||
|
||||
|
||||
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())
|
||||
{
|
||||
Lv2Log::info("Found Jack service.");
|
||||
sleep(3); // jack needs a little time to get up to speed.
|
||||
}
|
||||
else
|
||||
{
|
||||
Lv2Log::info("Jack service not started.");
|
||||
|
||||
// pre-cache device info before we let audio services run.
|
||||
model.GetAlsaDevices();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ Description=Jack Audio Daemon
|
||||
After=sound.target
|
||||
After=local-fs.target
|
||||
After=dbus.socket
|
||||
After=pipedald
|
||||
|
||||
|
||||
[Service]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
[Unit]
|
||||
Description=${DESCRIPTION}
|
||||
Before=jack.service
|
||||
After=network.target
|
||||
After=sound.target
|
||||
After=local-fs.target
|
||||
|
||||
Reference in New Issue
Block a user