diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b05a8c..bda523b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ install (FILES ${PROJECT_SOURCE_DIR}/build/src/notices.txt install (FILES ${PROJECT_SOURCE_DIR}/src/template.service ${PROJECT_SOURCE_DIR}/src/templateShutdown.service + ${PROJECT_SOURCE_DIR}/src/templateJack.service DESTINATION /etc/pipedal ) install(CODE diff --git a/config/config.json b/config/config.json index 4a733cc..4422a65 100644 --- a/config/config.json +++ b/config/config.json @@ -1,6 +1,6 @@ { /* One or more directories containing LV2 plugins, seperated by ':' */ - "lv2_path": "/usr/modep/lv2:/usr/local/lib/lv2", + "lv2_path": "/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2", /* A writable directory in which state files can be stored, preferrably only by the server process. */ diff --git a/debugConfig/config.json b/debugConfig/config.json index bfdaeda..ea7711f 100644 --- a/debugConfig/config.json +++ b/debugConfig/config.json @@ -1,7 +1,6 @@ { /* One or more directories containing LV2 plugins, seperated by ':' */ - "lv2_path": "/usr/modep/lv2:/usr/local/lib/lv2", - //"lv2_path" : "/usr/modep/lv2/rkr.lv2", + "lv2_path": "/usr/lib/lv2:/usr/local/lib/lv2:/usr/modep/lv2", /* A writable directory in which state files can be stored, preferrably only by the server process. */ diff --git a/install b/install index 430e023..8b72fb4 100755 --- a/install +++ b/install @@ -1 +1 @@ -cmake --install /home/patch/src/pipedal/build --prefix /usr/local --config Release +cmake --install build --prefix /usr/local --config Release -v diff --git a/react/CMakeLists.txt b/react/CMakeLists.txt index c8f912a..c986a24 100644 --- a/react/CMakeLists.txt +++ b/react/CMakeLists.txt @@ -25,6 +25,7 @@ add_custom_command( src/JackServerSettingsDialog.tsx src/JackServerSettings.tsx src/DialogEx.tsx + src/AlsaDeviceInfo.tsx src/AboutDialog.tsx src/App.test.tsx diff --git a/react/public/var/config.json b/react/public/var/config.json index c1902a0..a16e5a4 100644 --- a/react/public/var/config.json +++ b/react/public/var/config.json @@ -1,5 +1,5 @@ { - "socket_server_port": 80, + "socket_server_port": 8080, "socket_server_address": "*", "max_upload_size": 1048576, diff --git a/react/src/AlsaDeviceInfo.tsx b/react/src/AlsaDeviceInfo.tsx new file mode 100644 index 0000000..0cd10ff --- /dev/null +++ b/react/src/AlsaDeviceInfo.tsx @@ -0,0 +1,66 @@ + + +export default class AlsaDeviceInfo { + deserialize(input: any): AlsaDeviceInfo { + this.cardId = input.cardId; + this.id = input.id; + this.name = input.name; + this.longName = input.longName; + this.sampleRates = input.sampleRates as number[]; + this.minBufferSize = input.minBufferSize; + this.maxBufferSize = input.maxBufferSize; + return this; + } + static deserialize_array(input: any): AlsaDeviceInfo[] + { + let result: AlsaDeviceInfo[] = []; + for (let i = 0; i < input.length; ++i) + { + result.push(new AlsaDeviceInfo().deserialize(input[i])); + } + return result; + } + closestSampleRate(sr: number): number + { + let result = 48000; + let bestError = 1E36; + for (let rate of this.sampleRates) + { + let error = (sr-rate)*(sr-rate); + if (error < bestError) + { + bestError = error; + result = rate; + } + + } + return result; + } + closestBufferSize(buffSize: number): number + { + let result = 64; + let bestError = 1E36; + + for (let sz = 2; sz <= this.maxBufferSize; sz *= 2) + { + if (sz >= this.minBufferSize) + { + let error = (sz-buffSize)*(sz-buffSize); + if (error < bestError) + { + bestError = error; + result = sz; + } + } + } + return result; + } + + cardId: number = -1; + id: string = ""; + name: string = ""; + longName: string = ""; + sampleRates: number[] = []; + minBufferSize: number = 0; + maxBufferSize: number = 0; +}; \ No newline at end of file diff --git a/react/src/JackServerSettings.tsx b/react/src/JackServerSettings.tsx index f44a8cd..0fd5b51 100644 --- a/react/src/JackServerSettings.tsx +++ b/react/src/JackServerSettings.tsx @@ -23,31 +23,37 @@ export default class JackServerSettings { deserialize(input: any) : JackServerSettings{ this.valid = input.valid; this.rebootRequired = input.rebootRequired; + this.alsaDevice = input.alsaDevice?? ""; this.sampleRate = input.sampleRate; this.bufferSize = input.bufferSize; this.numberOfBuffers = input.numberOfBuffers; return this; } - constructor(sampleRate?: number, bufferSize?: number, numberOfBuffers?: number) + // constructor(alsaDevice: string, sampleRate?: number, bufferSize?: number, numberOfBuffers?: number) + // { + // if (sampleRate) this.sampleRate = sampleRate; + // if (bufferSize) this.bufferSize = bufferSize; + // if (numberOfBuffers) this.numberOfBuffers = numberOfBuffers; + // if (numberOfBuffers) { + // this.valid = true; + // } + // } + clone(): JackServerSettings { - if (sampleRate) this.sampleRate = sampleRate; - if (bufferSize) this.bufferSize = bufferSize; - if (numberOfBuffers) this.numberOfBuffers = numberOfBuffers; - if (numberOfBuffers) { - this.valid = true; - } + return new JackServerSettings().deserialize(this); } valid: boolean = false; rebootRequired = false; + alsaDevice: string = ""; sampleRate = 48000; bufferSize = 64; numberOfBuffers = 3; getSummaryText() { if (this.valid) { - return "Sample Rate: " + this.sampleRate + " BufferSize: " + this.bufferSize + " Number of Buffers: " + this.numberOfBuffers; + return this.alsaDevice + " Sample Rate: " + this.sampleRate + " BufferSize: " + this.bufferSize + " Number of Buffers: " + this.numberOfBuffers; } else { - return ""; + return "Not configured"; } } diff --git a/react/src/JackServerSettingsDialog.tsx b/react/src/JackServerSettingsDialog.tsx index 3729ce6..494be63 100644 --- a/react/src/JackServerSettingsDialog.tsx +++ b/react/src/JackServerSettingsDialog.tsx @@ -35,9 +35,15 @@ import DialogContent from '@material-ui/core/DialogContent'; import MenuItem from '@material-ui/core/MenuItem'; import { nullCast } from './Utility'; import Typography from '@material-ui/core/Typography'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import AlsaDeviceInfo from './AlsaDeviceInfo'; + +const INVALID_DEVICE_ID = "_invalid_"; interface JackServerSettingsDialogState { latencyText: string; + jackServerSettings: JackServerSettings; + alsaDevices?: AlsaDeviceInfo[]; }; const styles = (theme: Theme) => @@ -54,116 +60,297 @@ export interface JackServerSettingsDialogProps extends WithStyles open: boolean; jackServerSettings: JackServerSettings; onClose: () => void; - onApply: (sampleRate: number, bufferSize: number, numberOfBuffers: number) => void; + onApply: (jackServerSettings: JackServerSettings) => void; } - -function getLatencyText(sampleRate: number, bufferSize: number, numberOfBuffers: number): string { - let ms = bufferSize * numberOfBuffers / sampleRate * 1000; + +function getLatencyText(settings: JackServerSettings ): string { + if (!settings.valid) return "\u00A0"; + let ms = settings.bufferSize * settings.numberOfBuffers / settings.sampleRate * 1000; return ms.toFixed(1) + "ms"; } const JackServerSettingsDialog = withStyles(styles)( class extends Component { + + model: PiPedalModel; + constructor(props: JackServerSettingsDialogProps) { super(props); - let jackServerSettings = props.jackServerSettings; + this.model = PiPedalModelFactory.getInstance(); + this.state = { - latencyText: - getLatencyText( - jackServerSettings.sampleRate, - jackServerSettings.bufferSize, - jackServerSettings.numberOfBuffers) + latencyText: "\u00A0", + jackServerSettings: props.jackServerSettings.clone() // invalid, but not nullish }; } + mounted: boolean = false; + + requestAlsaInfo() { + this.model.getAlsaDevices() + .then((devices) => { + if (this.mounted) { + if (this.props.open) { + let settings = this.applyAlsaDevices(this.props.jackServerSettings, devices); + this.setState({ + alsaDevices: devices, + jackServerSettings: settings, + latencyText: getLatencyText(settings) + }); + } else { + this.setState({ alsaDevices: devices }); + } + } + }) + .catch((error) => { - updateLatency(e: any): void { - setTimeout(()=> { - let sampleRate: number = parseInt(nullCast(document.getElementById('jsd_sampleRate')).value); - let bufferSize: number = parseInt(nullCast(document.getElementById('jsd_bufferSize')).value); - let bufferCount: number = parseInt(nullCast(document.getElementById('jsd_bufferCount')).value); - this.setState({ - latencyText: getLatencyText(sampleRate, bufferSize, bufferCount) }); - },0); } - handleApply() { - let sampleRate = parseInt(nullCast(document.getElementById('jsd_sampleRate')).value); - let bufferSize = parseInt(nullCast(document.getElementById('jsd_bufferSize')).value); - let bufferCount = parseInt(nullCast(document.getElementById('jsd_bufferCount')).value); - this.props.onApply(sampleRate,bufferSize,bufferCount); + applyAlsaDevices(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[]) { + let result = jackServerSettings.clone(); + if (!alsaDevices) { + return result; + } + if (alsaDevices.length === 0) { + result.valid = false; + result.alsaDevice = INVALID_DEVICE_ID; + return result; + } + + let selectedDevice: AlsaDeviceInfo | undefined = undefined; + for (let i = 0; i < alsaDevices.length; ++i) { + if (alsaDevices[i].id === result.alsaDevice) { + selectedDevice = alsaDevices[i] + break; + } + } + if (!selectedDevice) { + selectedDevice = alsaDevices[0]; + result.alsaDevice = selectedDevice.id; + + } + if (result.sampleRate === 0) result.sampleRate = 48000; + if (result.bufferSize === 0) result.bufferSize = 64; + if (result.numberOfBuffers === 0) result.numberOfBuffers = 3; + result.sampleRate = selectedDevice.closestSampleRate(result.sampleRate); + result.bufferSize = selectedDevice.closestBufferSize(result.bufferSize); + result.valid = true; + return result; + } + + componentDidMount() { + this.mounted = true; + this.requestAlsaInfo(); + + } + componentDidUpdate(oldProps: JackServerSettingsDialogProps) { + if (this.props.open && !oldProps.open) { + let settings = this.applyAlsaDevices(this.props.jackServerSettings.clone(), this.state.alsaDevices); + this.setState({ + jackServerSettings: settings, + latencyText: getLatencyText(settings) + }); + this.requestAlsaInfo(); + } + } + + componentWillUnmount() { + this.mounted = false; + + } + getSelectedDevice(deviceId: string): AlsaDeviceInfo | null { + if (!this.state.alsaDevices) return null; + for (let i = 0; i < this.state.alsaDevices.length; ++i) { + if (this.state.alsaDevices[i].id === deviceId) { + return this.state.alsaDevices[i]; + } + + } + return null; + } + + handleDeviceChanged(e: any) { + let device = e.target.value as string; + + let selectedDevice = this.getSelectedDevice(device); + if (!selectedDevice) return; + let settings = this.state.jackServerSettings.clone(); + settings.alsaDevice = device; + settings.sampleRate = selectedDevice.closestSampleRate(settings.sampleRate); + settings.bufferSize = selectedDevice.closestSampleRate(settings.bufferSize); + + this.setState({ + jackServerSettings: settings, + latencyText: getLatencyText(settings) + }); + } + handleRateChanged(e: any) { + let rate = e.target.value as number; + console.log("New rate: " + rate); + let selectedDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaDevice); + if (!selectedDevice) return; + let settings = this.state.jackServerSettings.clone(); + settings.sampleRate = rate; + + this.setState({ + jackServerSettings: settings, + latencyText: getLatencyText(settings) + }); + } + handleSizeChanged(e: any) { + let size = e.target.value as number; + + let selectedDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaDevice); + if (!selectedDevice) return; + let settings = this.state.jackServerSettings.clone(); + settings.bufferSize = size; + + this.setState({ + jackServerSettings: settings, + latencyText: getLatencyText(settings) + }); + } + handleNumberOfBuffersChanged(e: any) { + let bufferCount = e.target.value as number; + + let selectedDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaDevice); + if (!selectedDevice) return; + let settings = this.state.jackServerSettings.clone(); + settings.numberOfBuffers = bufferCount; + + this.setState({ + jackServerSettings: settings, + latencyText: getLatencyText(settings) + }); + } + + handleApply() { + + this.props.onApply(this.state.jackServerSettings.clone()); }; - + getBufferSizes(alsaDevice: AlsaDeviceInfo): number[] { + let result: number[] = []; + let max = alsaDevice.maxBufferSize; + if (max > 2048) max = 2048; + + for (let i = 16; i <= max; i *= 2) { + if (i >= alsaDevice.minBufferSize) { + result.push(i); + } + } + return result; + } + render() { const classes = this.props.classes; - const { onClose, jackServerSettings, open } = this.props; + const { onClose, open } = this.props; const handleClose = () => { onClose(); }; + let waitingForDevices = !this.state.alsaDevices + let noDevices = this.state.alsaDevices && this.state.alsaDevices.length === 0; + let selectedDevice: AlsaDeviceInfo | undefined = undefined; + if (this.state.alsaDevices) { + for (let device of this.state.alsaDevices) { + if (device.id === this.state.jackServerSettings.alsaDevice) { + selectedDevice = device; + break; + } + } + } + let bufferSizes: number[] = []; + if (selectedDevice) { + bufferSizes = this.getBufferSizes(selectedDevice); + } return ( - - Sample rate - this.handleDeviceChanged(e)} + value={this.state.jackServerSettings.alsaDevice} + style={{width: 220}} + inputProps={{ + name: "Device", + id: "jsd_device", + }} + > + {(noDevices && !waitingForDevices) && + ( + No suitable devices. + ) } - }} - > - 44100 - 48000 - 88200 - 96000 - 176400 - 192000 - - -
- - Buffer size - +
- Buffers + Sample rate +
+ + Buffer size + + + + Buffers + + +
@@ -175,7 +362,8 @@ const JackServerSettingsDialog = withStyles(styles)( - diff --git a/react/src/PiPedalModel.tsx b/react/src/PiPedalModel.tsx index 504f57e..0dfbc0b 100644 --- a/react/src/PiPedalModel.tsx +++ b/react/src/PiPedalModel.tsx @@ -34,6 +34,7 @@ import MidiBinding from './MidiBinding'; import PluginPreset from './PluginPreset'; import WifiConfigSettings from './WifiConfigSettings'; import WifiChannel from './WifiChannel'; +import AlsaDeviceInfo from './AlsaDeviceInfo'; export enum State { @@ -348,6 +349,7 @@ export interface PiPedalModel { getWifiChannels(countryIso3661: string): Promise; + getAlsaDevices() : Promise; }; class PiPedalModelImpl implements PiPedalModel { @@ -1586,6 +1588,23 @@ class PiPedalModelImpl implements PiPedalModel { return result; } + getAlsaDevices() : Promise + { + let result = new Promise((resolve, reject) => { + if (!this.webSocket) { + reject("Connection closed."); + return; + } + this.webSocket.request("getAlsaDevices") + .then((data) => { + resolve(AlsaDeviceInfo.deserialize_array(data)); + }) + .catch((err) => reject(err)); + }); + return result; + + } + }; diff --git a/react/src/SettingsDialog.tsx b/react/src/SettingsDialog.tsx index 899c8a6..a8a6a73 100644 --- a/react/src/SettingsDialog.tsx +++ b/react/src/SettingsDialog.tsx @@ -400,36 +400,19 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( overflowX: "hidden", overflowY: "auto", marginTop: 22 }} > - {!isConfigValid && ( -
- - Error - - - The server encounter the following problem: - - - {this.state.jackConfiguration.errorState} - - - Please get the Jack Audio Server running properly on the server before attempting to configure audio. - - -
- - )} - - -
+
AUDIO
- {JackHostStatus.getDisplayView("Status:\u00A0", this.state.jackStatus)} + { (!isConfigValid) ? + ( + Status: Not configured. + ): + JackHostStatus.getDisplayView("Status:\u00A0", this.state.jackStatus) + }
this.handleJackServerSettings()} - disabled={!(isConfigValid && this.state.jackServerSettings.valid)} - style={{ opacity: !(isConfigValid && this.state.jackServerSettings.valid) ? 0.6 : 1.0 }} >
@@ -441,9 +424,11 @@ const SettingsDialog = withStyles(styles, { withTheme: true })( open={this.state.showJackServerSettingsDialog} jackServerSettings={this.state.jackServerSettings} onClose={() => this.setState({ showJackServerSettingsDialog: false })} - onApply={(sampleRate, bufferSize, numberOfBuffers) => { - this.setState({ showJackServerSettingsDialog: false }); - this.model.setJackServerSettings(new JackServerSettings(sampleRate, bufferSize, numberOfBuffers)); + onApply={(jackServerSettings) => { + this.setState({ showJackServerSettingsDialog: false, + jackServerSettings: jackServerSettings + }); + this.model.setJackServerSettings(jackServerSettings); }} /> diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c4b847e..85d57c8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -118,6 +118,7 @@ set (PIPEDAL_SOURCES WifiChannels.hpp WifiChannels.cpp RegDb.cpp RegDb.hpp + PiPedalAlsa.hpp PiPedalAlsa.cpp ) configure_file(config.hpp.in config.hpp) @@ -136,7 +137,7 @@ target_include_directories(pipedald PRIVATE ${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS} ${LIBNL3_INCLUDE_DIRS} ${LVDEV_INCLUDE_DIRS} ) -target_link_libraries(pipedald PRIVATE pthread atomic stdc++fs +target_link_libraries(pipedald PRIVATE pthread atomic stdc++fs asound ${LILV_0_LIBRARIES} ${JACK_LIBRARIES} ${LIBNL3_LIBRARIES} systemd ) @@ -144,6 +145,7 @@ target_link_libraries(pipedald PRIVATE pthread atomic stdc++fs add_executable(pipedaltest ${PIPEDAL_SOURCES} testMain.cpp jsonTest.cpp WifiChannelsTest.cpp + PiPedalAlsaTest.cpp SystemConfigFile.hpp SystemConfigFile.cpp SystemConfigFileTest.cpp @@ -160,7 +162,7 @@ target_include_directories(pipedaltest PRIVATE ${JACK_INCLUDE_DIRS} ${LILV_0_INCLUDE_DIRS} ${LIBNL3_INCLUDE_DIRS} ) -target_link_libraries(pipedaltest PRIVATE pthread atomic stdc++fs +target_link_libraries(pipedaltest PRIVATE pthread atomic stdc++fs asound ${LILV_0_LIBRARIES} ${JACK_LIBRARIES} ${LIBNL3_LIBRARIES} systemd ) diff --git a/src/ConfigMain.cpp b/src/ConfigMain.cpp index d765513..170eb05 100644 --- a/src/ConfigMain.cpp +++ b/src/ConfigMain.cpp @@ -18,6 +18,8 @@ // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include +#include +#include #include "CommandLineParser.hpp" #include "PiPedalException.hpp" #include @@ -27,27 +29,29 @@ #include "SetWifiConfig.hpp" #include "SysExec.hpp" #include +#include using namespace std; using namespace pipedal; #define SERVICE_ACCOUNT_NAME "pipedal_d" #define SERVICE_GROUP_NAME "pipedal_d" +#define JACK_SERVICE_ACCOUNT_NAME "jack" +#define JACK_SERVICE_GROUP_NAME "jack" +#define AUDIO_SERVICE_GROUP_NAME "audio" #define SYSTEMCTL_BIN "/usr/bin/systemctl" #define GROUPADD_BIN "/usr/sbin/groupadd" -#define USERADD_BIN "/usr/sbin/useradd" +#define USERADD_BIN "/usr/sbin/useradd" #define USERMOD_BIN "/usr/sbin/usermod" #define CHGRP_BIN "/usr/bin/chgrp" #define CHOWN_BIN "/usr/bin/chown" #define CHMOD_BIN "/usr/bin/chmod" - #define SERVICE_PATH "/usr/lib/systemd/system" #define NATIVE_SERVICE "pipedald" #define SHUTDOWN_SERVICE "pipedalshutdownd" -// #define REACT_SERVICE "pipedal_react" - +#define JACK_SERVICE "jack" std::filesystem::path GetServiceFileName(const std::string &serviceName) { @@ -113,6 +117,14 @@ void StopService() cout << "Error: Failed to stop the " SHUTDOWN_SERVICE " service."; } } + +void Uninstall() { + StopService(); + DisableService(); + system(SYSTEMCTL_BIN " stop jack"); + system(SYSTEMCTL_BIN " disable jack"); +} + void StartService() { @@ -132,8 +144,98 @@ void RestartService() StartService(); } +static bool userExists(const char *userName) +{ + struct passwd pwd; + struct passwd *result; + char *buf; + size_t bufsize; + int s; + + bufsize = sysconf(_SC_GETPW_R_SIZE_MAX); + if (bufsize == -1) /* Value was indeterminate */ + bufsize = 16384; /* Should be more than enough */ + + buf = (char *)malloc(bufsize); + if (buf == NULL) + { + perror("malloc"); + exit(EXIT_FAILURE); + } + + s = getpwnam_r(userName, &pwd, buf, bufsize, &result); + if (result == NULL) + { + return false; + } + return true; +} + +void InstallLimits() +{ + if (SysExec(GROUPADD_BIN " -f " AUDIO_SERVICE_GROUP_NAME) != EXIT_SUCCESS) + { + throw PiPedalException("Failed to create audio service group."); + } + + std::filesystem::path limitsConfig = "/usr/security/audio.conf"; + + if (!std::filesystem::exists(limitsConfig)) + { + ofstream output(limitsConfig); + output << "# Realtime priority group used by pipedal/jack daemon" << endl; + output << "@audio - rtprio 95" << endl; + output << "@audio - memlock unlimited" << endl; + } + + +} + +void MaybeStartJackService() { + std::filesystem::path drcFile = "/etc/jackdrc"; + + if (std::filesystem::exists(drcFile) && std::filesystem::file_size(drcFile) != 0) { + system(SYSTEMCTL_BIN " start jack"); + } else { + system(SYSTEMCTL_BIN " mask jack"); + } +} +void InstallJackService() +{ + InstallLimits(); + if (SysExec(GROUPADD_BIN " -f " JACK_SERVICE_GROUP_NAME) != EXIT_SUCCESS) + { + throw PiPedalException("Failed to create jack service group."); + } + if (!userExists(JACK_SERVICE_ACCOUNT_NAME)) + { + if (SysExec(USERADD_BIN " " JACK_SERVICE_ACCOUNT_NAME " -g " JACK_SERVICE_GROUP_NAME " -M -N -r") != EXIT_SUCCESS) + { + // throw PiPedalException("Failed to create service account."); + } + // lock account for login. + SysExec("passwd -l " JACK_SERVICE_ACCOUNT_NAME); + } + + // Add to audio groups. + SysExec(USERMOD_BIN " -a -G " JACK_SERVICE_GROUP_NAME " " JACK_SERVICE_ACCOUNT_NAME); + SysExec(USERMOD_BIN " -a -G" AUDIO_SERVICE_GROUP_NAME " " JACK_SERVICE_ACCOUNT_NAME); + + // deploy the systemd service file + std::map map; // nothing to customize. + + WriteTemplateFile(map, std::filesystem::path("/etc/pipedal/templateJack.service"), GetServiceFileName(JACK_SERVICE)); + + + MaybeStartJackService(); + +} + + void Install(const std::filesystem::path &programPrefix, const std::string endpointAddress) { + + InstallJackService(); auto endpos = endpointAddress.find_last_of(':'); if (endpos == string::npos) { @@ -168,13 +270,15 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo { throw PiPedalException("Failed to create service group."); } - if (SysExec(USERADD_BIN " " SERVICE_ACCOUNT_NAME " -g " SERVICE_GROUP_NAME " -M -N -r") != EXIT_SUCCESS) + if (!userExists(SERVICE_ACCOUNT_NAME)) { - // throw PiPedalException("Failed to create service account."); + if (SysExec(USERADD_BIN " " SERVICE_ACCOUNT_NAME " -g " SERVICE_GROUP_NAME " -M -N -r") != EXIT_SUCCESS) + { + // throw PiPedalException("Failed to create service account."); + } + // lock account for login. + SysExec("passwd -l " SERVICE_ACCOUNT_NAME); } - // lock account for login. - SysExec("passwd -l " SERVICE_ACCOUNT_NAME); - // Add to audio groups. SysExec(USERMOD_BIN " -a -G jack " SERVICE_ACCOUNT_NAME); @@ -278,8 +382,9 @@ void Install(const std::filesystem::path &programPrefix, const std::string endpo cout << "Complete" << endl; } -int SudoExec(char**argv) { - int pbPid; +int SudoExec(char **argv) +{ + int pbPid; int returnValue = 0; if ((pbPid = fork()) == 0) @@ -295,7 +400,6 @@ int SudoExec(char**argv) { } } - int main(int argc, char **argv) { CommandLineParser parser; @@ -328,7 +432,7 @@ int main(int argc, char **argv) { parser.Parse(argc, (const char **)argv); - int actionCount = install + uninstall + stop + start + enable + disable + enable_ap + disable_ap; + int actionCount = install + uninstall + stop + start + enable + disable + enable_ap + disable_ap + restart; if (actionCount > 1) { throw PiPedalException("Please provide only one action."); @@ -364,7 +468,8 @@ int main(int argc, char **argv) if (help || helpError) { - if (helpError) cout << endl; + if (helpError) + cout << endl; cout << "pipedalconfig - Post-install configuration for pipdeal" << endl << "Copyright (c) 2021 Robin Davies. All rights reserved." << endl @@ -409,7 +514,7 @@ int main(int argc, char **argv) << "Description:" << endl << " pipedalconfig performs post-install configuration of pipedal." << endl << endl; - exit(helpError? EXIT_FAILURE: EXIT_SUCCESS); + exit(helpError ? EXIT_FAILURE : EXIT_SUCCESS); } if (portOption.size() != 0 && !install) { @@ -421,16 +526,16 @@ int main(int argc, char **argv) if (uid != 0 && !nopkexec) { // re-execute with PKEXEC in order to prompt form SUDO credentials. - std::vector args; + std::vector args; std::string pkexec = "/usr/bin/pkexec"; // staged because "ISO C++ forbids converting a string constant to std::vector::value_type"(!) - args.push_back((char*)(pkexec.c_str())); + args.push_back((char *)(pkexec.c_str())); std::filesystem::path path = std::filesystem::absolute(argv[0]); std::string sPath = path; - args.push_back((char*)path.c_str()); + args.push_back((char *)path.c_str()); for (int arg = 1; arg < argc; ++arg) { - args.push_back((char*)argv[arg]); + args.push_back((char *)argv[arg]); } std::string prefixArg; // lifetime management for the prefix arguments @@ -440,18 +545,14 @@ int main(int argc, char **argv) { std::filesystem::path prefix = std::filesystem::path(argv[0]).parent_path().parent_path(); prefixOptionArg = "-prefix"; - args.push_back((char*)(prefixOptionArg.c_str())); + args.push_back((char *)(prefixOptionArg.c_str())); prefixArg = prefix; - args.push_back((char*)prefixArg.c_str()); + args.push_back((char *)prefixArg.c_str()); } args.push_back(nullptr); - char**rawArgv = &args[0]; + char **rawArgv = &args[0]; return SudoExec(rawArgv); - - - - } try @@ -488,6 +589,7 @@ int main(int argc, char **argv) } else if (uninstall) { + Uninstall(); } else if (stop) { diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index 3543a61..4acbeaa 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -55,6 +55,7 @@ namespace pipedal size_t GetBlockLength() const { return blockLength_; } size_t GetMidiBufferSize() const { return midiBufferSize_;} double GetMaxAllowedMidiDelta() const { return maxAllowedMidiDelta_; } + void SetErrorStatus(const std::string&message) { this->errorStatus_ = message; } const std::vector &GetInputAudioPorts() const { return inputAudioPorts_; } const std::vector &GetOutputAudioPorts() const { return outputAudioPorts_; } diff --git a/src/JackHost.cpp b/src/JackHost.cpp index 4eae928..2834d99 100644 --- a/src/JackHost.cpp +++ b/src/JackHost.cpp @@ -68,6 +68,7 @@ namespace pipedal class JackHostImpl : public JackHost { private: + std::recursive_mutex mutex; int64_t overrunGracePeriodSamples = 0; @@ -145,7 +146,7 @@ private: auto port = outputPorts[i]; if (port != nullptr) { - jack_disconnect(client, channelSelection.getOutputAudioPorts()[i].c_str(), jack_port_name(port)); + jack_disconnect(client, jack_port_name(port),channelSelection.getOutputAudioPorts()[i].c_str()); } } for (size_t i = 0; i < midiInputPorts.size(); ++i) @@ -906,6 +907,7 @@ public: virtual void Open(const JackChannelSelection &channelSelection) { + std::lock_guard guard(mutex); this->currentSample = 0; @@ -1262,11 +1264,18 @@ private: { this_->restarting = true; this_->Close(); - ShutdownClient::SetJackServerConfiguration(jackServerSettings); - this_->Open(this_->channelSelection); - this_->restarting = false; - onComplete(true, ""); - isComplete = true; + try { + ShutdownClient::SetJackServerConfiguration(jackServerSettings); + this_->Open(this_->channelSelection); + this_->restarting = false; + onComplete(true, ""); + isComplete = true; + } catch (const std::exception &e) + { + onComplete(false,e.what()); + this_->restarting = false; + isComplete = true; + } } static void ThreadProc_(RestartThread *this_) { diff --git a/src/JackHost.hpp b/src/JackHost.hpp index 4cdb300..d78befb 100644 --- a/src/JackHost.hpp +++ b/src/JackHost.hpp @@ -27,6 +27,7 @@ #include "json.hpp" #include "JackServerSettings.hpp" #include +#include "PiPedalAlsa.hpp" namespace pipedal { @@ -118,7 +119,6 @@ public: class IHost; class JackHost { - protected: JackHost() { } public: diff --git a/src/JackServerSettings.cpp b/src/JackServerSettings.cpp index e65c3c9..c86a71a 100644 --- a/src/JackServerSettings.cpp +++ b/src/JackServerSettings.cpp @@ -23,6 +23,9 @@ #include "JackServerSettings.hpp" #include #include "PiPedalException.hpp" +#include +#include "unistd.h" +#include #define DRC_FILENAME "/etc/jackdrc" @@ -55,24 +58,46 @@ static std::vector SplitArgs(const char *szBuff) return result; } -static uint64_t GetJackArg(const std::vector &args, const char *shortOption, const char *longOption) +static std::string GetJackStringArg(const std::vector &args, const std::string&shortOption, const std::string&longOption) { - int pos = -1; + std::string strVal; for (size_t i = 1; i < args.size(); ++i) { - if (args[i] == shortOption || args[i] == longOption) - { - pos = i; + auto pos = args[i].rfind(longOption,0); + if (pos != std::string::npos) { + if (args[i].length() == longOption.length()) + { + if (i == args.size()-1) { + throw PiPedalException("Can't read Jack configuration."); + } + strVal = args[i+1]; + } else { + strVal = args[i].substr(longOption.length()); + } + break; + } + pos = args[i].rfind(shortOption,0); + if (pos != std::string::npos) { + if (args[i].length() == shortOption.length()) + { + if (i == args.size()-1) { + throw PiPedalException("Can't read Jack configuration."); + } + strVal = args[i+1]; + } else { + strVal = args[i].substr(shortOption.length()); + } break; } } - if (pos == -1 || pos == args.size() - 1) - { - throw PiPedalException("Can't read Jack configuration."); - } + return strVal; +} +static std::int32_t GetJackArg(const std::vector &args, const std::string&shortOption, const std::string&longOption) +{ + std::string strVal = GetJackStringArg(args,shortOption,longOption); try { - unsigned long value = std::stoul(args[pos + 1]); + unsigned long value = std::stoul(strVal); return value; } catch (const std::exception &) @@ -80,35 +105,13 @@ static uint64_t GetJackArg(const std::vector &args, const char *sho throw PiPedalException("Can't read Jack configuration."); } } -static void SetJackArg(std::vector &args, const char *shortOption, const char *longOption,uint64_t value) -{ - int pos = -1; - for (size_t i = 1; i < args.size(); ++i) - { - if (args[i] == shortOption || args[i] == longOption) - { - pos = i; - break; - } - } - if (pos == -1 || pos == args.size() - 1) - { - throw PiPedalException("Can't read Jack configuration."); - } - stringstream s; - s << value; - args[pos+1] = s.str(); - return; -} - void JackServerSettings::ReadJackConfiguration() { this->valid_ = false; - char firstLine[1024]; - + std::string lastLine; { ifstream input(DRC_FILENAME); @@ -120,22 +123,32 @@ void JackServerSettings::ReadJackConfiguration() while (true) { - if (input.eof()) - { - Lv2Log::error("Premature end of file in " DRC_FILENAME); - return; + std::string line; + std::getline(input,line); + if (line.length() != 0) { + lastLine = line; } - input.getline(firstLine, sizeof(firstLine)); - if (firstLine[0] != '#') + if (input.eof()) { break; + } } - try { - std::vector < std::string> argv = SplitArgs(firstLine); - this->bufferSize_ = GetJackArg(argv, "-p", "-period"); - this->numberOfBuffers_ = (uint32_t)GetJackArg(argv, "-n", "-nperiods"); - this->sampleRate_ = (uint32_t)GetJackArg(argv, "-r", "-rate"); + try + { + std::vector argv = SplitArgs(lastLine.c_str()); + for (auto i = argv.begin(); i != argv.end(); ++i) + { + if ((*i) == "-dalsa") { + argv.erase(i); + break; + } + } + this->bufferSize_ = GetJackArg(argv, "-p", "--period"); + this->numberOfBuffers_ = (uint32_t)GetJackArg(argv, "-n", "--nperiods"); + this->sampleRate_ = (uint32_t)GetJackArg(argv, "-r", "--rate"); + this->alsaDevice_ = GetJackStringArg(argv,"-d", "--device"); valid_ = true; - } catch (std::exception &) + } + catch (std::exception &) { Lv2Log::error("Can't parse " DRC_FILENAME); } @@ -147,10 +160,10 @@ void JackServerSettings::Write() this->valid_ = false; std::vector precedingLines; - std::vector < std::string> argv; + std::vector argv; + std::string lastLine; - { - char firstLine[1024]; + if (std::filesystem::exists(DRC_FILENAME)) { ifstream input(DRC_FILENAME); if (!input.is_open()) @@ -164,46 +177,64 @@ void JackServerSettings::Write() if (input.eof()) { Lv2Log::error("Premature end of file in " DRC_FILENAME); - return; - } - input.getline(firstLine, sizeof(firstLine)); - if (firstLine[0] != '#') break; - precedingLines.push_back(firstLine); + } + std::getline(input,lastLine); + precedingLines.push_back(lastLine); + if (input.eof()) + { + break; + } } - - // set new values for arguments. - argv = SplitArgs(firstLine); - try { - SetJackArg(argv, "-p", "-period", this->bufferSize_); - SetJackArg(argv, "-n", "-nperiods", this->numberOfBuffers_); - SetJackArg(argv, "-r", "-rate",this->sampleRate_); - } catch (std::exception &) + // erase blank lines at the end. + while (precedingLines.size() != 0 && precedingLines[precedingLines.size()-1] == "") { - Lv2Log::error("Can't parse " DRC_FILENAME); - return; + precedingLines.erase(precedingLines.begin()+precedingLines.size()-1); } - } + // erase the last line, which should contain the command invocation. + if (precedingLines.size() != 0) + { + precedingLines.erase(precedingLines.begin()+precedingLines.size()-1); + } + } // write to the output. - try { + try + { ofstream output(DRC_FILENAME); if (!output.is_open()) { throw PiPedalException("Can't write " DRC_FILENAME); } - for (auto line: precedingLines) + if (precedingLines.size() == 0) + { + // jack1 incantation for promiscuous servers. + output << "#!/bin/sh" <alsaDevice_ + << " -r" << this->sampleRate_ + << " -p" << this->bufferSize_ + << " -n" << this->numberOfBuffers_ << " -Xseq" + << endl; + system("/usr/bin/chmod 755 " DRC_FILENAME); + system("pulseaudio --kill"); + system("/usr/bin/systemctl enable jack"); + if (system("/usr/bin/systemctl restart jack") != 0) { - if (i != 0) { - output << " "; - } - output << argv[i]; + throw PiPedalException("Failed to start jack audio service."); } - output << endl; - } catch (const std::exception &e) { + } + catch (const std::exception &e) + { stringstream s; s << "jack - " << e.what(); Lv2Log::error(s.str()); @@ -211,10 +242,10 @@ void JackServerSettings::Write() } JSON_MAP_BEGIN(JackServerSettings) - JSON_MAP_REFERENCE(JackServerSettings,valid) - JSON_MAP_REFERENCE(JackServerSettings,rebootRequired) - JSON_MAP_REFERENCE(JackServerSettings,sampleRate) - JSON_MAP_REFERENCE(JackServerSettings,bufferSize) - JSON_MAP_REFERENCE(JackServerSettings,numberOfBuffers) +JSON_MAP_REFERENCE(JackServerSettings, valid) +JSON_MAP_REFERENCE(JackServerSettings, rebootRequired) +JSON_MAP_REFERENCE(JackServerSettings, alsaDevice) +JSON_MAP_REFERENCE(JackServerSettings, sampleRate) +JSON_MAP_REFERENCE(JackServerSettings, bufferSize) +JSON_MAP_REFERENCE(JackServerSettings, numberOfBuffers) JSON_MAP_END() - diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp index f219393..48ab2e4 100644 --- a/src/JackServerSettings.hpp +++ b/src/JackServerSettings.hpp @@ -26,6 +26,7 @@ namespace pipedal { class JackServerSettings { bool valid_ = false; bool rebootRequired_ = false; + std::string alsaDevice_; uint64_t sampleRate_ = 0; uint32_t bufferSize_ = 0; uint32_t numberOfBuffers_ = 0; @@ -36,6 +37,7 @@ namespace pipedal { uint64_t GetSampleRate() const { return sampleRate_; } uint32_t GetBufferSize() const { return bufferSize_; } uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; } + const std::string&GetAlsaDevice() const { return alsaDevice_; } void ReadJackConfiguration(); diff --git a/src/Locale.cpp b/src/Locale.cpp index ba0a1e8..de936dd 100644 --- a/src/Locale.cpp +++ b/src/Locale.cpp @@ -22,6 +22,7 @@ #include "Locale.hpp" #include +#include "Lv2Log.hpp" // Must be UNICODE. Should reflect system locale. (.e.g en-US.UTF8, de-de.UTF8) @@ -36,12 +37,29 @@ static const char*getLocale(const char*localeEnvironmentVariable) { result = getenv("LC_ALL"); } + if (result == nullptr) { result = "en_US.UTF-8"; } + std::stringstream s; + s << "Locale: " << result; + Lv2Log::error(s.str()); return result; } -static std::locale collationLocale(getLocale("LC_COLLATION")); -const std::collate& Locale::collation = std::use_facet >(collationLocale); + +void Locale::setDefaultLocale() { + const char* locale = getLocale("LC_ALL"); + try { + setlocale(LC_ALL,locale); + } catch (const std::exception&) + { + std::stringstream s; + s << "Failed to set default locale (" << locale << "). Defaulting to en_US."; + Lv2Log::error(s.str()); + + std::setlocale(LC_ALL,"en_US.UTF-8"); + } +} +const std::collate& Locale::collation() { return std::use_facet >(std::locale()); } diff --git a/src/Locale.hpp b/src/Locale.hpp index 0fb0a26..78f4b6a 100644 --- a/src/Locale.hpp +++ b/src/Locale.hpp @@ -25,6 +25,8 @@ namespace pipedal { class Locale { public: - static const std::collate& collation; + static void setDefaultLocale(); + + static const std::collate& collation(); }; } diff --git a/src/Lv2Host.cpp b/src/Lv2Host.cpp index 9fd212e..44dfaf7 100644 --- a/src/Lv2Host.cpp +++ b/src/Lv2Host.cpp @@ -394,13 +394,14 @@ void Lv2Host::Load(const char *lv2Path) } } - auto compare = []( + const auto &collation = std::use_facet >(std::locale()); + auto compare = [&collation]( const std::shared_ptr &left, const std::shared_ptr &right) { const char *pb1 = left->name().c_str(); const char *pb2 = right->name().c_str(); - return Locale::collation.compare( + return collation.compare( pb1, pb1 + left->name().size(), pb2, pb2 + right->name().size()) < 0; }; @@ -1005,9 +1006,11 @@ std::vector Lv2Host::GetPluginPresets(const std::string &plugin } lilv_nodes_free(presets); - auto compare = [] (const Lv2PluginPreset&left, const Lv2PluginPreset&right) + auto& collation = std::use_facet >(std::locale()); + + auto compare = [&collation] (const Lv2PluginPreset&left, const Lv2PluginPreset&right) { - return Locale::collation.compare( + return collation.compare( left.name_.c_str(), left.name_.c_str()+left.name_.size(), right.name_.c_str(), diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp new file mode 100644 index 0000000..477a2ce --- /dev/null +++ b/src/PiPedalAlsa.cpp @@ -0,0 +1,200 @@ +// Copyright (c) 2021 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "pch.h" +#include "PiPedalAlsa.hpp" +#include "alsa/asoundlib.h" +#include "Lv2Log.hpp" +#include + +using namespace pipedal; + +static uint32_t RATES[] = { + 44100, 48000, 44100 * 2, 48000 * 2, 44100 * 4, 48000 * 4}; + + +std::mutex alsaMutex; + +std::vector PiPedalAlsaDevices::GetAvailableAlsaDevices() +{ + std::lock_guard guard{alsaMutex}; + + std::vector result; + + int cardNum = -1; // Start with first card + int err; + + for (;;) + { + if ((err = snd_card_next(&cardNum)) < 0) + { + Lv2Log::error("Unexpected error enumerating ALSA devices."); + break; + } + if (cardNum < 0) + // No more cards + break; + + { + std::stringstream ss; + ss << "hw:" << cardNum; + std::string cardId = ss.str(); + + snd_ctl_t *hDevice = nullptr; + + if ((err = snd_ctl_open(&hDevice, cardId.c_str(), 0)) < 0) + { + continue; + } + + snd_ctl_card_info_t *alsaInfo; + if (snd_ctl_card_info_malloc(&alsaInfo) != 0) + { + Lv2Log::error("Failed to allocate ALSA card info"); + snd_ctl_close(hDevice); + continue; + } + + err = snd_ctl_card_info(hDevice, alsaInfo); + if (err == 0) + { + AlsaDeviceInfo info; + info.cardId_ = cardNum; + info.id_ = std::string("hw:") + snd_ctl_card_info_get_id(alsaInfo); + const char* driver = snd_ctl_card_info_get_driver(alsaInfo); + + info.name_ = snd_ctl_card_info_get_name(alsaInfo); + info.longName_ = snd_ctl_card_info_get_longname(alsaInfo); + + + snd_pcm_t *hDevice = nullptr; + + // must support capture AND playback + err = snd_pcm_open(&hDevice, cardId.c_str(), SND_PCM_STREAM_CAPTURE, 0); + if (err == 0) { + snd_pcm_close(hDevice); + } + if (err == 0) + { + err = snd_pcm_open(&hDevice, cardId.c_str(), SND_PCM_STREAM_PLAYBACK, 0); + } + if (err == 0) + { + snd_pcm_hw_params_t *params = nullptr; + err = snd_pcm_hw_params_malloc(¶ms); + if (err == 0) + { + err = snd_pcm_hw_params_any(hDevice, params); + if (err == 0) + { + unsigned int minRate = 0, maxRate = 0; + snd_pcm_uframes_t minBufferSize,maxBufferSize; + int dir; + err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir); + if (err == 0) + { + err = snd_pcm_hw_params_get_rate_max(params, &maxRate, &dir); + } + if (err == 0) + { + err = snd_pcm_hw_params_get_buffer_size_min(params,&minBufferSize); + } + if (err == 0) + { + err = snd_pcm_hw_params_get_buffer_size_max(params,&maxBufferSize); + } + if (err == 0) + { + for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i) + { + uint32_t rate = RATES[i]; + if (rate >= minRate && rate <= maxRate) + { + info.sampleRates_.push_back(rate); + } + } + info.minBufferSize_ = (uint32_t)minBufferSize; + info.maxBufferSize_ = (uint32_t)maxBufferSize; + result.push_back(std::move(info)); + } + } + } + if (params != nullptr) + snd_pcm_hw_params_free(params); + snd_pcm_close(hDevice); + } + } + snd_ctl_card_info_free(alsaInfo); + snd_ctl_close(hDevice); + } + } + snd_config_update_free_global(); + return result; +} + + +void PiPedalAlsaDevices::PreLoadJackDevice(const std::string&deviceName) +{ + // save the device info before we start the jack server, because + // we won't be able to do it later. + std::vector devices = GetAvailableAlsaDevices(); + this->hasJackDevice = false; + for (auto &device : devices) + { + if (device.id_ == deviceName) { + this->currentJackDevice = device; + this->hasJackDevice = true; + break; + } + } +} + +std::vector PiPedalAlsaDevices::GetAlsaDevices() +{ + std::vector devices = GetAvailableAlsaDevices(); + if (this->hasJackDevice) + { + bool found = false; + for (auto &device: devices) + { + if (device.id_ == this->currentJackDevice.id_) + { + found = true; + break; + } + } + if (!found) + { + devices.push_back(this->currentJackDevice); + } + } + return devices; +} + + + +JSON_MAP_BEGIN(AlsaDeviceInfo) +JSON_MAP_REFERENCE(AlsaDeviceInfo, cardId) +JSON_MAP_REFERENCE(AlsaDeviceInfo, id) +JSON_MAP_REFERENCE(AlsaDeviceInfo, name) +JSON_MAP_REFERENCE(AlsaDeviceInfo, longName) +JSON_MAP_REFERENCE(AlsaDeviceInfo, sampleRates) +JSON_MAP_REFERENCE(AlsaDeviceInfo, minBufferSize) +JSON_MAP_REFERENCE(AlsaDeviceInfo, maxBufferSize) +JSON_MAP_END() diff --git a/src/PiPedalAlsa.hpp b/src/PiPedalAlsa.hpp new file mode 100644 index 0000000..849435f --- /dev/null +++ b/src/PiPedalAlsa.hpp @@ -0,0 +1,48 @@ +// Copyright (c) 2021 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#pragma once + +#include "json.hpp" + +namespace pipedal { + class AlsaDeviceInfo { + public: + int cardId_ = -1; + std::string id_; + std::string name_; + std::string longName_; + std::vector sampleRates_; + uint32_t minBufferSize_ = 0,maxBufferSize_ = 0; + + DECLARE_JSON_MAP(AlsaDeviceInfo); + + }; + + class PiPedalAlsaDevices { + + bool hasJackDevice = false; + AlsaDeviceInfo currentJackDevice; + std::vector GetAvailableAlsaDevices(); + public: + void PreLoadJackDevice(const std::string&deviceName); + + std::vector GetAlsaDevices(); + }; +} \ No newline at end of file diff --git a/src/PiPedalAlsaTest.cpp b/src/PiPedalAlsaTest.cpp new file mode 100644 index 0000000..435617b --- /dev/null +++ b/src/PiPedalAlsaTest.cpp @@ -0,0 +1,38 @@ +// Copyright (c) 2021 Robin Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +#include "pch.h" +#include "catch.hpp" +#include +#include +#include + +#include "PiPedalAlsa.hpp" + +using namespace pipedal; + +TEST_CASE( "ALSA Test", "[pipedal_alsa_test]" ) { + + PiPedalAlsaDevices devices; + auto result = devices.GetAlsaDevices(); + REQUIRE(result.size() >= 1); +} + + + diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index c94787d..24ee3ae 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -87,6 +87,8 @@ void PiPedalModel::Load(const PiPedalConfiguration &configuration) this->jackServerSettings.ReadJackConfiguration(); + alsaDevices.PreLoadJackDevice(this->jackServerSettings.GetAlsaDevice()); + storage.SetDataRoot(configuration.GetLocalStoragePath().c_str()); storage.Initialize(); @@ -332,7 +334,7 @@ PedalBoard PiPedalModel::getPreset(int64_t instanceId) std::lock_guard guard(mutex); return this->storage.GetPreset(instanceId); } -void PiPedalModel::getBank(int64_t instanceId, BankFile*pResult) +void PiPedalModel::getBank(int64_t instanceId, BankFile *pResult) { std::lock_guard guard(mutex); this->storage.GetBankFile(instanceId, pResult); @@ -855,13 +857,19 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet { std::lock_guard guard(mutex); - this->jackServerSettings = jackServerSettings; - if (!ShutdownClient::CanUseShutdownClient()) { - throw PiPedalException("Can't change server settings when running interactively."); + throw PiPedalException("Can't change server settings when running a debug server."); } + if (this->jackServerSettings.GetAlsaDevice() != jackServerSettings.GetAlsaDevice()) + { + this->alsaDevices.PreLoadJackDevice(jackServerSettings.GetAlsaDevice()); + } + this->jackServerSettings = jackServerSettings; + + + // take a snapshot incase a client unsusbscribes in the notification handler (in which case the mutex won't protect us) IPiPedalModelSubscriber **t = new IPiPedalModelSubscriber *[this->subscribers.size()]; for (size_t i = 0; i < subscribers.size(); ++i) @@ -879,29 +887,38 @@ void PiPedalModel::SetJackServerSettings(const JackServerSettings &jackServerSet { this->jackConfiguration.SetIsRestarting(true); fireJackConfigurationChanged(this->jackConfiguration); - this->jackHost->UpdateServerConfiguration(jackServerSettings, - [this](bool success, const std::string &errorMessage) - { - std::lock_guard guard(mutex); - if (!success) - { - std::stringstream s; - s << "UpdateServerconfiguration failed: " << errorMessage; - Lv2Log::error(s.str().c_str()); - } - // Update jack server status. - this->jackConfiguration.SetIsRestarting(false); - fireJackConfigurationChanged(this->jackConfiguration); + this->jackHost->UpdateServerConfiguration( + jackServerSettings, + [this](bool success, const std::string &errorMessage) + { + std::lock_guard guard(mutex); + if (!success) + { + std::stringstream s; + s << "UpdateServerconfiguration failed: " << errorMessage; + Lv2Log::error(s.str().c_str()); + } + // Update jack server status. + this->jackConfiguration.SetIsRestarting(false); + if (!success) + { + this->jackConfiguration.SetErrorStatus(errorMessage); + } + else + { + this->jackConfiguration.SetErrorStatus(""); + fireJackConfigurationChanged(this->jackConfiguration); - // restart the pedalboard on a new instance. - std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; - this->lv2PedalBoard = lv2PedalBoard; + // restart the pedalboard on a new instance. + std::shared_ptr lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)}; + this->lv2PedalBoard = lv2PedalBoard; - jackHost->SetPedalBoard(lv2PedalBoard); - updateRealtimeVuSubscriptions(); - updateRealtimeMonitorPortSubscriptions(); - }); - } + jackHost->SetPedalBoard(lv2PedalBoard); + updateRealtimeVuSubscriptions(); + updateRealtimeMonitorPortSubscriptions(); + } + }); + } } void PiPedalModel::updateDefaults(PedalBoardItem *pedalBoardItem) @@ -1040,3 +1057,7 @@ void PiPedalModel::cancelListenForMidiEvent(int64_t clientId, int64_t clientHand jackHost->SetListenForMidiEvent(false); } } +std::vector PiPedalModel::GetAlsaDevices() +{ + return this->alsaDevices.GetAlsaDevices(); +} \ No newline at end of file diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index f5da49b..6339b80 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -60,6 +60,8 @@ public: class PiPedalModel: private IJackHostCallbacks { private: + PiPedalAlsaDevices alsaDevices; + class MidiListener { public: @@ -202,6 +204,7 @@ public: void listenForMidiEvent(int64_t clientId, int64_t clientHandle, bool listenForControlsOnly); void cancelListenForMidiEvent(int64_t clientId, int64_t clientHandled); + std::vector GetAlsaDevices(); }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 6386fae..ade926f 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -33,6 +33,7 @@ #include "WifiConfigSettings.hpp" #include "WifiChannels.hpp" #include "SysExec.hpp" +#include "PiPedalAlsa.hpp" using namespace std; using namespace pipedal; @@ -689,6 +690,12 @@ public: { JackHostStatus status = model.getJackStatus(); this->Reply(replyTo,"getJackStatus",status); + } else if (message == "getAlsaDevices") + { + std::vector devices = model.GetAlsaDevices(); + this->Reply(replyTo,"getAlsaDevices",devices); + + } else if (message == "getWifiChannels") { std::string country; diff --git a/src/ShutdownClient.cpp b/src/ShutdownClient.cpp index 95050c0..a51e944 100644 --- a/src/ShutdownClient.cpp +++ b/src/ShutdownClient.cpp @@ -109,7 +109,8 @@ bool ShutdownClient::WriteMessage(const char*message) { while (!eolFound) { ssize_t nRead = read(sock,pWrite,available); - if (nRead == 0) { + if (nRead == -1) + { *pWrite = 0; break; } @@ -156,12 +157,11 @@ bool ShutdownClient::WriteMessage(const char*message) { bool ShutdownClient::SetJackServerConfiguration(const JackServerSettings & jackServerSettings) { std::stringstream s; - s << "setJackConfiguration " - << jackServerSettings.GetSampleRate() - << " " << jackServerSettings.GetBufferSize() - << " " << jackServerSettings.GetNumberOfBuffers() - << "\n"; - + s << "setJackConfiguration "; + + json_writer writer(s,true); + writer.write(jackServerSettings); + s << std::endl; return WriteMessage(s.str().c_str()); } diff --git a/src/ShutdownMain.cpp b/src/ShutdownMain.cpp index f71672c..8c7048e 100644 --- a/src/ShutdownMain.cpp +++ b/src/ShutdownMain.cpp @@ -80,27 +80,12 @@ static void ReleaseAccessPoint(const std::string gatewayAddress) } -bool setJackConfiguration(uint32_t sampleRate,uint32_t bufferSize,uint32_t numberOfBuffers) +bool setJackConfiguration(JackServerSettings serverSettings) { bool success = true; - JackServerSettings serverSettings(sampleRate,bufferSize,numberOfBuffers); - try { - serverSettings.Write(); - } catch (const std::exception &e) { - std::stringstream s; - s << "Failed to write jackdrc settings. " << e.what(); - Lv2Log::error(s.str()); - success = false; - } - ::system("pulseaudio --kill"); - - if (::system("systemctl restart jack") != 0) - { - Lv2Log::error("Failed to restart jack server."); - success = false; - } - return false; + serverSettings.Write(); + return true; } @@ -245,24 +230,28 @@ private: } else if (startsWith(s,"setJackConfiguration ")) { auto remainder = s.substr(strlen("setJackConfiguration ")); + + // xxx delete me + Lv2Log::error("setJackConfiguration: " + remainder); std::stringstream input(remainder); - uint32_t sampleRate; - uint32_t bufferSize; - uint32_t numberOfBuffers; - input >> sampleRate >> bufferSize >> numberOfBuffers; + JackServerSettings serverSettings; + json_reader reader(input); + + reader.read(&serverSettings); + if (input.fail()) { result = -1; } else { - result = setJackConfiguration(sampleRate,bufferSize,numberOfBuffers) ? 0: -1; + result = setJackConfiguration(serverSettings) ? 0: -1; } } } catch (const std::exception &e) { std::stringstream t; - t << "-2 " << e.what(); + t << "-2 " << e.what() << "\n"; this->response = t.str(); this->writePosition = 0; WriteSome(); diff --git a/src/main.cpp b/src/main.cpp index 319d6f8..d294b7c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -418,6 +418,8 @@ uint16_t g_ShutdownPort = 0; int main(int argc, char *argv[]) { + + sem_init(&signalSemaphore, 0, 0); signal(SIGINT, sig_handler); diff --git a/src/template.service b/src/template.service index 811855b..98c8aad 100644 --- a/src/template.service +++ b/src/template.service @@ -5,6 +5,7 @@ After=network.target BindsTo=jack.service + [Service] LimitMEMLOCK=infinity LimitRTPRIO=95 @@ -12,11 +13,11 @@ ExecStart=${COMMAND} User=pipedal_d Group=pipedal_d Restart=always -RestartSec=15 +RestartSec=25 TimeoutStopSec=10 WorkingDirectory=/var/pipedal -Environment=JACK_PROMISCUOUS_SERVER=jack -Environment=JACK_START_SERVER=1 +Environment=JACK_PROMISCUOUS_SERVER= +Environment=JACK_STJACK_NO_START_SERVER= [Install] diff --git a/src/templateJack.service b/src/templateJack.service new file mode 100644 index 0000000..441ea2f --- /dev/null +++ b/src/templateJack.service @@ -0,0 +1,24 @@ +[Unit] +Description=Jack Audio Daemon +After=sound.target +After=local-fs.target +After=dbus.socket + + +[Service] +LimitMEMLOCK=infinity +LimitRTTIME=infinity +LimitRTPRIO=95 +ExecStartPre=/bin/sleep 3 +ExecStart=/etc/jackdrc +User=jack +Group=jack +Restart=always +RestartSec=15 + +Environment=JACK_PROMISCUOUS_SERVER=jack +Environment=JACK_NO_AUDIO_RESERVATION=1 + + +[Install] +WantedBy=multi-user.target \ No newline at end of file