Email Changes - Backwards Compatibility

Introduce legacy alsaDevice_ field in JackServerSettings to preserve compatibility with older configs. Include this field in serialization to allow old config files to load. Add migration logic in Storage::GetJackServerSettings to copy the legacy device value to new separate input/output fields and clear the legacy value after migration.

Update frontend validation so that device selections are only validated when the user presses OK. Improve buffer selection logic to prioritize 32×4, then other x4, x3, or x2 options, falling back to 64×3 if none match.

Use standard MUI styling for device selectors. Labels stay above the field and no longer animate. Dropdown labels now use background padding to prevent underline overlap.

Add a compactWidth flag to adjust layout on narrow screens. Stack device selectors vertically in compact mode while keeping the refresh button on the right. Refreshing ALSA info sets unknown device IDs to a blank value.

Add real-time status updates with a polling timer. Separate Apply and OK handlers so settings can be tested without closing the dialog. Add an Apply button alongside Cancel and OK.

Update the UI summary logic to show “Input → Output” when devices differ and “Not selected” when config is incomplete. Filter out internal devices like HDMI and bcm2835 from the device list.

Retain the legacy alsaDevice_ field in the JSON for backward compatibility, with helper accessors and documentation for legacy constructor behavior. Update latency text calculation to show when sample rate and buffer size are defined, even if the config is invalid.

Removed libsdl2-dev from dependency checking and dependencies.
This commit is contained in:
Extremesecrecy
2025-07-24 19:27:39 -07:00
parent fb974f5af5
commit 874d859916
8 changed files with 300 additions and 183 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ Run the following commands to install dependent libraries required by the PiPeda
authbind libavahi-client-dev libnm-dev libicu-dev \ authbind libavahi-client-dev libnm-dev libicu-dev \
libsdbus-c++-dev libzip-dev google-perftools \ libsdbus-c++-dev libzip-dev google-perftools \
libgoogle-perftools-dev \ libgoogle-perftools-dev \
libpipewire-0.3-dev libwebsocketpp-dev libsdl2-dev libpipewire-0.3-dev libwebsocketpp-dev
After installing these packages, run the dependency checker to verify that After installing these packages, run the dependency checker to verify that
everything required is present: everything required is present:
+1 -1
View File
@@ -8,7 +8,7 @@ packages=(\
authbind libavahi-client-dev libnm-dev libicu-dev \ authbind libavahi-client-dev libnm-dev libicu-dev \
libsdbus-c++-dev libzip-dev google-perftools \ libsdbus-c++-dev libzip-dev google-perftools \
libgoogle-perftools-dev libpipewire-0.3-dev \ libgoogle-perftools-dev libpipewire-0.3-dev \
libwebsocketpp-dev libsdl2-dev libwebsocketpp-dev
) )
missing=() missing=()
+8 -1
View File
@@ -80,10 +80,17 @@ namespace pipedal
bool IsValid() const { return valid_; } bool IsValid() const { return valid_; }
// JackServerSettings(uint64_t sampleRate, uint32_t bufferSize, uint32_t numberOfBuffers) // Legacy constructor used by tests prior to the addition of
// separate input and output ALSA devices.
// JackServerSettings(const std::string &device,
// uint64_t sampleRate,
// uint32_t bufferSize,
// uint32_t numberOfBuffers)
// { // {
// this->valid_ = true; // this->valid_ = true;
// this->rebootRequired_ = true; // this->rebootRequired_ = true;
// this->alsaInputDevice_ = device;
// this->alsaOutputDevice_ = device;
// this->sampleRate_ = sampleRate; // this->sampleRate_ = sampleRate;
// this->bufferSize_ = bufferSize; // this->bufferSize_ = bufferSize;
// this->numberOfBuffers_ = numberOfBuffers; // this->numberOfBuffers_ = numberOfBuffers;
+1 -1
View File
@@ -95,7 +95,7 @@ void PrintHelp()
pp << Indent(0) << "Examples\n\n"; pp << Indent(0) << "Examples\n\n";
pp << Indent(2) << "pipedal_latency_test --list\n\n"; pp << Indent(2) << "pipedal_latency_test --list\n\n";
pp << Indent(2) << "pipedal_latency_test hw:M2\n"; pp << Indent(2) << "pipedal_latency_test hw:M2 hw:M2\n";
pp << Indent(2) << "pipedal_latency_test hw:M2 hw:Device2\n\n"; pp << Indent(2) << "pipedal_latency_test hw:M2 hw:Device2\n\n";
} }
+15 -2
View File
@@ -22,6 +22,7 @@
#include "alsa/asoundlib.h" #include "alsa/asoundlib.h"
#include "Lv2Log.hpp" #include "Lv2Log.hpp"
#include <mutex> #include <mutex>
#include <algorithm>
using namespace pipedal; using namespace pipedal;
@@ -191,12 +192,24 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
} }
snd_config_update_free_global(); snd_config_update_free_global();
auto isFiltered = [](const AlsaDeviceInfo &d) {
std::string name = d.name_ + " " + d.longName_;
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c){return std::tolower(c);});
return name.find("hdmi") != std::string::npos || name.find("bcm2835") != std::string::npos;
};
std::vector<AlsaDeviceInfo> filtered;
for (auto &d : result)
{
if (!isFiltered(d)) filtered.push_back(d);
}
Lv2Log::debug("GetAlsaDevices --"); Lv2Log::debug("GetAlsaDevices --");
for (auto &device : result) for (auto &device : filtered)
{ {
Lv2Log::debug(SS(" " << device.name_ << " " << device.longName_ << " " << device.cardId_)); Lv2Log::debug(SS(" " << device.name_ << " " << device.longName_ << " " << device.cardId_));
} }
return result; return filtered;
} }
static void AddMidiCardDevicesToList(snd_ctl_t *ctl, int card, int device, AlsaMidiDeviceInfo::Direction direction, std::vector<AlsaMidiDeviceInfo> *result) static void AddMidiCardDevicesToList(snd_ctl_t *ctl, int card, int device, AlsaMidiDeviceInfo::Direction direction, std::vector<AlsaMidiDeviceInfo> *result)
+8 -8
View File
@@ -1573,14 +1573,6 @@ std::map<std::string, bool> Storage::GetFavorites() const
{ {
json_reader reader(f); json_reader reader(f);
reader.read(&result); reader.read(&result);
if (!result.GetLegacyAlsaDevice().empty() &&
result.GetAlsaInputDevice().empty() &&
result.GetAlsaOutputDevice().empty())
{
result.SetAlsaInputDevice(result.GetLegacyAlsaDevice());
result.SetAlsaOutputDevice(result.GetLegacyAlsaDevice());
result.SetLegacyAlsaDevice("");
}
} }
return result; return result;
} }
@@ -1606,6 +1598,14 @@ pipedal::JackServerSettings Storage::GetJackServerSettings()
{ {
json_reader reader(f); json_reader reader(f);
reader.read(&result); reader.read(&result);
if (!result.GetLegacyAlsaDevice().empty() &&
result.GetAlsaInputDevice().empty() &&
result.GetAlsaOutputDevice().empty())
{
result.SetAlsaInputDevice(result.GetLegacyAlsaDevice());
result.SetAlsaOutputDevice(result.GetLegacyAlsaDevice());
result.SetLegacyAlsaDevice("");
}
} }
#if JACK_HOST #if JACK_HOST
result.Initialize(); result.Initialize();
+14 -5
View File
@@ -56,13 +56,22 @@ export default class JackServerSettings {
numberOfBuffers = 3; numberOfBuffers = 3;
getSummaryText() { getSummaryText() {
if (this.valid) { if (!this.valid || !this.alsaInputDevice || !this.alsaOutputDevice) {
let inDev = this.alsaInputDevice.startsWith("hw:") ? this.alsaInputDevice .substring(3) : this.alsaInputDevice; return "Not selected";
let outDev = this.alsaOutputDevice.startsWith("hw:") ? this.alsaOutputDevice.substring(3) : this.alsaOutputDevice; }
return "In: "+inDev+" Out: "+outDev+" — Rate "+this.sampleRate+", "+this.bufferSize+"×"+this.numberOfBuffers;
let inDev = this.alsaInputDevice.startsWith("hw:")
? this.alsaInputDevice.substring(3)
: this.alsaInputDevice;
let outDev = this.alsaOutputDevice.startsWith("hw:")
? this.alsaOutputDevice.substring(3)
: this.alsaOutputDevice;
if (inDev === outDev) {
return inDev;
} else { } else {
return "Not configured"; return inDev+" → "+outDev;
} }
} }
+250 -162
View File
@@ -22,6 +22,7 @@
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import React from 'react';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
import {createStyles} from './WithStyles'; import {createStyles} from './WithStyles';
@@ -44,10 +45,18 @@ import IconButtonEx from './IconButtonEx';
import RefreshIcon from '@mui/icons-material/Refresh'; import RefreshIcon from '@mui/icons-material/Refresh';
import Checkbox from '@mui/material/Checkbox'; import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel'; import FormControlLabel from '@mui/material/FormControlLabel';
import JackHostStatus from './JackHostStatus';
import AlsaDeviceInfo from './AlsaDeviceInfo'; import AlsaDeviceInfo from './AlsaDeviceInfo';
import ResizeResponsiveComponent from './ResizeResponsiveComponent'; import ResizeResponsiveComponent from './ResizeResponsiveComponent';
function filterDevices(devices: AlsaDeviceInfo[]): AlsaDeviceInfo[] {
return devices.filter(d => {
const name = (d.name + ' ' + d.longName).toLowerCase();
return !(name.includes('hdmi') || name.includes('bcm2835'));
});
}
function sortDevices(devices: AlsaDeviceInfo[]): AlsaDeviceInfo[] { function sortDevices(devices: AlsaDeviceInfo[]): AlsaDeviceInfo[] {
function category(d: AlsaDeviceInfo): number { function category(d: AlsaDeviceInfo): number {
if (d.supportsCapture && d.supportsPlayback) return 0; if (d.supportsCapture && d.supportsPlayback) return 0;
@@ -73,14 +82,18 @@ interface BufferSetting {
numberOfBuffers: number; numberOfBuffers: number;
}; };
const INVALID_DEVICE_ID = "_invalid_"; // empty string used when no valid device is selected - the default.
const INVALID_DEVICE_ID = "";
interface JackServerSettingsDialogState { interface JackServerSettingsDialogState {
latencyText: string; latencyText: string;
jackServerSettings: JackServerSettings; jackServerSettings: JackServerSettings;
alsaDevices?: AlsaDeviceInfo[]; alsaDevices?: AlsaDeviceInfo[];
okEnabled: boolean; okEnabled: boolean;
useSameDevice: boolean;
fullScreen: boolean; fullScreen: boolean;
compactWidth: boolean;
jackStatus?: JackHostStatus;
showDeviceWarning: boolean;
dontShowWarningAgain: boolean;
} }
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@@ -92,6 +105,15 @@ const styles = (theme: Theme) =>
selectEmpty: { selectEmpty: {
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
}, },
inputLabel: {
backgroundColor: theme.palette.background.paper,
paddingLeft: 4,
paddingRight: 4
},
cpuStatusColor: {
color: theme.palette.text.secondary,
opacity: 0.7
}
}); });
export interface JackServerSettingsDialogProps extends WithStyles<typeof styles> { export interface JackServerSettingsDialogProps extends WithStyles<typeof styles> {
open: boolean; open: boolean;
@@ -101,13 +123,15 @@ export interface JackServerSettingsDialogProps extends WithStyles<typeof styles>
} }
function getLatencyText(settings?: JackServerSettings ): string { function getLatencyText(settings?: JackServerSettings ): string {
if (!settings) if (!settings) {
{
return "\u00A0"; return "\u00A0";
} }
if (!settings.valid) return "\u00A0"; if (!settings.sampleRate || !settings.bufferSize || !settings.numberOfBuffers) {
let ms = settings.bufferSize * settings.numberOfBuffers / settings.sampleRate * 1000; return "\u00A0";
}
let ms = (settings.bufferSize * settings.numberOfBuffers) / settings.sampleRate * 1000;
return ms.toFixed(1) + "ms"; return ms.toFixed(1) + "ms";
} }
@@ -213,33 +237,57 @@ function getBestBuffers(
return {bufferSize: bufferSize, numberOfBuffers:numberOfBuffers}; return {bufferSize: bufferSize, numberOfBuffers:numberOfBuffers};
} }
} }
// otherwise select a sensible starting value. // otherwise select a sensible starting value.
// favored default: 64x3. // Build candidate sizes with 32 first if present.
if (64*3 >= minSize && 64*3 <= maxSize) let validBufferSizes = getValidBufferSizesMultiple(inDevice, outDevice);
{ validBufferSizes.sort((a,b)=>a-b);
return { bufferSize: 64, numberOfBuffers: 3}; if (validBufferSizes.indexOf(32) !== -1) {
validBufferSizes = [32, ...validBufferSizes.filter(v => v !== 32)];
} }
// if that isn't possible then minBufferSize/2 x 4.
bufferSize = minSize/2; function tryPick(count: number): BufferSetting | undefined {
for (let bs of validBufferSizes) {
const counts = getValidBufferCountsMultiple(bs, inDevice, outDevice);
if (counts.indexOf(count) !== -1) {
if (bs * count >= minSize && bs * count <= maxSize) {
return { bufferSize: bs, numberOfBuffers: count };
}
}
}
return undefined;
}
let result = tryPick(4);
if (!result) result = tryPick(3);
if (!result) result = tryPick(2);
if (result) return result;
// Fallback to previous behaviour.
if (64*3 >= minSize && 64*3 <= maxSize) {
return { bufferSize: 64, numberOfBuffers: 3 };
}
bufferSize = minSize/2;
numberOfBuffers = 4; numberOfBuffers = 4;
// otherwise, minBufferSize/2 x 2. if (bufferSize * numberOfBuffers > maxSize) {
if (bufferSize*numberOfBuffers > maxSize)
{
numberOfBuffers = 2; numberOfBuffers = 2;
} }
return { bufferSize: bufferSize, numberOfBuffers: numberOfBuffers}; return { bufferSize: bufferSize, numberOfBuffers: numberOfBuffers };
}; };
function isOkEnabled(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[]) function isOkEnabled(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[])
{ {
if (!jackServerSettings.valid) return false;
if (!alsaDevices) return false; if (!alsaDevices) return false;
const inDevice = alsaDevices.find(d => d.id === jackServerSettings.alsaInputDevice && d.supportsCapture); if (!jackServerSettings.alsaInputDevice || !jackServerSettings.alsaOutputDevice) return false;
const inDevice = alsaDevices.find(d => d.id === jackServerSettings.alsaInputDevice && d.supportsCapture);
const outDevice = alsaDevices.find(d => d.id === jackServerSettings.alsaOutputDevice && d.supportsPlayback); const outDevice = alsaDevices.find(d => d.id === jackServerSettings.alsaOutputDevice && d.supportsPlayback);
if (!inDevice || !outDevice) return false; if (!inDevice || !outDevice) return false;
const sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates);
if (sampleRates.indexOf(jackServerSettings.sampleRate) === -1) return false;
const deviceBufferSize = jackServerSettings.bufferSize * jackServerSettings.numberOfBuffers; const deviceBufferSize = jackServerSettings.bufferSize * jackServerSettings.numberOfBuffers;
const minSize = Math.max(inDevice.minBufferSize, outDevice.minBufferSize); const minSize = Math.max(inDevice.minBufferSize, outDevice.minBufferSize);
const maxSize = Math.min(inDevice.maxBufferSize, outDevice.maxBufferSize); const maxSize = Math.min(inDevice.maxBufferSize, outDevice.maxBufferSize);
@@ -248,9 +296,6 @@ function isOkEnabled(jackServerSettings: JackServerSettings, alsaDevices?: AlsaD
const validBufferCounts = getValidBufferCountsMultiple(jackServerSettings.bufferSize, inDevice, outDevice); const validBufferCounts = getValidBufferCountsMultiple(jackServerSettings.bufferSize, inDevice, outDevice);
if (validBufferCounts.indexOf(jackServerSettings.numberOfBuffers) === -1) return false; if (validBufferCounts.indexOf(jackServerSettings.numberOfBuffers) === -1) return false;
const sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates);
if (sampleRates.indexOf(jackServerSettings.sampleRate) === -1) return false;
return true; return true;
} }
@@ -265,17 +310,23 @@ const JackServerSettingsDialog = withStyles(
this.model = PiPedalModelFactory.getInstance(); this.model = PiPedalModelFactory.getInstance();
const sameDevice = props.jackServerSettings.alsaInputDevice === props.jackServerSettings.alsaOutputDevice; this.suppressDeviceWarning = localStorage.getItem("suppressSeparateDeviceWarning") === "1";
this.state = { this.state = {
latencyText: getLatencyText(props.jackServerSettings), latencyText: getLatencyText(props.jackServerSettings),
jackServerSettings: props.jackServerSettings.clone(), // invalid, but not nullish jackServerSettings: props.jackServerSettings.clone(), // invalid, but not nullish
alsaDevices: undefined, alsaDevices: undefined,
okEnabled: false, okEnabled: false,
useSameDevice: sameDevice, fullScreen: this.getFullScreen(),
fullScreen: this.getFullScreen() compactWidth: document.documentElement.clientWidth < 600,
}; jackStatus: undefined,
showDeviceWarning: false,
dontShowWarningAgain: false
};
} }
mounted: boolean = false; mounted: boolean = false;
statusTimer?: number = undefined;
suppressDeviceWarning: boolean;
getFullScreen() { getFullScreen() {
return document.documentElement.clientWidth < 420 || return document.documentElement.clientWidth < 420 ||
@@ -285,8 +336,9 @@ const JackServerSettingsDialog = withStyles(
onWindowSizeChanged(width: number, height: number): void { onWindowSizeChanged(width: number, height: number): void {
super.onWindowSizeChanged(width, height); super.onWindowSizeChanged(width, height);
const fullScreen = this.getFullScreen(); const fullScreen = this.getFullScreen();
if (fullScreen !== this.state.fullScreen) { const compactWidth = width < 600;
this.setState({ fullScreen }); if (fullScreen !== this.state.fullScreen || compactWidth !== this.state.compactWidth) {
this.setState({ fullScreen, compactWidth });
} }
} }
@@ -295,15 +347,16 @@ const JackServerSettingsDialog = withStyles(
.then((devices) => { .then((devices) => {
if (this.mounted) { if (this.mounted) {
if (this.props.open) { if (this.props.open) {
let settings = this.applyAlsaDevices(this.state.jackServerSettings, devices); let f = filterDevices(devices);
let settings = this.applyAlsaDevices(this.state.jackServerSettings, f);
this.setState({ this.setState({
alsaDevices: devices, alsaDevices: f,
jackServerSettings: settings, jackServerSettings: settings,
latencyText: getLatencyText(settings), latencyText: getLatencyText(settings),
okEnabled: isOkEnabled(settings,devices) okEnabled: isOkEnabled(settings, f)
}); });
} else { } else {
this.setState({ alsaDevices: devices }); this.setState({ alsaDevices: filterDevices(devices) });
} }
} }
}) })
@@ -312,7 +365,28 @@ const JackServerSettingsDialog = withStyles(
}); });
} }
applyAlsaDevices(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[], useSameDevice?: boolean) { tickStatus() {
this.model.getJackStatus()
.then(status => {
if (this.mounted) {
this.setState({ jackStatus: status });
}
})
.catch(() => { });
}
startStatusTimer() {
if (this.statusTimer) return;
this.tickStatus();
this.statusTimer = window.setInterval(() => this.tickStatus(), 1000);
}
stopStatusTimer() {
if (this.statusTimer) {
clearInterval(this.statusTimer);
this.statusTimer = undefined;
}
}
applyAlsaDevices(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[]) {
let result = jackServerSettings.clone(); let result = jackServerSettings.clone();
if (!alsaDevices || alsaDevices.length === 0) { if (!alsaDevices || alsaDevices.length === 0) {
result.valid = false; result.valid = false;
@@ -321,44 +395,30 @@ const JackServerSettingsDialog = withStyles(
return result; return result;
} }
let inDevice = alsaDevices.find(d => d.id === result.alsaInputDevice && d.supportsCapture); let devices = filterDevices(alsaDevices);
let outDevice = alsaDevices.find(d => d.id === result.alsaOutputDevice && d.supportsPlayback); let inDevice = devices.find(d => d.id === result.alsaInputDevice && d.supportsCapture);
let outDevice = devices.find(d => d.id === result.alsaOutputDevice && d.supportsPlayback);
const sameDevice = useSameDevice !== undefined ? useSameDevice : this.state.useSameDevice; if (!inDevice || !outDevice) {
if (sameDevice) { const capture = devices.filter(d => d.supportsCapture);
if (!inDevice || !inDevice.supportsPlayback) { const playback = devices.filter(d => d.supportsPlayback);
const both = alsaDevices.find(d => d.supportsCapture && d.supportsPlayback); if (capture.length === 1 && playback.length === 1) {
if (both) { inDevice = capture[0];
result.alsaInputDevice = both.id; outDevice = playback[0];
result.alsaOutputDevice = both.id; result.alsaInputDevice = inDevice.id;
inDevice = both; result.alsaOutputDevice = outDevice.id;
outDevice = both; } else if (capture.length === 1 && capture[0].supportsPlayback && playback.length === 0) {
} else { inDevice = capture[0];
result.valid = false; outDevice = capture[0];
result.alsaInputDevice = INVALID_DEVICE_ID; result.alsaInputDevice = inDevice.id;
result.alsaOutputDevice = INVALID_DEVICE_ID; result.alsaOutputDevice = outDevice.id;
return result;
}
} else {
result.alsaOutputDevice = result.alsaInputDevice;
outDevice = inDevice;
}
} else {
if (!inDevice) {
result.valid = false;
result.alsaInputDevice = INVALID_DEVICE_ID;
}
if (!outDevice) {
result.valid = false;
result.alsaOutputDevice = INVALID_DEVICE_ID;
}
if (!inDevice || !outDevice) {
return result;
} }
} }
if (!inDevice || !outDevice) { if (!inDevice || !outDevice) {
result.valid = false; result.valid = false;
if (!inDevice) result.alsaInputDevice = INVALID_DEVICE_ID;
if (!outDevice) result.alsaOutputDevice = INVALID_DEVICE_ID;
return result; return result;
} }
@@ -376,8 +436,6 @@ const JackServerSettingsDialog = withStyles(
let bestBuffers = getBestBuffers(inDevice, outDevice, result.bufferSize, result.numberOfBuffers); let bestBuffers = getBestBuffers(inDevice, outDevice, result.bufferSize, result.numberOfBuffers);
result.bufferSize = bestBuffers.bufferSize; result.bufferSize = bestBuffers.bufferSize;
result.numberOfBuffers = bestBuffers.numberOfBuffers; result.numberOfBuffers = bestBuffers.numberOfBuffers;
result.valid = !!result.alsaInputDevice && !!result.alsaOutputDevice;
return result; return result;
} }
@@ -386,27 +444,31 @@ const JackServerSettingsDialog = withStyles(
this.mounted = true; this.mounted = true;
if (this.props.open) { if (this.props.open) {
this.requestAlsaInfo(); this.requestAlsaInfo();
this.startStatusTimer();
} }
} }
componentDidUpdate(oldProps: JackServerSettingsDialogProps) { componentDidUpdate(oldProps: JackServerSettingsDialogProps) {
if ((this.props.open && !oldProps.open) && this.mounted) { if ((this.props.open && !oldProps.open) && this.mounted) {
let settings = this.applyAlsaDevices(this.props.jackServerSettings.clone(), this.state.alsaDevices); let settings = this.applyAlsaDevices(this.props.jackServerSettings.clone(), this.state.alsaDevices);
this.setState({ this.setState({
jackServerSettings: settings, jackServerSettings: settings,
latencyText: getLatencyText(settings), latencyText: getLatencyText(settings),
okEnabled: isOkEnabled(settings,this.state.alsaDevices), okEnabled: isOkEnabled(settings,this.state.alsaDevices)
useSameDevice: settings.alsaInputDevice === settings.alsaOutputDevice
}); });
if (!this.state.alsaDevices) { if (!this.state.alsaDevices) {
this.requestAlsaInfo(); this.requestAlsaInfo();
} }
this.startStatusTimer();
} else if (!this.props.open && oldProps.open) {
this.stopStatusTimer();
} }
} }
componentWillUnmount() { componentWillUnmount() {
super.componentWillUnmount(); super.componentWillUnmount();
this.mounted = false; this.mounted = false;
this.stopStatusTimer();
} }
getSelectedDevice(deviceId: string): AlsaDeviceInfo | undefined { getSelectedDevice(deviceId: string): AlsaDeviceInfo | undefined {
@@ -427,6 +489,7 @@ const JackServerSettingsDialog = withStyles(
if (!inDev && !outDev) return; if (!inDev && !outDev) return;
let settings = this.state.jackServerSettings.clone(); let settings = this.state.jackServerSettings.clone();
settings.sampleRate = rate; settings.sampleRate = rate;
settings.valid = false;
this.setState({ this.setState({
jackServerSettings: settings, jackServerSettings: settings,
@@ -442,7 +505,8 @@ const JackServerSettingsDialog = withStyles(
if (!inDev && !outDev) return; if (!inDev && !outDev) return;
let settings = this.state.jackServerSettings.clone(); let settings = this.state.jackServerSettings.clone();
settings.bufferSize = size; settings.bufferSize = size;
let bestBufferSetting = getBestBuffers(inDev,outDev,settings.bufferSize,settings.numberOfBuffers,true); settings.valid = false;
let bestBufferSetting = getBestBuffers(inDev,outDev,settings.bufferSize,settings.numberOfBuffers,true);
settings.bufferSize = bestBufferSetting.bufferSize; settings.bufferSize = bestBufferSetting.bufferSize;
settings.numberOfBuffers = bestBufferSetting.numberOfBuffers; settings.numberOfBuffers = bestBufferSetting.numberOfBuffers;
@@ -460,6 +524,7 @@ const JackServerSettingsDialog = withStyles(
if (!inDev && !outDev) return; if (!inDev && !outDev) return;
let settings = this.state.jackServerSettings.clone(); let settings = this.state.jackServerSettings.clone();
settings.numberOfBuffers = bufferCount; settings.numberOfBuffers = bufferCount;
settings.valid = false;
this.setState({ this.setState({
jackServerSettings: settings, jackServerSettings: settings,
@@ -471,16 +536,46 @@ const JackServerSettingsDialog = withStyles(
handleApply() { handleApply() {
if (this.state.okEnabled) if (this.state.okEnabled)
{ {
this.props.onApply(this.state.jackServerSettings.clone()); let s = this.state.jackServerSettings.clone();
s.valid = false;
this.model.setJackServerSettings(s);
} }
}; };
handleOk() {
if (!this.state.okEnabled) return;
if (this.state.jackServerSettings.alsaInputDevice !== this.state.jackServerSettings.alsaOutputDevice
&& !this.suppressDeviceWarning) {
this.setState({ showDeviceWarning: true });
return;
}
let s = this.state.jackServerSettings.clone();
s.valid = true;
this.props.onApply(s);
};
handleWarningProceed() {
if (this.state.dontShowWarningAgain) {
localStorage.setItem("suppressSeparateDeviceWarning", "1");
this.suppressDeviceWarning = true;
}
this.setState({ showDeviceWarning: false, dontShowWarningAgain: false }, () => {
let s = this.state.jackServerSettings.clone();
s.valid = true;
this.props.onApply(s);
});
}
handleWarningCancel() {
this.setState({ showDeviceWarning: false, dontShowWarningAgain: false });
}
handleWarningCheck(e: any, checked: boolean) {
this.setState({ dontShowWarningAgain: checked });
}
handleInputDeviceChanged(e: any) { handleInputDeviceChanged(e: any) {
const d = e.target.value as string; const d = e.target.value as string;
let s = this.state.jackServerSettings.clone(); let s = this.state.jackServerSettings.clone();
s.alsaInputDevice = d; s.alsaInputDevice = d;
if (this.state.useSameDevice) { s.valid = false;
s.alsaOutputDevice = d;
}
let settings = this.applyAlsaDevices(s, this.state.alsaDevices); let settings = this.applyAlsaDevices(s, this.state.alsaDevices);
this.setState({ this.setState({
jackServerSettings: settings, jackServerSettings: settings,
@@ -493,9 +588,7 @@ const JackServerSettingsDialog = withStyles(
const d = e.target.value as string; const d = e.target.value as string;
let s = this.state.jackServerSettings.clone(); let s = this.state.jackServerSettings.clone();
s.alsaOutputDevice = d; s.alsaOutputDevice = d;
if (this.state.useSameDevice) { s.valid = false;
s.alsaInputDevice = d;
}
let settings = this.applyAlsaDevices(s, this.state.alsaDevices); let settings = this.applyAlsaDevices(s, this.state.alsaDevices);
this.setState({ this.setState({
jackServerSettings: settings, jackServerSettings: settings,
@@ -503,28 +596,8 @@ const JackServerSettingsDialog = withStyles(
okEnabled: isOkEnabled(settings, this.state.alsaDevices) okEnabled: isOkEnabled(settings, this.state.alsaDevices)
}); });
} }
handleUseSameDeviceChanged(e: any, checked: boolean) {
let s = this.state.jackServerSettings.clone();
const useSame = checked;
if (!useSame) { render() {
// clear selections when switching to separate devices
s.alsaInputDevice = INVALID_DEVICE_ID;
s.alsaOutputDevice = INVALID_DEVICE_ID;
s.valid = false;
}
let settings = this.applyAlsaDevices(s, this.state.alsaDevices, useSame);
this.setState({
useSameDevice: useSame,
jackServerSettings: settings,
latencyText: getLatencyText(settings),
okEnabled: isOkEnabled(settings, this.state.alsaDevices)
}, () => {
this.requestAlsaInfo();
});
}
render() {
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
const { onClose, open } = this.props; const { onClose, open } = this.props;
@@ -537,13 +610,8 @@ const JackServerSettingsDialog = withStyles(
let selectedInputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaInputDevice); let selectedInputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaInputDevice);
let selectedOutputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaOutputDevice); let selectedOutputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaOutputDevice);
if (this.state.useSameDevice) {
selectedOutputDevice = selectedInputDevice;
}
const devicesSelected = this.state.useSameDevice ? const devicesSelected = (selectedInputDevice && selectedOutputDevice);
!!selectedInputDevice :
(selectedInputDevice && selectedOutputDevice);
let bufferSizes: number[] = devicesSelected ? let bufferSizes: number[] = devicesSelected ?
getValidBufferSizesMultiple(selectedInputDevice, selectedOutputDevice) : []; getValidBufferSizesMultiple(selectedInputDevice, selectedOutputDevice) : [];
@@ -553,67 +621,61 @@ const JackServerSettingsDialog = withStyles(
let bufferCountDisabled = !devicesSelected; let bufferCountDisabled = !devicesSelected;
let sampleRates = devicesSelected && selectedInputDevice && selectedOutputDevice ? let sampleRates = devicesSelected && selectedInputDevice && selectedOutputDevice ?
intersectArrays(selectedInputDevice.sampleRates, selectedOutputDevice.sampleRates) intersectArrays(selectedInputDevice.sampleRates, selectedOutputDevice.sampleRates)
: (this.state.useSameDevice && selectedInputDevice ? selectedInputDevice.sampleRates : []); : [];
let sampleRateOptions = sampleRates.length === 0 && this.state.jackServerSettings.sampleRate ? [this.state.jackServerSettings.sampleRate] : sampleRates; let sampleRateOptions = sampleRates.length === 0 && this.state.jackServerSettings.sampleRate ? [this.state.jackServerSettings.sampleRate] : sampleRates;
return ( return (
<>
<DialogEx tag="jack" onClose={handleClose} aria-labelledby="select-channels-title" open={open} <DialogEx tag="jack" onClose={handleClose} aria-labelledby="select-channels-title" open={open}
onEnterKey={() => { onEnterKey={() => {
this.handleApply(); this.handleOk();
}} }}
fullScreen={this.state.fullScreen} fullScreen={this.state.fullScreen}
> >
<DialogContent> <DialogContent>
<div style={{ display: "flex", alignItems: "center", flexWrap: "wrap", gap: 8 }}> <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div style={{ display: "flex", flexDirection: "row", alignItems: "center", gap: 8 }}>
<div style={{ display: "flex", flexDirection: this.state.compactWidth ? "column" : "row", gap: 8 }}>
{/* Audio Input Device */} {/* Audio Input Device */}
<FormControl className={classes.formControl}> <FormControl variant="standard" className={classes.formControl}>
<InputLabel htmlFor="jsd_inputDevice">{this.state.useSameDevice ? "Device" : "Input Device"}</InputLabel> <InputLabel shrink className={classes.inputLabel} htmlFor="jsd_inputDevice">Input Device</InputLabel>
<Select <Select variant="standard"
id="jsd_inputDevice" id="jsd_inputDevice"
value={this.state.jackServerSettings.alsaInputDevice} value={this.state.jackServerSettings.alsaInputDevice}
onChange={e => this.handleInputDeviceChanged(e)} onChange={e => this.handleInputDeviceChanged(e)}
disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0} disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0}
style={{ width: 220 }} style={{ width: 220 }}
> >
{sortedDevices.filter(d => d.supportsCapture && (!this.state.useSameDevice || d.supportsPlayback)).map(d => ( {sortedDevices.filter(d => d.supportsCapture).map(d => (
<MenuItem key={d.id} value={d.id}>{d.name}</MenuItem> <MenuItem key={d.id} value={d.id}>{d.name}</MenuItem>
)) || <MenuItem value="" disabled>Loading...</MenuItem>} )) || <MenuItem value="" disabled>Loading...</MenuItem>}
</Select> </Select>
</FormControl> </FormControl>
{/* Audio Output Device */} {/* Audio Output Device */}
<FormControl className={classes.formControl} <FormControl variant="standard" className={classes.formControl}>
style={{ display: this.state.useSameDevice ? 'none' : undefined }}> <InputLabel shrink className={classes.inputLabel} htmlFor="jsd_outputDevice">Output Device</InputLabel>
<InputLabel htmlFor="jsd_outputDevice">Output Device</InputLabel> <Select variant="standard"
<Select id="jsd_outputDevice"
id="jsd_outputDevice" value={this.state.jackServerSettings.alsaOutputDevice}
value={this.state.jackServerSettings.alsaOutputDevice} onChange={e => this.handleOutputDeviceChanged(e)}
onChange={e => this.handleOutputDeviceChanged(e)} disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0}
disabled={this.state.useSameDevice || !this.state.alsaDevices || this.state.alsaDevices.length === 0} style={{ width: 220 }}
style={{ width: 220 }} >
>
{sortedDevices.filter(d => d.supportsPlayback).map(d => ( {sortedDevices.filter(d => d.supportsPlayback).map(d => (
<MenuItem key={d.id} value={d.id}>{d.name}</MenuItem> <MenuItem key={d.id} value={d.id}>{d.name}</MenuItem>
)) || <MenuItem value="" disabled>Loading...</MenuItem>} )) || <MenuItem value="" disabled>Loading...</MenuItem>}
</Select> </Select>
</FormControl> </FormControl>
<IconButtonEx tooltip="Refresh devices" onClick={() => this.requestAlsaInfo()} aria-label="refresh-audio-devices"> </div>
<RefreshIcon /> <IconButtonEx tooltip="Refresh devices" onClick={() => this.requestAlsaInfo()} aria-label="refresh-audio-devices">
</IconButtonEx> <RefreshIcon />
<FormControlLabel </IconButtonEx>
control={ </div>
<Checkbox
checked={this.state.useSameDevice}
onChange={(e, c) => this.handleUseSameDeviceChanged(e, c)}
/>
}
label="Use same device for input/output"
style={{ marginLeft: 16 }}
/>
</div><div> </div><div>
<FormControl className={classes.formControl}> <FormControl variant="standard" className={classes.formControl}>
<InputLabel htmlFor="jsd_sampleRate">Sample rate</InputLabel> <InputLabel shrink className={classes.inputLabel} htmlFor="jsd_sampleRate">Sample rate</InputLabel>
<Select variant="standard" <Select variant="standard"
onChange={(e) => this.handleRateChanged(e)} onChange={(e) => this.handleRateChanged(e)}
value={this.state.jackServerSettings.sampleRate} value={this.state.jackServerSettings.sampleRate}
@@ -631,8 +693,8 @@ const JackServerSettingsDialog = withStyles(
</Select> </Select>
</FormControl> </FormControl>
<div style={{ display: "inline", whiteSpace: "nowrap" }}> <div style={{ display: "inline", whiteSpace: "nowrap" }}>
<FormControl className={classes.formControl}> <FormControl variant="standard" className={classes.formControl}>
<InputLabel htmlFor="bufferSize">Buffer size</InputLabel> <InputLabel shrink className={classes.inputLabel} htmlFor="bufferSize">Buffer size</InputLabel>
<Select variant="standard" <Select variant="standard"
onChange={(e) => this.handleSizeChanged(e)} onChange={(e) => this.handleSizeChanged(e)}
value={this.state.jackServerSettings.bufferSize} value={this.state.jackServerSettings.bufferSize}
@@ -650,8 +712,8 @@ const JackServerSettingsDialog = withStyles(
)} )}
</Select> </Select>
</FormControl> </FormControl>
<FormControl className={classes.formControl}> <FormControl variant="standard" className={classes.formControl}>
<InputLabel htmlFor="numberofBuffers">Buffers</InputLabel> <InputLabel shrink className={classes.inputLabel} htmlFor="numberofBuffers">Buffers</InputLabel>
<Select variant="standard" <Select variant="standard"
onChange={(e) => this.handleNumberOfBuffersChanged(e)} onChange={(e) => this.handleNumberOfBuffersChanged(e)}
value={this.state.jackServerSettings.numberOfBuffers} value={this.state.jackServerSettings.numberOfBuffers}
@@ -678,7 +740,10 @@ const JackServerSettingsDialog = withStyles(
color="textSecondary"> color="textSecondary">
Latency: {this.state.latencyText} Latency: {this.state.latencyText}
</Typography> </Typography>
{!this.state.okEnabled && ( <div className={classes.cpuStatusColor} style={{ paddingLeft: 24 }}>
{JackHostStatus.getDisplayView("", this.state.jackStatus)}
</div>
{!this.state.okEnabled && (
<Typography display="block" variant="caption" color="error" <Typography display="block" variant="caption" color="error"
style={{ textAlign: "left", marginLeft: 24 }}> style={{ textAlign: "left", marginLeft: 24 }}>
Please select all audio settings. Please select all audio settings.
@@ -687,16 +752,39 @@ const JackServerSettingsDialog = withStyles(
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button variant="dialogSecondary" onClick={handleClose} > <Button variant="dialogSecondary" onClick={handleClose}>
Cancel Cancel
</Button> </Button>
<Button variant="dialogPrimary" onClick={() => this.handleApply()} <Button variant="dialogSecondary" onClick={() => this.handleApply()} disabled={!this.state.okEnabled}>Apply</Button>
disabled={!this.state.okEnabled}> <Button variant="dialogPrimary" onClick={() => this.handleOk()} disabled={!this.state.okEnabled}>
OK OK
</Button> </Button>
</DialogActions> </DialogActions>
</DialogEx> </DialogEx>
{this.state.showDeviceWarning && (
<DialogEx open={this.state.showDeviceWarning} tag="ioWarning"
onEnterKey={() => this.handleWarningProceed()}
onClose={() => this.handleWarningCancel()}
style={{ userSelect: "none" }}
>
<DialogContent>
<Typography variant="body2" color="textPrimary" gutterBottom>
Using different input and output devices may cause issues when streaming audio.
Are you sure you want to continue?
</Typography>
<FormControlLabel
control={<Checkbox checked={this.state.dontShowWarningAgain} onChange={(e,c)=>this.handleWarningCheck(e,c)} />}
label="Don't show me this message again"
/>
</DialogContent>
<DialogActions>
<Button onClick={() => this.handleWarningCancel()} variant="dialogSecondary" style={{ width: 120 }}>Cancel</Button>
<Button onClick={() => this.handleWarningProceed()} variant="dialogPrimary" style={{ width: 120 }}>PROCEED</Button>
</DialogActions>
</DialogEx>
)}
</>
); );
} }
}, },