From 0a67b3910375290178b325cc01d229ce1d19929a Mon Sep 17 00:00:00 2001 From: Robin Davies Date: Fri, 1 Jul 2022 21:15:57 -0400 Subject: [PATCH] 0.9.11-rc-2 Switch from JACK to ALSA audio. --- .vscode/launch.json | 40 +- CMakeLists.txt | 8 +- README.md | 2 +- docs/Installing.md | 4 +- docs/download.md | 4 +- docs/index.md | 2 +- react/CMakeLists.txt | 1 + react/src/AlsaMidiDeviceInfo.tsx | 45 + react/src/Jack.tsx | 13 +- react/src/JackHostStatus.tsx | 4 +- react/src/JackServerSettingsDialog.tsx | 2 +- react/src/MidiBindingsDialog.tsx | 2 +- react/src/PiPedalModel.tsx | 6 + react/src/SelectMidiChannelsDialog.tsx | 35 +- react/src/SettingsDialog.tsx | 19 +- src/AdminMain.cpp | 6 + src/AlsaDriver.cpp | 1686 +++++++++++++++++++++++ src/AlsaDriver.hpp | 43 + src/AlsaDriverTest.cpp | 278 ++++ src/AudioConfig.hpp | 32 + src/AudioDriver.hpp | 89 ++ src/CMakeLists.txt | 35 +- src/CommandLineParser.hpp | 37 + src/ConfigMain.cpp | 48 +- src/CpuUse.cpp | 38 + src/CpuUse.hpp | 167 +++ src/JackConfiguration.cpp | 140 +- src/JackConfiguration.hpp | 32 +- src/JackDriver.cpp | 512 +++++++ src/JackDriver.hpp | 38 + src/JackHost.cpp | 417 ++---- src/JackHost.hpp | 3 +- src/JackServerSettings.cpp | 4 + src/JackServerSettings.hpp | 9 + src/Lv2Host.cpp | 2 +- src/Lv2Log.cpp | 13 +- src/Lv2Log.hpp | 9 + src/PiLatencyMain.cpp | 474 +++++++ src/PiPedalAlsa.cpp | 95 ++ src/PiPedalAlsa.hpp | 12 + src/PiPedalAlsaTest.cpp | 9 +- src/PiPedalException.hpp | 4 + src/PiPedalModel.cpp | 52 +- src/PiPedalModel.hpp | 7 +- src/PrettyPrinter.hpp | 27 + src/Storage.cpp | 277 ++-- src/Storage.hpp | 3 + src/main.cpp | 135 +- src/templates/jack.service.template | 1 + src/templates/pipedald.service.template | 1 - todo.txt | 11 + 51 files changed, 4301 insertions(+), 632 deletions(-) create mode 100644 react/src/AlsaMidiDeviceInfo.tsx create mode 100644 src/AlsaDriver.cpp create mode 100644 src/AlsaDriver.hpp create mode 100644 src/AlsaDriverTest.cpp create mode 100644 src/AudioConfig.hpp create mode 100644 src/AudioDriver.hpp create mode 100644 src/CpuUse.cpp create mode 100644 src/CpuUse.hpp create mode 100644 src/JackDriver.cpp create mode 100644 src/JackDriver.hpp create mode 100644 src/PiLatencyMain.cpp diff --git a/.vscode/launch.json b/.vscode/launch.json index 129fb6b..22a21e2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/CMakeLists.txt b/CMakeLists.txt index c31133d..a723a3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/README.md b/README.md index 5667402..028e6cd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -Download: v0.9.10-rc-1      Website: [https://rerdavies.github.io/pipedal](https://rerdavies.github.io/pipedal). +Download: v0.9.11-rc-2      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. diff --git a/docs/Installing.md b/docs/Installing.md index b21c45a..d61f3ea 100644 --- a/docs/Installing.md +++ b/docs/Installing.md @@ -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). diff --git a/docs/download.md b/docs/download.md index f7053d0..cbab4ab 100644 --- a/docs/download.md +++ b/docs/download.md @@ -4,14 +4,14 @@ Download the most recent Debian (.deb) package for your platform: -- [Ubuntu or Raspberry Pi OS (64-bit)](https://github.com/rerdavies/pipedal/releases/download/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. diff --git a/docs/index.md b/docs/index.md index 180c834..67be215 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ -v0.9.10-rc-1 +v0.9.11-rc-2 Use your Raspberry Pi as a guitar effects pedal. Configure and control PiPedal with your phone or tablet. diff --git a/react/CMakeLists.txt b/react/CMakeLists.txt index 7ab2011..57c5e3a 100644 --- a/react/CMakeLists.txt +++ b/react/CMakeLists.txt @@ -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 diff --git a/react/src/AlsaMidiDeviceInfo.tsx b/react/src/AlsaMidiDeviceInfo.tsx new file mode 100644 index 0000000..87ab63d --- /dev/null +++ b/react/src/AlsaMidiDeviceInfo.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; + } +} \ No newline at end of file diff --git a/react/src/Jack.tsx b/react/src/Jack.tsx index 19b6a72..ebca391 100644 --- a/react/src/Jack.tsx +++ b/react/src/Jack.tsx @@ -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[] = []; + }; diff --git a/react/src/JackHostStatus.tsx b/react/src/JackHostStatus.tsx index 2efb61d..7adbd4d 100644 --- a/react/src/JackHostStatus.tsx +++ b/react/src/JackHostStatus.tsx @@ -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 { {label} - Stopped   + {status.errorMessage === "" ? "Stopped" : status.errorMessage}   { status.temperaturemC > -100000 && diff --git a/react/src/JackServerSettingsDialog.tsx b/react/src/JackServerSettingsDialog.tsx index aaaa158..f8cac15 100644 --- a/react/src/JackServerSettingsDialog.tsx +++ b/react/src/JackServerSettingsDialog.tsx @@ -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; diff --git a/react/src/MidiBindingsDialog.tsx b/react/src/MidiBindingsDialog.tsx index 9327362..fab5785 100644 --- a/react/src/MidiBindingsDialog.tsx +++ b/react/src/MidiBindingsDialog.tsx @@ -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(); diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index a45c7d0..7e7b185 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -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 { diff --git a/react/src/SelectMidiChannelsDialog.tsx b/react/src/SelectMidiChannelsDialog.tsx index 0b6ef5a..832e0e9 100644 --- a/react/src/SelectMidiChannelsDialog.tsx +++ b/react/src/SelectMidiChannelsDialog.tsx @@ -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 ( - Select Channels + Select MIDI Device {availableChannels.map((channel) => ( toggleSelect(channel)} key={channel} /> + toggleSelect(channel)} key={channel.name} /> } - label={channel} + label={channel.description} /> ) diff --git a/react/src/SettingsDialog.tsx b/react/src/SettingsDialog.tsx index a0e36a2..29761ab 100644 --- a/react/src/SettingsDialog.tsx +++ b/react/src/SettingsDialog.tsx @@ -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 })( this.handleMidiSelection()} >
- Select MIDI input channels + Select MIDI input {this.midiSummary()}
@@ -767,9 +768,9 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( (this.state.showMidiSelectDialog) && ( 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} /> ) diff --git a/src/AdminMain.cpp b/src/AdminMain.cpp index c26db87..3b68132 100644 --- a/src/AdminMain.cpp +++ b/src/AdminMain.cpp @@ -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(); diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp new file mode 100644 index 0000000..a49bf3c --- /dev/null +++ b/src/AlsaDriver.cpp @@ -0,0 +1,1686 @@ +/* + * 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 +#include +#include "ss.hpp" +#include "AlsaDriver.hpp" +#include "JackServerSettings.hpp" +#include + +#include "CpuUse.hpp" + +#include + +#include "Lv2Log.hpp" +#include + +#undef ALSADRIVER_CONFIG_DBG + +#ifdef ALSADRIVER_CONFIG_DBG +#include +#endif + +using namespace pipedal; + +namespace pipedal +{ + + struct AudioFormat + { + char name[40]; + snd_pcm_format_t pcm_format; + }; + [[noreturn]] static void AlsaError(const std::string &message) + { + throw PiPedalStateException(message); + } + + static bool SetPreferredAlsaFormat( + const char *streamType, + snd_pcm_t *handle, + snd_pcm_hw_params_t *hwParams, + AudioFormat *formats, + size_t nItems) + { + for (size_t i = 0; i < nItems; ++i) + { + int err = snd_pcm_hw_params_set_format(handle, hwParams, formats[i].pcm_format); + if (err == 0) + { + return true; + } + } + return false; + } + + static void SetPreferredAlsaFormat( + const std::string &alsa_device_name, + const char *streamType, + snd_pcm_t *handle, + snd_pcm_hw_params_t *hwParams) + { + int err; + + static AudioFormat leFormats[]{ + {"32-bit float little-endian", SND_PCM_FORMAT_FLOAT_LE}, + {"24-bit little-endian in 3bytes format", SND_PCM_FORMAT_S24_3LE}, + {"32-bit integer little-endian", SND_PCM_FORMAT_S32_LE}, + {"24-bit little-endian", SND_PCM_FORMAT_S24_LE}, + {"16-bit little-endian", SND_PCM_FORMAT_S16_LE}, + + }; + static AudioFormat beFormats[]{ + {"32-bit float big-endian", SND_PCM_FORMAT_FLOAT_BE}, + {"24-bit big-endian in 3bytes format", SND_PCM_FORMAT_S24_3BE}, + {"32-bit integer big-endian", SND_PCM_FORMAT_S32_BE}, + {"24-bit big-endian", SND_PCM_FORMAT_S24_BE}, + {"16-bit big-endian", SND_PCM_FORMAT_S16_BE}, + }; + + if (std::endian::native == std::endian::big) + { + if (SetPreferredAlsaFormat(streamType, handle, hwParams, beFormats, sizeof(beFormats) / sizeof(beFormats[0]))) + return; + if (SetPreferredAlsaFormat(streamType, handle, hwParams, leFormats, sizeof(leFormats) / sizeof(leFormats[0]))) + return; + } + else + { + if (SetPreferredAlsaFormat(streamType, handle, hwParams, leFormats, sizeof(leFormats) / sizeof(leFormats[0]))) + return; + if (SetPreferredAlsaFormat(streamType, handle, hwParams, beFormats, sizeof(beFormats) / sizeof(beFormats[0]))) + return; + } + AlsaError(SS("No supported audio formats (" << alsa_device_name << "/" << streamType << ")")); + } + + class AlsaDriverImpl : public AudioDriver + { + private: + pipedal::CpuUse cpuUse; + +#ifdef ALSADRIVER_CONFIG_DBG + snd_output_t *snd_output = nullptr; + snd_pcm_status_t *snd_status = nullptr; + +#endif + uint32_t sampleRate = 0; + + uint32_t bufferSize; + uint32_t numberOfBuffers; + + int playbackChannels = 0; + int captureChannels = 0; + + uint32_t user_threshold = 0; + bool soft_mode = false; + + uint32_t playbackSampleSize = 0; + uint32_t captureSampleSize = 0; + uint32_t playbackFrameSize = 0; + uint32_t captureFrameSize = 0; + + using CopyFunction = void (AlsaDriverImpl::*)(size_t frames); + + CopyFunction copyInputFn; + CopyFunction copyOutputFn; + + bool inputSwapped = false; + bool outputSwapped = false; + + std::vector activeCaptureBuffers; + std::vector activePlaybackBuffers; + + std::vector captureBuffers; + std::vector playbackBuffers; + + uint8_t *rawCaptureBuffer = nullptr; + uint8_t *rawPlaybackBuffer = nullptr; + + AudioDriverHost *driverHost = nullptr; + + public: + AlsaDriverImpl(AudioDriverHost *driverHost) + : driverHost(driverHost) + { +#ifdef ALSADRIVER_CONFIG_DBG + snd_output_stdio_attach(&snd_output, stdout, 0); + snd_pcm_status_malloc(&snd_status); +#endif + } + virtual ~AlsaDriverImpl() + { + Close(); +#ifdef ALSADRIVER_CONFIG_DBG + snd_output_close(snd_output); + snd_pcm_status_free(snd_status); +#endif + } + + private: + void OnShutdown() + { + Lv2Log::info("Jack Audio Server has shut down."); + } + + static void + jack_shutdown_fn(void *arg) + { + ((AlsaDriverImpl *)arg)->OnShutdown(); + } + + static int xrun_callback_fn(void *arg) + { + ((AudioDriverHost *)arg)->OnUnderrun(); + return 0; + } + + virtual uint32_t GetSampleRate() + { + return this->sampleRate; + } + + JackServerSettings jackServerSettings; + + std::string alsa_device_name; + snd_pcm_t *playbackHandle = nullptr; + snd_pcm_t *captureHandle = nullptr; + + unsigned int periods = 0; + + snd_pcm_hw_params_t *captureHwParams; + snd_pcm_sw_params_t *captureSwParams; + snd_pcm_hw_params_t *playbackHwParams; + snd_pcm_sw_params_t *playbackSwParams; + + bool capture_and_playback_not_synced = false; + + std::mutex terminateSync; + + bool terminateAudio_ = false; + + void terminateAudio(bool terminate) + { + std::lock_guard lock{terminateSync}; + this->terminateAudio_ = terminate; + } + + bool terminateAudio() + { + std::lock_guard lock{terminateSync}; + return this->terminateAudio_; + } + + private: + void AlsaCleanup() + { + + if (captureHandle) + { + snd_pcm_close(captureHandle); + captureHandle = nullptr; + } + if (playbackHandle) + { + snd_pcm_close(playbackHandle); + playbackHandle = nullptr; + } + if (captureHwParams) + { + snd_pcm_hw_params_free(captureHwParams); + captureHwParams = nullptr; + } + if (captureSwParams) + { + snd_pcm_sw_params_free(captureSwParams); + captureSwParams = nullptr; + } + if (playbackHwParams) + { + snd_pcm_hw_params_free(playbackHwParams); + playbackHwParams = nullptr; + } + if (playbackSwParams) + { + snd_pcm_sw_params_free(playbackSwParams); + playbackSwParams = nullptr; + } + } + + std::string discover_alsa_using_apps() + { + return ""; // xxx fix me. + } + + void AlsaConfigureStream( + const std::string &alsa_device_name, + const char *streamType, + snd_pcm_t *handle, + snd_pcm_hw_params_t *hwParams, + snd_pcm_sw_params_t *swParams, + int *channels, + unsigned int *periods) + { + int err; + snd_pcm_uframes_t stop_th; + + if ((err = snd_pcm_hw_params_any(handle, hwParams)) < 0) + { + AlsaError(SS("No playback configurations available (" << snd_strerror(err) << ")")); + } + + err = snd_pcm_hw_params_set_access(handle, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED); + if (err < 0) + { + AlsaError("snd_pcm_hw_params_set_access failed."); + } + + SetPreferredAlsaFormat(alsa_device_name, streamType, handle, hwParams); + + unsigned int sampleRate = (unsigned int)this->sampleRate; + err = snd_pcm_hw_params_set_rate_near(handle, hwParams, + &sampleRate, NULL); + this->sampleRate = sampleRate; + if (err < 0) + { + AlsaError(SS("Can't set sample rate to " << this->sampleRate << " (" << alsa_device_name << "/" << streamType << ")")); + } + if (!*channels) + { + /*if not user-specified, try to find the maximum + * number of channels */ + unsigned int channels_max; + err = snd_pcm_hw_params_get_channels_max(hwParams, + &channels_max); + *channels = channels_max; + + if (*channels > 1024) + { + // The default PCM device has unlimited channels. + // report 2 channels + *channels = 2; + } + } + + if ((err = snd_pcm_hw_params_set_channels(handle, hwParams, + *channels)) < 0) + { + AlsaError(SS("Can't set channel count to " << *channels << " (" << alsa_device_name << "/" << streamType << ")")); + } + + snd_pcm_uframes_t effectivePeriodSize = this->bufferSize; + + if ((err = snd_pcm_hw_params_set_period_size_near(handle, hwParams, + &effectivePeriodSize, + 0)) < 0) + { + AlsaError(SS("Can't set period size to " << this->bufferSize << " (" << alsa_device_name << "/" << streamType << ")")); + } + this->bufferSize = effectivePeriodSize; + + *periods = this->numberOfBuffers; + snd_pcm_hw_params_set_periods_min(handle, hwParams, periods, NULL); + if (*periods < this->numberOfBuffers) + *periods = this->numberOfBuffers; + if (snd_pcm_hw_params_set_periods_near(handle, hwParams, + periods, NULL) < 0) + { + AlsaError(SS("Can't set number of periods to " << (*periods) << " (" << alsa_device_name << "/" << streamType << ")")); + } + + if (*periods < this->numberOfBuffers) + { + AlsaError(SS("Got smaller periods " << *periods << " than " << this->numberOfBuffers)); + } + + snd_pcm_uframes_t bSize; + + // if ((err = snd_pcm_hw_params_set_buffer_size(handle, hwParams, + // *periods * + // this->bufferSize)) < 0) + // { + // AlsaError(SS("Can't set buffer length to " << (*periods * this->bufferSize))); + // } + + if ((err = snd_pcm_hw_params(handle, hwParams)) < 0) + { + AlsaError(SS("Cannot set hardware parameters for " << alsa_device_name)); + } + + snd_pcm_sw_params_current(handle, swParams); + + if (handle == this->captureHandle) + { + if ((err = snd_pcm_sw_params_set_start_threshold(handle, swParams, + 0)) < 0) + { + AlsaError(SS("Cannot set start mode for " << alsa_device_name)); + } + } + else + { + if ((err = snd_pcm_sw_params_set_start_threshold(handle, swParams, + 0x7fffffff)) < 0) + { + AlsaError(SS("Cannot set start mode for " << alsa_device_name)); + } + } + + stop_th = *periods * this->bufferSize; + if (this->soft_mode) + { + stop_th = (snd_pcm_uframes_t)-1; + } + + if ((err = snd_pcm_sw_params_set_stop_threshold( + handle, swParams, stop_th)) < 0) + { + AlsaError(SS("ALSA: cannot set stop mode for " << alsa_device_name)); + } + + if ((err = snd_pcm_sw_params_set_silence_threshold( + handle, swParams, 0)) < 0) + { + AlsaError(SS("Cannot set silence threshold for " << alsa_device_name)); + } + + if (handle == this->playbackHandle) + err = snd_pcm_sw_params_set_avail_min( + handle, swParams, + this->bufferSize * (*periods - this->numberOfBuffers + 1)); + else + err = snd_pcm_sw_params_set_avail_min( + handle, swParams, this->bufferSize); + + if (err < 0) + { + AlsaError(SS("Cannot set avail min for " << alsa_device_name)); + } + + // err = snd_pcm_sw_params_set_tstamp_mode(handle, swParams, SND_PCM_TSTAMP_ENABLE); + // if (err < 0) + // { + // Lv2Log::info(SS( + // "Could not enable ALSA time stamp mode for " << alsa_device_name << " (err " << err << ")")); + // } + +#if SND_LIB_MAJOR >= 1 && SND_LIB_MINOR >= 1 + err = snd_pcm_sw_params_set_tstamp_type(handle, swParams, SND_PCM_TSTAMP_TYPE_MONOTONIC); + if (err < 0) + { + Lv2Log::info(SS( + "Could not use monotonic ALSA time stamps for " << alsa_device_name << "(err " << err << ")")); + } +#endif + + if ((err = snd_pcm_sw_params(handle, swParams)) < 0) + { + AlsaError(SS("Cannot set software parameters for " << alsa_device_name)); + } + err = snd_pcm_prepare(handle); + if (err < 0) + { + AlsaError(SS("ALSA prepare failed. " << snd_strerror(err))); + } + } + void SetAlsaParameters(uint32_t bufferSize, uint32_t numberOfBuffers, uint32_t sampleRate) + { + this->bufferSize = bufferSize; + this->numberOfBuffers = numberOfBuffers; + this->sampleRate = sampleRate; + + if (this->captureHandle) + { + AlsaConfigureStream( + this->alsa_device_name, + "capture", + captureHandle, + captureHwParams, + captureSwParams, + &captureChannels, + &this->periods); + } + if (this->playbackHandle) + { + AlsaConfigureStream( + this->alsa_device_name, + "playback", + playbackHandle, + playbackHwParams, + playbackSwParams, + &playbackChannels, + &this->periods); + } + +#ifdef ALSADRIVER_CONFIG_DBG + snd_pcm_dump(captureHandle, snd_output); + snd_pcm_dump(playbackHandle, snd_output); +#endif + } + void CopyCaptureFloatLe(size_t frames) + { + float *p = (float *)rawCaptureBuffer; + + std::vector &buffers = this->captureBuffers; + int channels = this->captureChannels; + for (size_t frame = 0; frame < frames; ++frame) + { + for (int channel = 0; channel < channels; ++channel) + { + float v = *p++; + buffers[channel][frame] = v; + } + } + } + + void CopyCaptureS32Le(size_t frames) + { + int32_t *p = (int32_t *)rawCaptureBuffer; + + std::vector &buffers = this->captureBuffers; + int channels = this->captureChannels; + constexpr float scale = 1.0f / (std::numeric_limits::max() + 1L); + for (size_t frame = 0; frame < frames; ++frame) + { + for (int channel = 0; channel < channels; ++channel) + { + int32_t v = *p++; + buffers[channel][frame] = scale * v; + } + } + } + void CopyPlaybackS32Le(size_t frames) + { + int32_t *p = (int32_t *)rawPlaybackBuffer; + + std::vector &buffers = this->playbackBuffers; + int channels = this->playbackChannels; + constexpr float scale = std::numeric_limits::max(); + for (size_t frame = 0; frame < frames; ++frame) + { + for (int channel = 0; channel < channels; ++channel) + { + float v = buffers[channel][frame]; + if (v > 1.0f) + v = 1.0f; + if (v < -1.0f) + v = -1.0f; + *p++ = (int32_t)(scale * v); + } + } + } + void CopyPlaybackFloatLe(size_t frames) + { + float *p = (float *)rawPlaybackBuffer; + + std::vector &buffers = this->playbackBuffers; + int channels = this->captureChannels; + for (size_t frame = 0; frame < frames; ++frame) + { + for (int channel = 0; channel < channels; ++channel) + { + float v = buffers[channel][frame]; + *p++ = v; + } + } + } + void AllocateBuffers(std::vector &buffers, size_t n) + { + buffers.resize(n); + for (size_t i = 0; i < n; ++i) + { + buffers[i] = new float[this->bufferSize]; + for (size_t j = 0; j < this->bufferSize; ++j) + { + buffers[i][j] = 0; + } + } + } + + JackChannelSelection channelSelection; + bool open = false; + virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + { + terminateAudio_ = false; + if (open) + { + throw PiPedalStateException("Already open."); + } + this->jackServerSettings = jackServerSettings; + this->channelSelection = channelSelection; + + open = true; + + OpenMidi(jackServerSettings, channelSelection); + OpenAudio(jackServerSettings, channelSelection); + } + + void OpenAudio(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + { + int err; + + alsa_device_name = jackServerSettings.GetAlsaDevice(); + + this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers(); + this->bufferSize = jackServerSettings.GetBufferSize(); + this->user_threshold = jackServerSettings.GetBufferSize(); + + try + { + + err = snd_pcm_open(&playbackHandle, alsa_device_name.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (err < 0) + { + switch (errno) + { + case EBUSY: + { + std::string apps = discover_alsa_using_apps(); + std::string message; + if (apps.size() != 0) + { + message = + SS("Device " << alsa_device_name << " in use. The following applications are using your soundcard: " << apps + << ". Stop them as neccesary before trying to restart pipedald."); + } + else + { + message = + SS("Device " << alsa_device_name << " in use. Stop the application using it before trying to restart pipedald. "); + } + Lv2Log::error(message); + throw PiPedalStateException(std::move(message)); + } + break; + case EPERM: + throw PiPedalStateException(SS("Permission denied opening device '" << alsa_device_name << "'")); + default: + throw PiPedalStateException(SS("Unexepected error (" << errno << ") opening device '" << alsa_device_name << "'")); + } + } + if (this->playbackHandle) + { + snd_pcm_nonblock(playbackHandle, 0); + } + + err = snd_pcm_open(&captureHandle, alsa_device_name.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); + + if (err < 0) + { + switch (errno) + { + case EBUSY: + { + std::string apps = discover_alsa_using_apps(); + std::string message; + if (apps.size() != 0) + { + message = + SS("Device " << alsa_device_name << " in use. The following applications are using your soundcard: " << apps + << ". Stop them as neccesary before trying to restart pipedald."); + } + else + { + message = + SS("Device " << alsa_device_name << " in use. Stop the application using it before trying to restart pipedald. "); + } + Lv2Log::error(message); + throw PiPedalStateException(std::move(message)); + } + break; + case EPERM: + throw PiPedalStateException(SS("Permission denied opening device '" << alsa_device_name << "'")); + default: + throw PiPedalStateException(SS("Unexepected error (" << errno << ") opening device '" << alsa_device_name << "'")); + } + } + if (this->captureHandle) + { + snd_pcm_nonblock(captureHandle, 0); + } + + if ((err = snd_pcm_hw_params_malloc(&captureHwParams)) < 0) + { + throw PiPedalStateException("Failed to allocate captureHwParams"); + } + if ((err = snd_pcm_sw_params_malloc(&captureSwParams)) < 0) + { + throw PiPedalStateException("Failed to allocate captureSwParams"); + } + if ((err = snd_pcm_hw_params_malloc(&playbackHwParams)) < 0) + { + throw PiPedalStateException("Failed to allocate playbackHwParams"); + } + if ((err = snd_pcm_sw_params_malloc(&playbackSwParams)) < 0) + { + throw PiPedalStateException("Failed to allocate playbackSwParams"); + } + + SetAlsaParameters(jackServerSettings.GetBufferSize(), jackServerSettings.GetNumberOfBuffers(), jackServerSettings.GetSampleRate()); + capture_and_playback_not_synced = false; + + if (captureHandle && playbackHandle) + { + if (snd_pcm_link(playbackHandle, + captureHandle) != 0) + { + capture_and_playback_not_synced = true; + } + } + + snd_pcm_format_t captureFormat; + snd_pcm_hw_params_get_format(captureHwParams, &captureFormat); + + switch (captureFormat) + { + case SND_PCM_FORMAT_FLOAT_LE: + captureSampleSize = 4; + copyInputFn = &AlsaDriverImpl::CopyCaptureFloatLe; + break; + case SND_PCM_FORMAT_S24_3LE: + captureSampleSize = 3; + break; + case SND_PCM_FORMAT_S32_LE: + captureSampleSize = 4; + copyInputFn = &AlsaDriverImpl::CopyCaptureS32Le; + break; + case SND_PCM_FORMAT_S24_LE: + captureSampleSize = 4; + break; + case SND_PCM_FORMAT_S16_LE: + captureSampleSize = 2; + break; + case SND_PCM_FORMAT_FLOAT_BE: + captureSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_3BE: + captureSampleSize = 3; + break; + case SND_PCM_FORMAT_S32_BE: + captureSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_BE: + captureSampleSize = 4; + break; + case SND_PCM_FORMAT_S16_BE: + captureSampleSize = 2; + break; + default: + throw PiPedalStateException("Invalid format."); + } + captureFrameSize = captureSampleSize * captureChannels; + rawCaptureBuffer = new uint8_t[captureFrameSize * bufferSize]; + memset(rawCaptureBuffer, 0, captureFrameSize * bufferSize); + + AllocateBuffers(captureBuffers, captureChannels); + + snd_pcm_format_t playbackFormat; + snd_pcm_hw_params_get_format(playbackHwParams, &playbackFormat); + + switch (playbackFormat) + { + case SND_PCM_FORMAT_FLOAT_LE: + playbackSampleSize = 4; + copyOutputFn = &AlsaDriverImpl::CopyPlaybackFloatLe; + break; + case SND_PCM_FORMAT_S24_3LE: + playbackSampleSize = 3; + break; + case SND_PCM_FORMAT_S32_LE: + copyOutputFn = &AlsaDriverImpl::CopyPlaybackS32Le; + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_LE: + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S16_LE: + playbackSampleSize = 2; + break; + case SND_PCM_FORMAT_FLOAT_BE: + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_3BE: + playbackSampleSize = 3; + break; + case SND_PCM_FORMAT_S32_BE: + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S24_BE: + playbackSampleSize = 4; + break; + case SND_PCM_FORMAT_S16_BE: + playbackSampleSize = 2; + break; + default: + throw PiPedalStateException("Invalid format."); + } + playbackFrameSize = playbackSampleSize * playbackChannels; + rawPlaybackBuffer = new uint8_t[playbackFrameSize * bufferSize]; + memset(rawPlaybackBuffer, 0, playbackFrameSize * bufferSize); + + AllocateBuffers(playbackBuffers, playbackChannels); + } + catch (const std::exception &e) + { + AlsaCleanup(); + throw; + } + } + + void FillOutputBuffer() + { + memset(rawPlaybackBuffer, 0, playbackFrameSize * bufferSize); + while (true) + { + auto avail = snd_pcm_avail(this->playbackHandle); + if (avail < 0) + { + int err = snd_pcm_prepare(playbackHandle); + if (err < 0) + { + throw PiPedalStateException(SS("Audio playback failed. " << snd_strerror(err))); + } + continue; + } + if (avail == 0) + break; + if (avail > this->bufferSize) + avail = this->bufferSize; + + ssize_t err = WriteBuffer(playbackHandle, rawPlaybackBuffer, avail); + if (err < 0) + { + throw PiPedalStateException(SS("Audio playback failed. " << snd_strerror(err))); + } + } + } + void XrunRecoverOutputOverrun(snd_pcm_t *handle, int err) + { + if (err == -EPIPE) + { + err = snd_pcm_prepare(handle); + if (err < 0) + { + throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")")); + } + } + FillOutputBuffer(); + } + + void DumpStatus(snd_pcm_t *handle) + { +#ifdef ALSADRIVER_CONFIG_DBG + snd_pcm_status(handle, snd_status); + snd_pcm_status_dump(snd_status, snd_output); +#endif + } + + void XrunRecoverInputUnderrun(snd_pcm_t *handle, int err) + { + if (err == -EPIPE) + { /* underrun */ + + this->driverHost->OnUnderrun(); + + // DumpStatus(handle); + err = snd_pcm_drop(handle); + if (err < 0) + { + throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")")); + } + err = snd_pcm_prepare(handle); + if (err < 0) + { + throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")")); + } + FillOutputBuffer(); + + err = snd_pcm_start(handle); + // if (err < 0) + // { + // throw PiPedalStateException(SS("Can't recover from ALSA underrun. (" << snd_strerror(err) << ")")); + // } + return; + } + else if (err == -ESTRPIPE) + { + audioRunning = false; + while ((err = snd_pcm_resume(handle)) == -EAGAIN) + { + sleep(1); + } + if (err < 0) + { + err = snd_pcm_prepare(handle); + if (err < 0) + { + throw PiPedalStateException(SS("Can't recover from ALSA suspend. (" << snd_strerror(err) << ")")); + } + } + return; + } + throw PiPedalStateException(SS("ALSA error:" << snd_strerror(err))); + } + + std::jthread *audioThread; + bool audioRunning; + + bool block = false; + + snd_pcm_sframes_t ReadBuffer(snd_pcm_t *handle, uint8_t *buffer, snd_pcm_uframes_t frames) + { + // transcode to jack format. + // expand running status if neccessary. + // deal with regular and sysex messages split across + // buffer boundaries. + snd_pcm_sframes_t framesRead; + + auto state = snd_pcm_state(handle); + auto frame_bytes = this->captureFrameSize; + do + { + framesRead = snd_pcm_readi(handle, buffer, frames); + if (framesRead < 0) + { + return framesRead; + } + if (framesRead > 0) + { + buffer += framesRead * frame_bytes; + frames -= framesRead; + } + if (framesRead == 0) + { + snd_pcm_wait(captureHandle, 1); + } + } while (frames > 0); + return framesRead; + } + + long WriteBuffer(snd_pcm_t *handle, uint8_t *buf, size_t frames) + { + long framesRead; + auto frame_bytes = this->captureFrameSize; + + while (frames > 0) + { + framesRead = snd_pcm_writei(handle, buf, frames); + if (framesRead == -EAGAIN) + continue; + if (framesRead < 0) + return framesRead; + buf += framesRead * frame_bytes; + frames -= framesRead; + } + return 0; + } + void AudioThread() + { + try + { +#if defined(__WIN32) + // bump thread prioriy two levels to + // ensure that the service thread doesn't + // get bogged down by UIwork. Doesn't have to be realtime, but it + // MUST run at higher priority than UI threads. + xxx; // TO DO. +#elif defined(__linux__) + int min = sched_get_priority_min(SCHED_RR); + int max = sched_get_priority_max(SCHED_RR); + + struct sched_param param; + memset(¶m, 0, sizeof(param)); + param.sched_priority = 79; + + int result = sched_setscheduler(0, SCHED_RR, ¶m); + if (result == 0) + { + Lv2Log::debug("Service thread priority successfully boosted."); + } + else + { + Lv2Log::error(SS("Failed to set ALSA AudioThread priority. (" << strerror(result) << ")")); + } +#else + xxx; // TODO! +#endif + + bool ok = true; + + auto playbackState = snd_pcm_state(playbackHandle); + + FillOutputBuffer(); + + int err; + if ((err = snd_pcm_start(captureHandle)) < 0) + { + throw PiPedalStateException("Unable to start ALSA capture."); + } + + cpuUse.SetStartTime(cpuUse.Now()); + while (true) + { + cpuUse.UpdateCpuUse(); + + if (terminateAudio()) + { + break; + } + + // snd_pcm_wait(captureHandle, 1); + + ssize_t framesRead; + if ((framesRead = ReadBuffer(captureHandle, this->rawCaptureBuffer, bufferSize)) < 0) + { + this->driverHost->OnUnderrun(); + auto state = snd_pcm_state(playbackHandle); + XrunRecoverInputUnderrun(captureHandle, framesRead); + continue; + } + else + { + cpuUse.AddSample(ProfileCategory::Read); + if (framesRead == 0) + continue; + if (framesRead != bufferSize) + { + throw PiPedalStateException("Invalid read."); + } + + (this->*copyInputFn)(framesRead); + cpuUse.AddSample(ProfileCategory::Driver); + + this->driverHost->OnProcess(framesRead); + + cpuUse.AddSample(ProfileCategory::Execute); + + (this->*copyOutputFn)(framesRead); + cpuUse.AddSample(ProfileCategory::Driver); + // process. + + ssize_t err = WriteBuffer(playbackHandle, rawPlaybackBuffer, framesRead); + + if (err < 0) + { + this->driverHost->OnUnderrun(); + XrunRecoverOutputOverrun(playbackHandle, err); + } + cpuUse.AddSample(ProfileCategory::Write); + } + } + } + catch (const std::exception &e) + { + Lv2Log::error(e.what()); + Lv2Log::error("ALSA audio thread terminated abnormally."); + } + this->driverHost->OnAudioStopped(); + } + + bool alsaActive = false; + + static int IndexFromPortName(const std::string &s) + { + auto pos = s.find_last_of('_'); + if (pos == std::string::npos) + { + throw std::invalid_argument("Bad port name."); + } + const char *p = s.c_str() + (pos + 1); + + int v = atoi(p); + if (v < 0) + { + throw std::invalid_argument("Bad port name."); + } + return v; + } + + bool activated = false; + virtual void Activate() + { + if (activated) + { + throw PiPedalStateException("Already activated."); + } + activated = true; + + this->activeCaptureBuffers.resize(channelSelection.GetInputAudioPorts().size()); + + int ix = 0; + for (auto &x : channelSelection.GetInputAudioPorts()) + { + int sourceIndex = IndexFromPortName(x); + if (sourceIndex >= captureBuffers.size()) + { + throw PiPedalArgumentException("Invalid input port."); + } + this->activeCaptureBuffers[ix++] = this->captureBuffers[sourceIndex]; + } + + this->activePlaybackBuffers.resize(channelSelection.GetOutputAudioPorts().size()); + + ix = 0; + for (auto &x : channelSelection.GetOutputAudioPorts()) + { + int sourceIndex = IndexFromPortName(x); + if (sourceIndex >= playbackBuffers.size()) + { + throw PiPedalArgumentException("Invalid output port."); + } + this->activePlaybackBuffers[ix++] = this->playbackBuffers[sourceIndex]; + } + + audioThread = new std::jthread([this]() + { AudioThread(); }); + } + + virtual void Deactivate() + { + if (!activated) + { + return; + } + activated = false; + terminateAudio(true); + if (audioThread) + { + this->audioThread->join(); + this->audioThread = 0; + } + Lv2Log::debug("Audio thread joined."); + } + + static constexpr size_t MIDI_BUFFER_SIZE = 16 * 1024; + static constexpr size_t MAX_MIDI_EVENT = 4 * 1024; + + public: + class MidiState + { + private: + snd_rawmidi_t *hIn = nullptr; + snd_rawmidi_params_t *hInParams = nullptr; + std::string deviceName; + + // running status state. + uint8_t runningStatus = 0; + int dataLength = 0; + int dataIndex = 0; + size_t statusBytesRemaining = 0; + size_t data0; + size_t data1; + + size_t eventCount = 0; + MidiEvent events[MAX_MIDI_EVENT]; + size_t bufferCount = 0; + uint8_t buffer[MIDI_BUFFER_SIZE]; + + uint8_t readBuffer[1024]; + + ssize_t sysexStartIndex = -1; + + void checkError(int result, const char *message) + { + if (result < 0) + { + throw PiPedalStateException(SS("Unexpected error: " << message << " (" << this->deviceName)); + } + } + + public: + void Open(const AlsaMidiDeviceInfo &device) + { + bufferCount = 0; + eventCount = 0; + sysexStartIndex = -1; + runningStatus = 0; + dataIndex = 0; + dataLength = 0; + + this->deviceName = device.description_; + + int err = snd_rawmidi_open(&hIn, nullptr, device.name_.c_str(), SND_RAWMIDI_NONBLOCK); + if (err < 0) + { + throw PiPedalStateException(SS("Can't open midi device " << deviceName << ". (" << snd_strerror(err))); + } + + err = snd_rawmidi_params_malloc(&hInParams); + checkError(err, "snd_rawmidi_params_malloc failed."); + + err = snd_rawmidi_params_set_buffer_size(hIn, hInParams, 2048); + checkError(err, "snd_rawmidi_params_set_buffer_size failed."); + + err = snd_rawmidi_params_set_no_active_sensing(hIn, hInParams, 1); + checkError(err, "snd_rawmidi_params_set_no_active_sensing failed."); + } + void Close() + { + if (hIn) + { + snd_rawmidi_close(hIn); + hIn = nullptr; + } + if (hInParams) + { + snd_rawmidi_params_free(hInParams); + hInParams = 0; + } + } + + int GetDataLength(uint8_t cc) + { + static int sDataLength[] = {0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 1, 1, -1}; + return sDataLength[cc >> 4]; + } + + size_t GetMidiInputEventCount() + { + return eventCount; + } + + void NextEventBuffer() + { + // xxx preserve unflushed sysex data. + if (sysexStartIndex != -1) + { + int end = bufferCount; + bufferCount = 0; + eventCount = 0; + for (int i = sysexStartIndex; i < end; ++i) + { + buffer[bufferCount++] = buffer[i]; + } + sysexStartIndex = 0; + } + else + { + bufferCount = 0; + eventCount = 0; + } + } + bool GetMidiInputEvent(MidiEvent *event, size_t nFrame) + { + if (nFrame >= eventCount) + return false; + *event = this->events[nFrame]; + return true; + } + + void MidiPut(uint8_t cc, uint8_t d0, uint8_t d1) + { + if (cc == 0) + return; + + // check for overrun. + if (bufferCount + 1 + dataLength >= sizeof(buffer)) + { + bufferCount = sizeof(buffer); + return; + } + if (eventCount >= MAX_MIDI_EVENT) + { + return; + } + + auto *event = &(this->events[eventCount++]); + + event->time = 0; + event->buffer = &buffer[bufferCount]; + event->size = dataLength + 1; + + buffer[bufferCount++] = cc; + if (dataLength >= 1) + { + buffer[bufferCount++] = d0; + if (dataLength >= 2) + { + buffer[bufferCount++] = d1; + } + } + } + + void FillBuffer() + { + while (true) + { + ssize_t nRead = snd_rawmidi_read(hIn, readBuffer, sizeof(readBuffer)); + if (nRead == -EAGAIN) + return; + if (nRead < 0) + { + checkError(nRead, SS(this->deviceName << "MIDI event read failed. (" << snd_strerror(nRead)).c_str()); + } + WriteBuffer(readBuffer, nRead); // expose write to test code. + } + } + + void FlushSysex() + { + if (sysexStartIndex != -1) + { + if (this->eventCount != MAX_MIDI_EVENT) + { + auto *event = &(events[eventCount++]); + event->size = this->bufferCount - sysexStartIndex; + event->buffer = &(this->buffer[this->sysexStartIndex]); + event->time = 0; + } + sysexStartIndex = -1; + } + } + + int GetSystemCommonLength(uint8_t cc) + { + static int sizes[] = {-1, 1, 2, 1, -1, -1, 0, 0}; + return sizes[(cc >> 4) & 0x07]; + } + void WriteBuffer(uint8_t *readBuffer, size_t nRead) + { + for (ssize_t i = 0; i < nRead; ++i) + { + uint8_t v = readBuffer[i]; + + if (v >= 0x80) + { + if (v >= 0xF0) + { + if (v == 0xF0) + { + if (bufferCount == sizeof(buffer)) + { + break; + } + sysexStartIndex = bufferCount; + + buffer[bufferCount++] = 0xF0; + + runningStatus = 0; // discard subsequent data. + dataLength = -2; // indefinitely. + dataIndex = -1; + } + else if (v >= 0xF8) + { + // don't overwrite running status. + // don't break sysexes on a running status message. + // LV2 standard is ambiguous how realtime messages are handled, so just discard them. + continue; + } + else + { + FlushSysex(); + int length = GetSystemCommonLength(v); + if (length == -1) + break; // ignore illegal messages. + runningStatus = v; + dataLength = length; + dataIndex = 0; + } + } + else + { + FlushSysex(); + int dataLength = GetDataLength(v); + runningStatus = v; + if (dataLength == -1) + { + this->dataLength = dataLength; + dataIndex = -1; + } + else + { + this->dataLength = dataLength; + dataIndex = 0; + } + } + } + else + { + if (sysexStartIndex != -1) + { + if (bufferCount != sizeof(buffer)) + { + buffer[bufferCount++] = v; + } + } + else + { + switch (dataIndex) + { + default: + // discard. + break; + case 0: + data0 = v; + dataIndex = 1; + break; + case 1: + data1 = v; + dataIndex = 2; + break; + } + } + } + if (dataIndex == dataLength) + { + MidiPut(runningStatus, data0, data1); + dataIndex = 0; + } + } + } + }; + + std::vector midiStates; + + void OpenMidi(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) + { + const auto &devices = channelSelection.GetInputMidiDevices(); + + midiStates.resize(devices.size()); + + for (size_t i = 0; i < devices.size(); ++i) + { + const auto &device = devices[i]; + MidiState *state = new MidiState(); + midiStates[i] = state; + state->Open(device); + } + } + + virtual size_t MidiInputBufferCount() const + { + return this->midiStates.size(); + } + virtual void *GetMidiInputBuffer(size_t channel, size_t nFrames) + { + return (void *)midiStates[channel]; + } + + virtual size_t GetMidiInputEventCount(void *portBuffer) + { + MidiState *state = (MidiState *)portBuffer; + return state->GetMidiInputEventCount(); + } + virtual bool GetMidiInputEvent(MidiEvent *event, void *portBuf, size_t nFrame) + { + MidiState *state = (MidiState *)portBuf; + return state->GetMidiInputEvent(event, nFrame); + } + + virtual void FillMidiBuffers() + { + for (size_t i = 0; i < this->midiStates.size(); ++i) + { + auto *state = midiStates[i]; + state->NextEventBuffer(); + state->FillBuffer(); + } + } + + virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } + virtual float *GetInputBuffer(size_t channel, size_t nFrames) + { + return activeCaptureBuffers[channel]; + } + + virtual size_t OutputBufferCount() const { return activePlaybackBuffers.size(); } + virtual float *GetOutputBuffer(size_t channel, size_t nFrames) + { + return activePlaybackBuffers[channel]; + } + + void FreeBuffers(std::vector &buffer) + { + for (size_t i = 0; i < buffer.size(); ++i) + { + // delete[] buffer[i]; + buffer[i] = 0; + } + buffer.clear(); + } + void DeleteBuffers() + { + activeCaptureBuffers.clear(); + activePlaybackBuffers.clear(); + FreeBuffers(this->playbackBuffers); + FreeBuffers(this->captureBuffers); + } + virtual void Close() + { + if (!open) + { + return; + } + open = false; + Deactivate(); + AlsaCleanup(); + DeleteBuffers(); + } + + virtual float CpuUse() + { + return cpuUse.GetCpuUse(); + } + + virtual float CpuOverhead() + { + return cpuUse.GetCpuOverhead(); + } + + }; + + AudioDriver *CreateAlsaDriver(AudioDriverHost *driverHost) + { + return new AlsaDriverImpl(driverHost); + } + + bool GetAlsaChannels(const JackServerSettings &jackServerSettings, + std::vector &inputAudioPorts, + std::vector &outputAudioPorts) + { + + snd_pcm_t *playbackHandle = nullptr; + snd_pcm_t *captureHandle = nullptr; + snd_pcm_hw_params_t *playbackHwParams = nullptr; + snd_pcm_hw_params_t *captureHwParams = nullptr; + std::string alsaDeviceName = jackServerSettings.GetAlsaDevice(); + bool result = false; + + try + { + int err; + for (int retry = 0; retry < 5; ++retry) + { + err = snd_pcm_open(&playbackHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (err == -EBUSY) + { + sleep(1); + continue; + } + break; + } + if (err < 0) + { + throw PiPedalStateException(SS(alsaDeviceName << " not found. " + << "(" << snd_strerror(err) << ")")); + } + + err = snd_pcm_open(&captureHandle, alsaDeviceName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); + if (err < 0) + throw PiPedalStateException(SS(alsaDeviceName << " not found.")); + + if (snd_pcm_hw_params_malloc(&playbackHwParams) < 0) + { + throw PiPedalLogicException("Out of memory."); + } + if (snd_pcm_hw_params_malloc(&captureHwParams) < 0) + { + throw PiPedalLogicException("Out of memory."); + } + + snd_pcm_hw_params_any(playbackHandle, playbackHwParams); + snd_pcm_hw_params_any(captureHandle, captureHwParams); + + SetPreferredAlsaFormat(alsaDeviceName, "capture", captureHandle, captureHwParams); + SetPreferredAlsaFormat(alsaDeviceName, "output", playbackHandle, playbackHwParams); + + unsigned int sampleRate = jackServerSettings.GetSampleRate(); + err = snd_pcm_hw_params_set_rate_near(playbackHandle, playbackHwParams, &sampleRate, 0); + if (err < 0) + { + throw PiPedalLogicException("Sample rate not supported."); + } + sampleRate = jackServerSettings.GetSampleRate(); + err = snd_pcm_hw_params_set_rate_near(captureHandle, captureHwParams, &sampleRate, 0); + if (err < 0) + { + throw PiPedalLogicException("Sample rate not supported."); + } + + unsigned int playbackChannels, captureChannels; + + err = snd_pcm_hw_params_get_channels_max(playbackHwParams, &playbackChannels); + if (err < 0) + { + throw PiPedalLogicException("No outut channels."); + } + err = snd_pcm_hw_params_get_channels_max(captureHwParams, &captureChannels); + if (err < 0) + { + throw PiPedalLogicException("No input channels."); + } + + inputAudioPorts.clear(); + for (unsigned int i = 0; i < playbackChannels; ++i) + { + inputAudioPorts.push_back(SS("system::playback_" << i)); + } + + outputAudioPorts.clear(); + for (unsigned int i = 0; i < captureChannels; ++i) + { + outputAudioPorts.push_back(SS("system::capture_" << i)); + } + + result = true; + } + catch (const std::exception &e) + { + Lv2Log::warning(SS("Unable to read ALSA configuration for " << alsaDeviceName << ". " << e.what() << ".")); + result = false; + throw; + } + if (playbackHwParams) + snd_pcm_hw_params_free(playbackHwParams); + if (captureHwParams) + snd_pcm_hw_params_free(captureHwParams); + + if (playbackHandle) + { + snd_pcm_close(playbackHandle); + } + if (captureHandle) + snd_pcm_close(captureHandle); + return result; + } + + static void AlsaAssert(bool value) + { + if (!value) + throw PiPedalStateException("Assert failed."); + } + + static void ExpectEvent(AlsaDriverImpl::MidiState &m, int event, const std::vector message) + { + MidiEvent e; + m.GetMidiInputEvent(&e,event); + AlsaAssert(e.size == message.size()); + for (size_t i = 0; i < message.size(); ++i) + { + AlsaAssert(message[i] == e.buffer[i]); + } + } + + void MidiDecoderTest() + { + + AlsaDriverImpl::MidiState midiState; + + MidiEvent event; + + // Running status decoding. + { + static uint8_t m0[] = {0x80, 0x1, 0x2, 0x3,0x4,0x5}; + midiState.NextEventBuffer(); + midiState.WriteBuffer(m0, sizeof(m0)); + AlsaAssert(midiState.GetMidiInputEventCount() == 2); + AlsaAssert(midiState.GetMidiInputEvent(&event, 0)); + + ExpectEvent(midiState,0, {0x80,0x1,0x2}); + ExpectEvent(midiState,1, {0x80,0x3,0x4}); + + static uint8_t m1[] = {0x06,0xC0,0x1,0x2}; + midiState.NextEventBuffer(); + midiState.WriteBuffer(m1,sizeof(m1)); + AlsaAssert(midiState.GetMidiInputEventCount() == 3); + ExpectEvent(midiState,0,{0x80,0x05,0x06}); + ExpectEvent(midiState,1,{0xC0,0x1}); + ExpectEvent(midiState,2,{0xC0,0x2}); + } + + + // SYSEX. + { + static uint8_t m0[] = {0xF0, 0x76, 0xF7, 0xA}; + midiState.NextEventBuffer(); + midiState.WriteBuffer(m0, 4); + AlsaAssert(midiState.GetMidiInputEventCount() == 2); + AlsaAssert(midiState.GetMidiInputEvent(&event, 0)); + AlsaAssert(event.size == 2); + AlsaAssert(event.buffer[0] == 0xF0); + AlsaAssert(event.buffer[1] == 0x76); + } + + // SPLIT SYSEX + { + static uint8_t m0[] = {0xF0, 0x76, 0x3B}; + midiState.NextEventBuffer(); + midiState.WriteBuffer(m0, sizeof(m0)); + AlsaAssert(midiState.GetMidiInputEventCount() == 0); + static uint8_t m1[] = {0x77, 0xF7}; + midiState.NextEventBuffer(); + midiState.WriteBuffer(m1, sizeof(m1)); + AlsaAssert(midiState.GetMidiInputEventCount() == 2); + + AlsaAssert(midiState.GetMidiInputEvent(&event, 0)); + AlsaAssert(event.size == 0x4); + AlsaAssert(event.buffer[0] == 0xF0); + AlsaAssert(event.buffer[1] == 0x76); + AlsaAssert(event.buffer[2] == 0x3B); + AlsaAssert(event.buffer[3] == 0x77); + } + } +} // namespace diff --git a/src/AlsaDriver.hpp b/src/AlsaDriver.hpp new file mode 100644 index 0000000..984aa04 --- /dev/null +++ b/src/AlsaDriver.hpp @@ -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&inputAudioPorts, + std::vector&outputAudioPorts + ); + + + AudioDriver* CreateAlsaDriver(AudioDriverHost*driverHost); + + + void MidiDecoderTest(); +} + diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp new file mode 100644 index 0000000..a04decd --- /dev/null +++ b/src/AlsaDriverTest.cpp @@ -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 +#include +#include + +#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(); +} + diff --git a/src/AudioConfig.hpp b/src/AudioConfig.hpp new file mode 100644 index 0000000..7b57e1f --- /dev/null +++ b/src/AudioConfig.hpp @@ -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 diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp new file mode 100644 index 0000000..b7fd10c --- /dev/null +++ b/src/AudioDriver.hpp @@ -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 + + + + +namespace pipedal { + + + using ProcessCallback = std::function; + + + + 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; + + }; + +}; \ No newline at end of file diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3eb5259..a89e7cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/CommandLineParser.hpp b/src/CommandLineParser.hpp index 1917453..6ce4276 100644 --- a/src/CommandLineParser.hpp +++ b/src/CommandLineParser.hpp @@ -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 + void AddOption(const std::string &shortOption, const std::string &longOption, T *pResult) + { + if (shortOption.length() != 0) + options.push_back(new Option("-" + shortOption, pResult)); + if (longOption.length() != 0) + options.push_back(new Option("--" + longOption, pResult)); + } + template void AddOption(const std::string &option, T *pResult) { diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index ebaa2cf..972b0e8 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -36,6 +36,17 @@ #include "DeviceIdFile.hpp" #include #include +#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 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) diff --git a/src/CpuUse.cpp b/src/CpuUse.cpp new file mode 100644 index 0000000..63d2032 --- /dev/null +++ b/src/CpuUse.cpp @@ -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; +} \ No newline at end of file diff --git a/src/CpuUse.hpp b/src/CpuUse.hpp new file mode 100644 index 0000000..e2dedac --- /dev/null +++ b/src/CpuUse.hpp @@ -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 +#include + +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; + } + }; + +} diff --git a/src/JackConfiguration.cpp b/src/JackConfiguration.cpp index b211ac7..b39dd33 100644 --- a/src/JackConfiguration.cpp +++ b/src/JackConfiguration.cpp @@ -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 #include #include +#endif + #include #include "defer.hpp" #include 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 *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 *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 makeValid(const std::vector & selec } return result; } +static std::vector makeValid(const std::vector & selected, const std::vector &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 sourceDevices = available; + std::vector result; + std::vector 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() diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index 10777a3..10b9225 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -22,6 +22,9 @@ #include #include #include "json.hpp" +#include "JackServerSettings.hpp" +#include "PiPedalAlsa.hpp" + namespace pipedal { @@ -42,11 +45,13 @@ namespace pipedal std::vector inputAudioPorts_; std::vector outputAudioPorts_; - std::vector inputMidiPorts_; - std::vector outputMidiPorts_; + std::vector 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 &GetInputAudioPorts() const { return inputAudioPorts_; } const std::vector &GetOutputAudioPorts() const { return outputAudioPorts_; } - const std::vector &GetInputMidiPorts() const { return inputMidiPorts_; } - const std::vector &GetOutputMidiPorts() const { return outputMidiPorts_; } + const std::vector &GetInputMidiDevices() const { return inputMidiDevices_; } DECLARE_JSON_MAP(JackConfiguration); @@ -69,10 +73,20 @@ namespace pipedal private: std::vector inputAudioPorts_; std::vector outputAudioPorts_; - std::vector inputMidiPorts_; + std::vector inputMidiDevices_; public: JackChannelSelection() { + } + JackChannelSelection( + std::vector inputAudioPorts, + std::vector outputAudioPorts, + std::vector inputMidiDevices + ) : inputAudioPorts_(inputAudioPorts), + outputAudioPorts_(outputAudioPorts), + inputMidiDevices_(inputMidiDevices) + { + } bool isValid() const { @@ -83,14 +97,14 @@ namespace pipedal { return inputAudioPorts_; } - const std::vector& getOutputAudioPorts() const + const std::vector& GetOutputAudioPorts() const { return outputAudioPorts_; } - const std::vector& GetInputMidiPorts() const + const std::vector& GetInputMidiDevices() const { - return inputMidiPorts_; + return inputMidiDevices_; } JackChannelSelection RemoveInvalidChannels(const JackConfiguration&config) const; diff --git a/src/JackDriver.cpp b/src/JackDriver.cpp new file mode 100644 index 0000000..d0b4764 --- /dev/null +++ b/src/JackDriver.cpp @@ -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 +#include +#include +#include +#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 inputPorts; + std::vector outputPorts; + std::vector 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 \ No newline at end of file diff --git a/src/JackDriver.hpp b/src/JackDriver.hpp new file mode 100644 index 0000000..b2d7bd3 --- /dev/null +++ b/src/JackDriver.hpp @@ -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 + diff --git a/src/JackHost.cpp b/src/JackHost.cpp index 98ab932..b338a33 100644 --- a/src/JackHost.cpp +++ b/src/JackHost.cpp @@ -21,12 +21,15 @@ #include "Lv2Log.hpp" +#include "JackDriver.hpp" +#include "AlsaDriver.hpp" + + using namespace pipedal; -#include -#include -#include -#include +#include "AudioConfig.hpp" + + #include #include #include @@ -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 currentPedalBoard; std::vector> activePedalBoards; // pedalboards that have been sent to the audio queue. Lv2PedalBoard *realtimeActivePedalBoard = nullptr; - std::vector inputPorts; - std::vector outputPorts; - std::vector midiInputPorts; - std::vector midiLv2Buffers; - uint32_t sampleRate; + uint32_t sampleRate = 0; uint64_t currentSample = 0; std::atomic underruns = 0; std::atomic 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) diff --git a/src/JackHost.hpp b/src/JackHost.hpp index f19f506..689db04 100644 --- a/src/JackHost.hpp +++ b/src/JackHost.hpp @@ -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; diff --git a/src/JackServerSettings.cpp b/src/JackServerSettings.cpp index f09cb2c..b641741 100644 --- a/src/JackServerSettings.cpp +++ b/src/JackServerSettings.cpp @@ -157,6 +157,7 @@ void JackServerSettings::ReadJackConfiguration() void JackServerSettings::Write() { + #if JACK_HOST this->valid_ = false; std::vector 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) diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp index ec70da2..0e81c99 100644 --- a/src/JackServerSettings.hpp +++ b/src/JackServerSettings.hpp @@ -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_; } diff --git a/src/Lv2Host.cpp b/src/Lv2Host.cpp index 68d5f62..d53f3da 100644 --- a/src/Lv2Host.cpp +++ b/src/Lv2Host.cpp @@ -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()); } diff --git a/src/Lv2Log.cpp b/src/Lv2Log.cpp index 55e3d2f..99c1b2c 100644 --- a/src/Lv2Log.cpp +++ b/src/Lv2Log.cpp @@ -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; } }; diff --git a/src/Lv2Log.hpp b/src/Lv2Log.hpp index b23680e..721c1a5 100644 --- a/src/Lv2Log.hpp +++ b/src/Lv2Log.hpp @@ -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) diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp new file mode 100644 index 0000000..1052052 --- /dev/null +++ b/src/PiLatencyMain.cpp @@ -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 +#include "PiPedalAlsa.hpp" +#include "Lv2Log.hpp" +#include +#include "AlsaDriver.hpp" +#include +#include + +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 [] \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()); + + 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; +} \ No newline at end of file diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp index 8e65811..c8ec346 100644 --- a/src/PiPedalAlsa.cpp +++ b/src/PiPedalAlsa.cpp @@ -167,9 +167,99 @@ std::vector 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 GetAlsaDevices(const char*devname, const char*direction) +{ + std::vector 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 pipedal::GetAlsaMidiInputDevices() +{ + return GetAlsaDevices("rawmidi","Input"); +} +std::vector 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() diff --git a/src/PiPedalAlsa.hpp b/src/PiPedalAlsa.hpp index 7dc783f..58eefec 100644 --- a/src/PiPedalAlsa.hpp +++ b/src/PiPedalAlsa.hpp @@ -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 GetAlsaDevices(); }; + std::vector GetAlsaMidiInputDevices(); + std::vector GetAlsaMidiOutputDevices(); } \ No newline at end of file diff --git a/src/PiPedalAlsaTest.cpp b/src/PiPedalAlsaTest.cpp index 9316a0c..17f2db9 100644 --- a/src/PiPedalAlsaTest.cpp +++ b/src/PiPedalAlsaTest.cpp @@ -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; + + } diff --git a/src/PiPedalException.hpp b/src/PiPedalException.hpp index cbe37bb..54a9c9e 100644 --- a/src/PiPedalException.hpp +++ b/src/PiPedalException.hpp @@ -57,6 +57,10 @@ namespace pipedal : PiPedalException(std::forward(what)) { } + PiPedalStateException(const std::string &what) + : PiPedalException(what) + { + } }; class PiPedalLogicException : public PiPedalException { diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 0c375c1..212d287 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -19,6 +19,7 @@ #include "pch.h" #include "DeviceIdFile.hpp" +#include "AudioConfig.hpp" #include #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 - -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{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{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; diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index a76d314..5a93006 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -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() diff --git a/src/PrettyPrinter.hpp b/src/PrettyPrinter.hpp index 6293823..f4edc34 100644 --- a/src/PrettyPrinter.hpp +++ b/src/PrettyPrinter.hpp @@ -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; + }; + } + } \ No newline at end of file diff --git a/src/Storage.cpp b/src/Storage.cpp index c8c38d8..3eb9dd0 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -19,6 +19,7 @@ #include "pch.h" #include "Storage.hpp" +#include "AudioConfig.hpp" #include "PiPedalException.hpp" #include #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 *> 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*> newPresets; + std::vector *> newPresets; for (size_t i = 0; i < presets.presets().size(); ++i) { - std::unique_ptr* p = entries[presets.presets()[i].instanceId()]; + std::unique_ptr *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 Storage::GetPluginPresetValues(const std::string&pluginUri, uint64_t instanceId) +std::vector 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 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 Storage::GetPluginPresetValues(const std::string&plugi } uint64_t Storage::SavePluginPreset( - const std::string&pluginUri, - const std::string&name, - const std::map & values) + const std::string &pluginUri, + const std::string &name, + const std::map &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 Storage::GetFavorites() const +std::map Storage::GetFavorites() const { - std::map result; + std::map result; std::filesystem::path fileName = this->dataRoot / "favorites.json"; if (!std::filesystem::exists(fileName)) @@ -1291,10 +1287,9 @@ std::map Storage::GetFavorites() const reader.read(&result); } return result; - - } -void Storage::SetFavorites(const std::map&favorites) { +void Storage::SetFavorites(const std::map &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&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() - diff --git a/src/Storage.hpp b/src/Storage.hpp index 24032c7..af30a65 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -25,6 +25,7 @@ #include "PluginPreset.hpp" #include "Banks.hpp" #include "JackConfiguration.hpp" +#include "JackServerSettings.hpp" #include "WifiConfigSettings.hpp" #include "WifiDirectConfigSettings.hpp" #include @@ -100,6 +101,8 @@ public: const BankIndex & GetBanks() const { return bankIndex; } + JackServerSettings GetJackServerSettings(); + void SetJackServerSettings(const pipedal::JackServerSettings&jackServerSettings); void LoadWifiConfigSettings(); void LoadWifiDirectConfigSettings(); diff --git a/src/main.cpp b/src/main.cpp index 6fc8c76..02b7b1f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 #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); diff --git a/src/templates/jack.service.template b/src/templates/jack.service.template index 5ccd2f2..5c56805 100644 --- a/src/templates/jack.service.template +++ b/src/templates/jack.service.template @@ -3,6 +3,7 @@ Description=Jack Audio Daemon After=sound.target After=local-fs.target After=dbus.socket +After=pipedald [Service] diff --git a/src/templates/pipedald.service.template b/src/templates/pipedald.service.template index 037f4a2..6d4af18 100644 --- a/src/templates/pipedald.service.template +++ b/src/templates/pipedald.service.template @@ -1,6 +1,5 @@ [Unit] Description=${DESCRIPTION} -Before=jack.service After=network.target After=sound.target After=local-fs.target diff --git a/todo.txt b/todo.txt index a8ff528..189b5ff 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,14 @@ +JackConfiguration Input midi ports. + +JackSettings Input midi ports. + +Midi Port selection UI. + +CPU Use should be % of AVAILABLE time, not % of total. +MIDI device selection. +raw Alsa midi. + + Pri Description -----------------