Jack configuration (alpha)

This commit is contained in:
Robin Davies
2021-08-29 14:04:59 -04:00
parent 6e1b7346f6
commit 94c7d406f7
32 changed files with 1061 additions and 293 deletions
+1
View File
@@ -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
+1 -1
View File
@@ -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. */
+1 -2
View File
@@ -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. */
+1 -1
View File
@@ -1 +1 @@
cmake --install /home/patch/src/pipedal/build --prefix /usr/local --config Release
cmake --install build --prefix /usr/local --config Release -v
+1
View File
@@ -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
+1 -1
View File
@@ -1,5 +1,5 @@
{
"socket_server_port": 80,
"socket_server_port": 8080,
"socket_server_address": "*",
"max_upload_size": 1048576,
+66
View File
@@ -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;
};
+15 -9
View File
@@ -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";
}
}
+260 -72
View File
@@ -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<typeof styles>
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<JackServerSettingsDialogProps, JackServerSettingsDialogState> {
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<any>(document.getElementById('jsd_sampleRate')).value);
let bufferSize: number = parseInt(nullCast<any>(document.getElementById('jsd_bufferSize')).value);
let bufferCount: number = parseInt(nullCast<any>(document.getElementById('jsd_bufferCount')).value);
this.setState({
latencyText: getLatencyText(sampleRate, bufferSize, bufferCount)
});
},0);
}
handleApply() {
let sampleRate = parseInt(nullCast<any>(document.getElementById('jsd_sampleRate')).value);
let bufferSize = parseInt(nullCast<any>(document.getElementById('jsd_bufferSize')).value);
let bufferCount = parseInt(nullCast<any>(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 (
<Dialog onClose={handleClose} aria-labelledby="select-channels-title" open={open}>
<DialogContent>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="sampleRate">Sample rate</InputLabel>
<Select
onChange={(e) => this.updateLatency(e)}
defaultValue={jackServerSettings.sampleRate}
inputProps={{
name: 'Sample Rate',
id: 'jsd_sampleRate',
style: {
textAlign: "right"
<div>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="jsd_device">Device</InputLabel>
<Select onChange={(e) => this.handleDeviceChanged(e)}
value={this.state.jackServerSettings.alsaDevice}
style={{width: 220}}
inputProps={{
name: "Device",
id: "jsd_device",
}}
>
{(noDevices && !waitingForDevices) &&
(
<MenuItem value={INVALID_DEVICE_ID}>No suitable devices.</MenuItem>
)
}
}}
>
<MenuItem value={44100}>44100</MenuItem>
<MenuItem value={48000}>48000</MenuItem>
<MenuItem value={88200}>88200</MenuItem>
<MenuItem value={96000}>96000</MenuItem>
<MenuItem value={176400}>176400</MenuItem>
<MenuItem value={192000}>192000</MenuItem>
</Select>
</FormControl>
<div style={{ display: "inline", whiteSpace: "nowrap" }}>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="bufferSize">Buffer size</InputLabel>
<Select
onChange={(e) => this.updateLatency(e)}
defaultValue={jackServerSettings.bufferSize}
inputProps={{
name: 'Buffer size',
id: 'jsd_bufferSize',
}}
>
<MenuItem value={16}>16</MenuItem>
<MenuItem value={32}>32</MenuItem>
<MenuItem value={64}>64</MenuItem>
<MenuItem value={128}>128</MenuItem>
<MenuItem value={256}>256</MenuItem>
<MenuItem value={512}>512</MenuItem>
<MenuItem value={1024}>1024</MenuItem>
{((!noDevices) && !waitingForDevices) && (
this.state.alsaDevices!.map((device) =>
(
<MenuItem value={device.id}>{device.name}</MenuItem>
)
)
)}
</Select>
</FormControl>
</div><div>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="numberofBuffers">Buffers</InputLabel>
<InputLabel htmlFor="jsd_sampleRate">Sample rate</InputLabel>
<Select
onChange={(e) => this.updateLatency(e)}
defaultValue={jackServerSettings.numberOfBuffers}
onChange={(e) => this.handleRateChanged(e)}
value={this.state.jackServerSettings.sampleRate}
inputProps={{
name: 'Number of buffers',
id: 'jsd_bufferCount',
id: 'jsd_sampleRate',
style: {
textAlign: "right"
}
}}
>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
<MenuItem value={4}>4</MenuItem>
{selectedDevice &&
selectedDevice.sampleRates.map((sr) => {
return ( <MenuItem value={sr}>{sr}</MenuItem> );
})
}
</Select>
</FormControl>
<div style={{ display: "inline", whiteSpace: "nowrap" }}>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="bufferSize">Buffer size</InputLabel>
<Select
onChange={(e) => this.handleSizeChanged(e)}
value={this.state.jackServerSettings.bufferSize}
inputProps={{
name: 'Buffer size',
id: 'jsd_bufferSize',
}}
>
{bufferSizes.map((buffSize) =>
(
<MenuItem value={buffSize}>{buffSize}</MenuItem>
)
)}
</Select>
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="numberofBuffers">Buffers</InputLabel>
<Select
onChange={(e) => this.handleNumberOfBuffersChanged(e)}
value={this.state.jackServerSettings.numberOfBuffers}
inputProps={{
name: 'Number of buffers',
id: 'jsd_bufferCount',
}}
>
<MenuItem value={2}>2</MenuItem>
<MenuItem value={3}>3</MenuItem>
<MenuItem value={4}>4</MenuItem>
</Select>
</FormControl>
</div>
</div>
<Typography display="block" variant="caption" style={{ textAlign: "left", marginTop: 8, marginLeft: 24 }}
color="textSecondary">
@@ -175,7 +362,8 @@ const JackServerSettingsDialog = withStyles(styles)(
<Button onClick={handleClose} color="primary">
Cancel
</Button>
<Button onClick={()=> this.handleApply()} color="secondary">
<Button onClick={() => this.handleApply()} color="secondary" disabled={
(!this.state.alsaDevices) || !this.state.jackServerSettings.valid}>
OK
</Button>
</DialogActions>
+19
View File
@@ -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<WifiChannel[]>;
getAlsaDevices() : Promise<AlsaDeviceInfo[]>;
};
class PiPedalModelImpl implements PiPedalModel {
@@ -1586,6 +1588,23 @@ class PiPedalModelImpl implements PiPedalModel {
return result;
}
getAlsaDevices() : Promise<AlsaDeviceInfo[]>
{
let result = new Promise<AlsaDeviceInfo[]>((resolve, reject) => {
if (!this.webSocket) {
reject("Connection closed.");
return;
}
this.webSocket.request<any>("getAlsaDevices")
.then((data) => {
resolve(AlsaDeviceInfo.deserialize_array(data));
})
.catch((err) => reject(err));
});
return result;
}
};
+12 -27
View File
@@ -400,36 +400,19 @@ const SettingsDialog = withStyles(styles, { withTheme: true })(
overflowX: "hidden", overflowY: "auto", marginTop: 22
}}
>
{!isConfigValid && (
<div>
<Typography className={classes.sectionHead} display="block" variant="caption" color="error">
Error
</Typography>
<Typography className={classes.textBlock} display="block" variant="body2" >
The server encounter the following problem:
</Typography>
<Typography className={classes.textBlockIndented} display="block" variant="body2" >
{this.state.jackConfiguration.errorState}
</Typography>
<Typography className={classes.textBlock} display="block" variant="body2">
Please get the Jack Audio Server running properly on the server before attempting to configure audio.
</Typography>
<Divider style={{ marginTop: 16, marginBottom: 16 }} />
</div>
)}
<div style={{ opacity: isConfigValid ? 1.0 : 0.6 }}>
<div >
<Typography className={classes.sectionHead} display="block" variant="caption" color="secondary">
AUDIO
</Typography>
<div style={{ paddingLeft: 48, position: "relative", top: -12 }}>
{JackHostStatus.getDisplayView("Status:\u00A0", this.state.jackStatus)}
{ (!isConfigValid) ?
(
<Typography variant="caption" color="textSecondary">Status: <span style={{color: "#F00"}}>Not configured.</span></Typography>
):
JackHostStatus.getDisplayView("Status:\u00A0", this.state.jackStatus)
}
</div>
<ButtonBase className={classes.setting} onClick={() => this.handleJackServerSettings()}
disabled={!(isConfigValid && this.state.jackServerSettings.valid)}
style={{ opacity: !(isConfigValid && this.state.jackServerSettings.valid) ? 0.6 : 1.0 }}
>
<SelectHoverBackground selected={false} showHover={true} />
<div style={{ width: "100%" }}>
@@ -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);
}}
/>
+4 -2
View File
@@ -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
)
+128 -26
View File
@@ -18,6 +18,8 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include <iostream>
#include <stdlib.h>
#include <unistd.h>
#include "CommandLineParser.hpp"
#include "PiPedalException.hpp"
#include <filesystem>
@@ -27,27 +29,29 @@
#include "SetWifiConfig.hpp"
#include "SysExec.hpp"
#include <sys/wait.h>
#include <pwd.h>
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<std::string,std::string> 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<char*> args;
std::vector<char *> args;
std::string pkexec = "/usr/bin/pkexec"; // staged because "ISO C++ forbids converting a string constant to std::vector<char*>::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)
{
+1
View File
@@ -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<std::string> &GetInputAudioPorts() const { return inputAudioPorts_; }
const std::vector<std::string> &GetOutputAudioPorts() const { return outputAudioPorts_; }
+15 -6
View File
@@ -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_)
{
+1 -1
View File
@@ -27,6 +27,7 @@
#include "json.hpp"
#include "JackServerSettings.hpp"
#include <functional>
#include "PiPedalAlsa.hpp"
namespace pipedal {
@@ -118,7 +119,6 @@ public:
class IHost;
class JackHost {
protected:
JackHost() { }
public:
+110 -79
View File
@@ -23,6 +23,9 @@
#include "JackServerSettings.hpp"
#include <fstream>
#include "PiPedalException.hpp"
#include <string>
#include "unistd.h"
#include <filesystem>
#define DRC_FILENAME "/etc/jackdrc"
@@ -55,24 +58,46 @@ static std::vector<std::string> SplitArgs(const char *szBuff)
return result;
}
static uint64_t GetJackArg(const std::vector<std::string> &args, const char *shortOption, const char *longOption)
static std::string GetJackStringArg(const std::vector<std::string> &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<std::string> &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<std::string> &args, const char *sho
throw PiPedalException("Can't read Jack configuration.");
}
}
static void SetJackArg(std::vector<std::string> &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<std::string> 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<std::string> precedingLines;
std::vector < std::string> argv;
std::vector<std::string> 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" <<endl;
output << "export JACK_PROMISCUOUS_SERVER=" << endl;
output << "export JACK_NO_AUDIO_RESERVATION=" << endl;
output << "umask 0" << endl;
}
for (auto line : precedingLines)
{
output << line << endl;
}
for (size_t i = 0; i < argv.size(); ++i)
// the style used by qjackctl. :-/
output << "/usr/bin/jackd "
<< "-R -P90"
<< " -driver alsa -d" << this->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()
+2
View File
@@ -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();
+20 -2
View File
@@ -22,6 +22,7 @@
#include "Locale.hpp"
#include <stdlib.h>
#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<char>& Locale::collation = std::use_facet<std::collate<char> >(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<char>& Locale::collation() { return std::use_facet<std::collate<char> >(std::locale()); }
+3 -1
View File
@@ -25,6 +25,8 @@ namespace pipedal {
class Locale {
public:
static const std::collate<char>& collation;
static void setDefaultLocale();
static const std::collate<char>& collation();
};
}
+7 -4
View File
@@ -394,13 +394,14 @@ void Lv2Host::Load(const char *lv2Path)
}
}
auto compare = [](
const auto &collation = std::use_facet<std::collate<char> >(std::locale());
auto compare = [&collation](
const std::shared_ptr<Lv2PluginInfo> &left,
const std::shared_ptr<Lv2PluginInfo> &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<Lv2PluginPreset> Lv2Host::GetPluginPresets(const std::string &plugin
}
lilv_nodes_free(presets);
auto compare = [] (const Lv2PluginPreset&left, const Lv2PluginPreset&right)
auto& collation = std::use_facet<std::collate<char> >(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(),
+200
View File
@@ -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 <mutex>
using namespace pipedal;
static uint32_t RATES[] = {
44100, 48000, 44100 * 2, 48000 * 2, 44100 * 4, 48000 * 4};
std::mutex alsaMutex;
std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAvailableAlsaDevices()
{
std::lock_guard guard{alsaMutex};
std::vector<AlsaDeviceInfo> 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(&params);
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<AlsaDeviceInfo> devices = GetAvailableAlsaDevices();
this->hasJackDevice = false;
for (auto &device : devices)
{
if (device.id_ == deviceName) {
this->currentJackDevice = device;
this->hasJackDevice = true;
break;
}
}
}
std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
{
std::vector<AlsaDeviceInfo> 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()
+48
View File
@@ -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<uint32_t> sampleRates_;
uint32_t minBufferSize_ = 0,maxBufferSize_ = 0;
DECLARE_JSON_MAP(AlsaDeviceInfo);
};
class PiPedalAlsaDevices {
bool hasJackDevice = false;
AlsaDeviceInfo currentJackDevice;
std::vector<AlsaDeviceInfo> GetAvailableAlsaDevices();
public:
void PreLoadJackDevice(const std::string&deviceName);
std::vector<AlsaDeviceInfo> GetAlsaDevices();
};
}
+38
View File
@@ -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 <sstream>
#include <cstdint>
#include <string>
#include "PiPedalAlsa.hpp"
using namespace pipedal;
TEST_CASE( "ALSA Test", "[pipedal_alsa_test]" ) {
PiPedalAlsaDevices devices;
auto result = devices.GetAlsaDevices();
REQUIRE(result.size() >= 1);
}
+46 -25
View File
@@ -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> lv2PedalBoard{this->lv2Host.CreateLv2PedalBoard(this->pedalBoard)};
this->lv2PedalBoard = lv2PedalBoard;
// restart the pedalboard on a new instance.
std::shared_ptr<Lv2PedalBoard> 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<AlsaDeviceInfo> PiPedalModel::GetAlsaDevices()
{
return this->alsaDevices.GetAlsaDevices();
}
+3
View File
@@ -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<AlsaDeviceInfo> GetAlsaDevices();
};
} // namespace pipedal.
+7
View File
@@ -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<AlsaDeviceInfo> devices = model.GetAlsaDevices();
this->Reply(replyTo,"getAlsaDevices",devices);
} else if (message == "getWifiChannels")
{
std::string country;
+7 -7
View File
@@ -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());
}
+13 -24
View File
@@ -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();
+2
View File
@@ -418,6 +418,8 @@ uint16_t g_ShutdownPort = 0;
int main(int argc, char *argv[])
{
sem_init(&signalSemaphore, 0, 0);
signal(SIGINT, sig_handler);
+4 -3
View File
@@ -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]
+24
View File
@@ -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