Merge pull request #360 from extremesecrecy/SplitIO to rerdavies:splitio_base branch.

Thank you again. This functionality will make a lot of people happy.
This commit is contained in:
Robin Davies
2025-07-30 02:45:35 -04:00
committed by GitHub
31 changed files with 624 additions and 229 deletions
+1
View File
@@ -1,3 +1,4 @@
#!/bin/bash
wpa_cli -ip2p-dev-wlan0 GET device_name pipedal wpa_cli -ip2p-dev-wlan0 GET device_name pipedal
wpa_cli -ip2p-dev-wlan0 GET country CA wpa_cli -ip2p-dev-wlan0 GET country CA
wpa_cli -ip2p-dev-wlan0 GET device_type 1-0050F204-1 wpa_cli -ip2p-dev-wlan0 GET device_type 1-0050F204-1
+3 -2
View File
@@ -57,17 +57,18 @@ https://github.com/user-attachments/assets/9a9fd0c6-78fc-4284-8b44-6a1929c00cc6
### [Command-Line Configuration of PiPedal](https://rerdavies.github.io/pipedal/CommandLine.html) ### [Command-Line Configuration of PiPedal](https://rerdavies.github.io/pipedal/CommandLine.html)
### [Changing the Web Server Port](https://rerdavies.github.io/pipedal/ChangingTheWebServerPort.html) ### [Changing the Web Server Port](https://rerdavies.github.io/pipedal/ChangingTheWebServerPort.html)
%nbsp;  
### [Using LV2 Audio Plugins](https://rerdavies.github.io/pipedal/UsingLv2Plugins.md) ### [Using LV2 Audio Plugins](https://rerdavies.github.io/pipedal/UsingLv2Plugins.md)
### [Which LV2 Plugins does PiPedal support?](https://rerdavies.github.io/pipedal/WhichLv2PluginsAreSupported.html) ### [Which LV2 Plugins does PiPedal support?](https://rerdavies.github.io/pipedal/WhichLv2PluginsAreSupported.html)
### [Support for LV2 Plugins with MOD User Interfaces](https://rerdavies.github.io/pipedal/ModUiSupport.html) ### [Support for LV2 Plugins with MOD User Interfaces](https://rerdavies.github.io/pipedal/ModUiSupport.html)
    
### [Building PiPedal from Source](https://rerdavies.github.io/pipedal/BuildingPiPedalFromSource.html) ### [Building PiPedal from Source](https://rerdavies.github.io/pipedal/BuildingPiPedalFromSource.html)
### [Build Prerequisites](https://rerdavies.github.io/pipedal/BuildPrerequisites.html) ### [Build Prerequisites](https://rerdavies.github.io/pipedal/BuildPrerequisites.html)
### [The Build System](https://rerdavies.github.io/pipedal/TheBuildSystem.html) ### [The Build System](https://rerdavies.github.io/pipedal/TheBuildSystem.html)
### [How to Debug PiPedal](https://rerdavies.github.io/pipedal/Debugging.html) ### [How to Debug PiPedal](https://rerdavies.github.io/pipedal/Debugging.html)
   
+1 -1
View File
@@ -6,4 +6,4 @@
# bundle install # bundle install
# #
cd docs cd docs
bundle exec jekyll serve --host bundle exec jekyll serve --host 0.0.0.0
+1 -1
View File
@@ -21,6 +21,7 @@ Run the following commands to install dependent libraries required by the PiPeda
sudo apt update sudo apt update
sudo apt upgrade sudo apt upgrade
sudo apt install -y liblilv-dev libboost-dev \ sudo apt install -y liblilv-dev libboost-dev \
libsystemd-dev catch libasound2-dev uuid-dev \ libsystemd-dev catch libasound2-dev uuid-dev \
authbind libavahi-client-dev libnm-dev libicu-dev \ authbind libavahi-client-dev libnm-dev libicu-dev \
@@ -28,7 +29,6 @@ Run the following commands to install dependent libraries required by the PiPeda
libgoogle-perftools-dev \ libgoogle-perftools-dev \
libpipewire-0.3-dev libpipewire-0.3-dev
### Installing Sources ### Installing Sources
If you are using Visual Studio Code, install the following Extensions: If you are using Visual Studio Code, install the following Extensions:
+1
View File
@@ -22,6 +22,7 @@ the following shell scripts have been provided in the root of the project.
sudo ./makePackage.sh # Build a .deb file for distribution. sudo ./makePackage.sh # Build a .deb file for distribution.
If you are using a development environment other than Visual Studio Code, it should be fairly straightforward to figure out how If you are using a development environment other than Visual Studio Code, it should be fairly straightforward to figure out how
to incorporate the PiPedal build procedure into your IDE workflow by using the contents of the build shell scripts as a model. to incorporate the PiPedal build procedure into your IDE workflow by using the contents of the build shell scripts as a model.
+1
View File
@@ -1,3 +1,4 @@
#!/bin/bash
cd build cd build
cpack -G DEB -C Release -config CPackConfig.cmake cpack -G DEB -C Release -config CPackConfig.cmake
cd .. cd ..
+2
View File
@@ -1,3 +1,5 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
${SCRIPT_DIR}/../build/src/profilePlugin -s 100 -w ToobNam_Profile -o /tmp/ToobNam.perf &&\ ${SCRIPT_DIR}/../build/src/profilePlugin -s 100 -w ToobNam_Profile -o /tmp/ToobNam.perf &&\
google-pprof --text ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/ToobAmp.lv2/ToobAmp.so /tmp/ToobNam.perf >./ToobNam.txt google-pprof --text ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/ToobAmp.lv2/ToobAmp.so /tmp/ToobNam.perf >./ToobNam.txt
+1
View File
@@ -1,3 +1,4 @@
#!/bin/bash
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
${SCRIPT_DIR}/../build/src/profilePlugin -w Nam_Profile -o /tmp/nam.perf &&\ ${SCRIPT_DIR}/../build/src/profilePlugin -w Nam_Profile -o /tmp/nam.perf &&\
google-pprof --web ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/neural_amp_modeler.lv2/neural_amp_modeler.so /tmp/nam.perf >./nam.txt google-pprof --web ${SCRIPT_DIR}/../build/src/profilePlugin --add_lib /usr/lib/lv2/neural_amp_modeler.lv2/neural_amp_modeler.so /tmp/nam.perf >./nam.txt
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash
rm -rf ./lv2/aarch64/* rm -rf ./lv2/aarch64/*
cp -R /usr/lib/lv2/ToobAmp.lv2/ ./lv2/aarch64/ cp -R /usr/lib/lv2/ToobAmp.lv2/ ./lv2/aarch64/
+5 -1
View File
@@ -11,4 +11,8 @@
# to NOT do auto-updating (probably best). The build doesn't currently have a procedure for # to NOT do auto-updating (probably best). The build doesn't currently have a procedure for
# knocking out Auto-Update from UpdaterSecurity.hpp. But I would happily accept a pull request # knocking out Auto-Update from UpdaterSecurity.hpp. But I would happily accept a pull request
# to bring such a feature back into mainline if you do it. # to bring such a feature back into mainline if you do it.
build/src/makeRelease
# Build the release package and create a detached GPG signature. This
# command assumes you have already run makePackage.sh so the .deb file
# exists in the build directory.
./build/src/makeRelease
+4
View File
@@ -618,6 +618,7 @@ namespace pipedal
if (this->captureHandle) if (this->captureHandle)
{ {
this->alsa_device_name = this->jackServerSettings.GetAlsaInputDevice();
AlsaConfigureStream( AlsaConfigureStream(
this->alsa_device_name, this->alsa_device_name,
"capture", "capture",
@@ -629,6 +630,7 @@ namespace pipedal
} }
if (this->playbackHandle) if (this->playbackHandle)
{ {
this->alsa_device_name = this->jackServerSettings.GetAlsaOutputDevice();
AlsaConfigureStream( AlsaConfigureStream(
this->alsa_device_name, this->alsa_device_name,
"playback", "playback",
@@ -1234,6 +1236,7 @@ namespace pipedal
try try
{ {
this->alsa_device_name = outputName;
err = snd_pcm_open(&playbackHandle, outputName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); err = snd_pcm_open(&playbackHandle, outputName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
if (err < 0) if (err < 0)
{ {
@@ -1269,6 +1272,7 @@ namespace pipedal
snd_pcm_nonblock(playbackHandle, 0); snd_pcm_nonblock(playbackHandle, 0);
} }
this->alsa_device_name = inputName;
err = snd_pcm_open(&captureHandle, inputName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); err = snd_pcm_open(&captureHandle, inputName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
if (err < 0) if (err < 0)
+1 -1
View File
@@ -64,7 +64,7 @@ public:
AlsaFormatEncodeDecodeTest(this); AlsaFormatEncodeDecodeTest(this);
JackServerSettings serverSettings("hw:M2",48000,32,3); JackServerSettings serverSettings("hw:M2","hw:M2",48000,32,3);
JackConfiguration jackConfiguration; JackConfiguration jackConfiguration;
if (useJack) if (useJack)
+7 -2
View File
@@ -143,9 +143,14 @@ namespace pipedal
SampleT totalTime = writeTime + readTime + processingTime; SampleT totalTime = writeTime + readTime + processingTime;
SampleT maxTime = waitTime + processingTime; SampleT maxTime = waitTime + processingTime;
float result = 0.0f;
float overhead = 0.0f;
if (maxTime != 0 && totalTime != 0)
{
result = 100.0f * (processingTime) / (maxTime);
overhead = 100.0f * (overheadTime * 2) / totalTime;
}
float result = 100.0f*(processingTime)/(maxTime);
float overhead = 100.0F*(overheadTime*2)/totalTime;
{ {
std::lock_guard lock{sync}; std::lock_guard lock{sync};
currentCpuUse = result; currentCpuUse = result;
+1
View File
@@ -254,6 +254,7 @@ JSON_MAP_REFERENCE(JackServerSettings, valid)
JSON_MAP_REFERENCE(JackServerSettings, isOnboarding) JSON_MAP_REFERENCE(JackServerSettings, isOnboarding)
JSON_MAP_REFERENCE(JackServerSettings, rebootRequired) JSON_MAP_REFERENCE(JackServerSettings, rebootRequired)
JSON_MAP_REFERENCE(JackServerSettings, isJackAudio) JSON_MAP_REFERENCE(JackServerSettings, isJackAudio)
JSON_MAP_REFERENCE(JackServerSettings, alsaDevice) // legacy field
JSON_MAP_REFERENCE(JackServerSettings, alsaInputDevice) JSON_MAP_REFERENCE(JackServerSettings, alsaInputDevice)
JSON_MAP_REFERENCE(JackServerSettings, alsaOutputDevice) JSON_MAP_REFERENCE(JackServerSettings, alsaOutputDevice)
JSON_MAP_REFERENCE(JackServerSettings, sampleRate) JSON_MAP_REFERENCE(JackServerSettings, sampleRate)
+14 -12
View File
@@ -33,6 +33,7 @@ namespace pipedal
bool rebootRequired_ = false; bool rebootRequired_ = false;
std::string alsaInputDevice_; std::string alsaInputDevice_;
std::string alsaOutputDevice_; std::string alsaOutputDevice_;
std::string alsaDevice_; // legacy
uint64_t sampleRate_ = 0; uint64_t sampleRate_ = 0;
uint32_t bufferSize_ = 64; uint32_t bufferSize_ = 64;
uint32_t numberOfBuffers_ = 3; uint32_t numberOfBuffers_ = 3;
@@ -42,13 +43,14 @@ namespace pipedal
void SetAlsaOutputDevice(const std::string &d){ alsaOutputDevice_ = d; } void SetAlsaOutputDevice(const std::string &d){ alsaOutputDevice_ = d; }
JackServerSettings(); JackServerSettings();
JackServerSettings( JackServerSettings(
const std::string alsaInputDevice, const std::string &alsaInputDevice,
const std::string &alsaOutputDevice,
uint64_t sampleRate, uint64_t sampleRate,
uint32_t bufferSize, uint32_t bufferSize,
uint32_t numberOfBuffers) uint32_t numberOfBuffers)
: valid_(true), : valid_(true),
alsaInputDevice_(alsaInputDevice), alsaInputDevice_(alsaInputDevice),
alsaOutputDevice_(alsaInputDevice), // default same alsaOutputDevice_(alsaOutputDevice),
sampleRate_(sampleRate), sampleRate_(sampleRate),
bufferSize_(bufferSize), bufferSize_(bufferSize),
numberOfBuffers_(numberOfBuffers), numberOfBuffers_(numberOfBuffers),
@@ -57,10 +59,13 @@ namespace pipedal
} }
uint64_t GetSampleRate() const { return sampleRate_; } uint64_t GetSampleRate() const { return sampleRate_; }
uint32_t GetBufferSize() const { return bufferSize_; } uint32_t GetBufferSize() const { return bufferSize_; }
uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; } uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; }
const std::string &GetAlsaInputDevice() const { return alsaInputDevice_; } const std::string &GetAlsaInputDevice() const { return alsaInputDevice_; }
const std::string &GetAlsaOutputDevice() const { return alsaOutputDevice_; } const std::string &GetAlsaOutputDevice() const { return alsaOutputDevice_; }
const std::string &GetLegacyAlsaDevice() const { return alsaDevice_; } //legacy
void SetLegacyAlsaDevice(const std::string &d) { alsaDevice_ = d; }
void UseDummyAudioDevice() { void UseDummyAudioDevice() {
this->valid_ = true; this->valid_ = true;
if (sampleRate_ == 0) sampleRate_ = 48000; if (sampleRate_ == 0) sampleRate_ = 48000;
@@ -76,14 +81,6 @@ namespace pipedal
bool IsValid() const { return valid_; } bool IsValid() const { return valid_; }
// JackServerSettings(uint64_t sampleRate, uint32_t bufferSize, uint32_t numberOfBuffers)
// {
// this->valid_ = true;
// this->rebootRequired_ = true;
// this->sampleRate_ = sampleRate;
// this->bufferSize_ = bufferSize;
// this->numberOfBuffers_ = numberOfBuffers;
// }
void WriteDaemonConfig(); // requires root perms. void WriteDaemonConfig(); // requires root perms.
void SetRebootRequired(bool value) void SetRebootRequired(bool value)
{ {
@@ -94,9 +91,14 @@ namespace pipedal
isOnboarding_ = value; isOnboarding_ = value;
} }
bool Equals(const JackServerSettings &other) bool Equals(const JackServerSettings &other) const
{ {
return this->alsaInputDevice_ == other.alsaInputDevice_ && this->alsaOutputDevice_ == other.alsaOutputDevice_ && this->sampleRate_ == other.sampleRate_ && this->bufferSize_ == other.bufferSize_ && this->numberOfBuffers_ == other.numberOfBuffers_; return this->alsaInputDevice_ == other.alsaInputDevice_ &&
this->alsaOutputDevice_ == other.alsaOutputDevice_ &&
this->alsaDevice_ == other.alsaDevice_ &&
this->sampleRate_ == other.sampleRate_ &&
this->bufferSize_ == other.bufferSize_ &&
this->numberOfBuffers_ == other.numberOfBuffers_;
} }
DECLARE_JSON_MAP(JackServerSettings); DECLARE_JSON_MAP(JackServerSettings);
+25 -16
View File
@@ -57,9 +57,9 @@ void PrintHelp()
pp << "Copyright (c) 2022 Robin Davies\n"; pp << "Copyright (c) 2022 Robin Davies\n";
pp << "\n"; pp << "\n";
pp << Indent(0) << "Syntax\n\n"; pp << Indent(0) << "Syntax\n\n";
pp << Indent(2) << "pipedal_latency_test [<options>] <device-name>\n\n"; pp << Indent(2) << "pipedal_latency_test [<options>] <input-device> [<output-device>]\n\n";
pp << "where <device-name> is the name of an ALSA device. Typically this should be the name of a hardware " pp << "where <input-device> is the name of an ALSA capture device and <output-device> is the name of a playback device. "
"device (a device name starting with 'hw:').\n\n"; "If <output-device> is omitted, the input device will be used for both capture and playback. Typically the device names start with 'hw:'.\n\n";
pp << Indent(0) << "Options\n\n"; pp << Indent(0) << "Options\n\n";
pp << Indent(15); pp << Indent(15);
@@ -95,7 +95,8 @@ 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\n"; pp << Indent(2) << "pipedal_latency_test hw:M2 hw:M2\n";
pp << Indent(2) << "pipedal_latency_test hw:M2 hw:Device2\n\n";
} }
void ListDevices() void ListDevices()
@@ -138,7 +139,8 @@ public:
private: private:
AudioDriver *audioDriver = nullptr; AudioDriver *audioDriver = nullptr;
const std::string &deviceId; const std::string &inputDeviceId;
const std::string &outputDeviceId;
ChannelsT inputChannels; ChannelsT inputChannels;
ChannelsT outputChannels; ChannelsT outputChannels;
uint32_t sampleRate; uint32_t sampleRate;
@@ -147,11 +149,13 @@ private:
public: public:
AlsaTester( AlsaTester(
const std::string &deviceId, const std::string &inputDeviceId,
const std::string &outputDeviceId,
const ChannelsT &inputChannels, const ChannelsT &inputChannels,
const ChannelsT &outputChannels, const ChannelsT &outputChannels,
uint32_t sampleRate, int bufferSize, int buffers) uint32_t sampleRate, int bufferSize, int buffers)
: deviceId(deviceId), : inputDeviceId(inputDeviceId),
outputDeviceId(outputDeviceId),
sampleRate(sampleRate), sampleRate(sampleRate),
inputChannels(inputChannels), inputChannels(inputChannels),
outputChannels(outputChannels), outputChannels(outputChannels),
@@ -186,7 +190,7 @@ public:
TestResult result; TestResult result;
try try
{ {
JackServerSettings serverSettings(deviceId, sampleRate, bufferSize, buffers); JackServerSettings serverSettings(inputDeviceId, outputDeviceId, sampleRate, bufferSize, buffers);
JackConfiguration jackConfiguration; JackConfiguration jackConfiguration;
jackConfiguration.AlsaInitialize(serverSettings); jackConfiguration.AlsaInitialize(serverSettings);
@@ -403,12 +407,13 @@ public:
}; };
TestResult RunLatencyTest( TestResult RunLatencyTest(
const std::string deviceId, const std::string inputDeviceId,
const std::string outputDeviceId,
const ChannelsT &inputChannels, const ChannelsT &inputChannels,
const ChannelsT &outputChannels, const ChannelsT &outputChannels,
uint32_t sampleRate, int bufferSize, int buffers) uint32_t sampleRate, int bufferSize, int buffers)
{ {
AlsaTester tester(deviceId, inputChannels, outputChannels, sampleRate, bufferSize, buffers); AlsaTester tester(inputDeviceId, outputDeviceId, inputChannels, outputChannels, sampleRate, bufferSize, buffers);
return tester.Test(); return tester.Test();
} }
@@ -428,13 +433,14 @@ static std::string overheadDisplay(float value)
} }
void RunLatencyTest( void RunLatencyTest(
const std::string &deviceId, const std::string &inputDeviceId,
const std::string &outputDeviceId,
const ChannelsT &inputChannels, const ChannelsT &inputChannels,
const ChannelsT &outputChannels, const ChannelsT &outputChannels,
uint32_t sampleRate) uint32_t sampleRate)
{ {
PrettyPrinter pp; PrettyPrinter pp;
pp << "Device: " << deviceId << " Rate: " << sampleRate << "\n\n"; pp << "Input: " << inputDeviceId << " Output: " << outputDeviceId << " Rate: " << sampleRate << "\n\n";
const int SIZE_COLUMN_WIDTH = 8; const int SIZE_COLUMN_WIDTH = 8;
const int BUFFERS_COLUMN_WIDTH = 20; const int BUFFERS_COLUMN_WIDTH = 20;
@@ -461,7 +467,7 @@ void RunLatencyTest(
for (auto bufferCount : bufferCounts) for (auto bufferCount : bufferCounts)
{ {
auto result = RunLatencyTest(deviceId, inputChannels,outputChannels, sampleRate, bufferSize, bufferCount); auto result = RunLatencyTest(inputDeviceId, outputDeviceId, inputChannels,outputChannels, sampleRate, bufferSize, bufferCount);
pp.Column(column); pp.Column(column);
column += BUFFERS_COLUMN_WIDTH; column += BUFFERS_COLUMN_WIDTH;
@@ -556,11 +562,14 @@ std:
{ {
ListDevices(); ListDevices();
} }
else if (parser.Arguments().size() == 1) else if (parser.Arguments().size() >= 1 && parser.Arguments().size() <= 2)
{ {
inputChannels = ParseChannels(strInputChannels); inputChannels = ParseChannels(strInputChannels);
outputChannels = ParseChannels(strInputChannels); outputChannels = ParseChannels(strOutputChannels);
RunLatencyTest(parser.Arguments()[0], inputChannels, outputChannels, sampleRate);
std::string inDev = parser.Arguments()[0];
std::string outDev = parser.Arguments().size() == 2 ? parser.Arguments()[1] : inDev;
RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate);
} }
else else
{ {
+37 -11
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;
@@ -117,19 +118,11 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
unsigned int minRate = 0, maxRate = 0; unsigned int minRate = 0, maxRate = 0;
snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0; snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0;
int dir; int dir;
err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir); err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir);
if (err == 0) if (err == 0)
{ {
err = snd_pcm_hw_params_get_rate_max(params, &maxRate, &dir); 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) if (err == 0)
{ {
for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i) for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i)
@@ -140,6 +133,27 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
info.sampleRates_.push_back(rate); info.sampleRates_.push_back(rate);
} }
} }
}
else
{
Lv2Log::warning(SS("Failed to get maximum sample rate for device '" << info.name_ << "'."));
}
}
else
{
Lv2Log::warning(SS("Failed to get minimum sample rate for device '" << info.name_ << "'."));
}
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)
{
if (minBufferSize < 16) if (minBufferSize < 16)
{ {
minBufferSize = 16; minBufferSize = 16;
@@ -180,12 +194,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
View File
@@ -1598,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();
-1
View File
@@ -1,4 +1,3 @@
#/usr/bin/sudo /usr/bin/bash
# CMake doesn't have an uninstall option, so remove files manually. # CMake doesn't have an uninstall option, so remove files manually.
export PREFIX=/usr export PREFIX=/usr
+1
View File
@@ -1 +1,2 @@
#!/bin/bash
valgrind --leak-check=full --log-file=vg.output ./build/src/pipedald /etc/pipedal/config ./vite/dist -port 0.0.0.0:8080 -log-level debug valgrind --leak-check=full --log-file=vg.output ./build/src/pipedald /etc/pipedal/config ./vite/dist -port 0.0.0.0:8080 -log-level debug
+2
View File
@@ -60,6 +60,7 @@ export class FakeAndroidHost implements AndroidHostInterface
launchExternalUrl(_url:string): boolean launchExternalUrl(_url:string): boolean
{ {
return false; return false;
} }
private theme = 1; private theme = 1;
@@ -71,6 +72,7 @@ export class FakeAndroidHost implements AndroidHostInterface
} }
setServerVersion(_serverVersion: string): void { setServerVersion(_serverVersion: string): void {
// No-op for fake host // No-op for fake host
} }
private keepScreenOn = false; private keepScreenOn = false;
+42
View File
@@ -189,4 +189,46 @@ export default class JackHostStatus {
} }
} }
static getDisplayViewNoCpu(label: string, status?: JackHostStatus): React.ReactNode {
if (!status) {
return (<div style={{ whiteSpace: "nowrap" }}>
<Typography variant="caption" color="inherit">{label}</Typography>
<Typography variant="caption">&nbsp;</Typography>
</div>);
}
if (status.restarting) {
return (
<div style={{ whiteSpace: "nowrap" }}>
<Typography variant="caption" color="inherit">{label}</Typography>
<span style={{ color: RED_COLOR }}>
<Typography variant="caption" color="inherit">Restarting&nbsp;&nbsp;</Typography>
</span>
</div>
);
} else if (!status.active) {
return (
<div style={{ whiteSpace: "nowrap" }}>
<Typography variant="caption" color="inherit">{label}</Typography>
<span style={{ color: RED_COLOR }}>
<Typography variant="caption" color="inherit">{status.errorMessage === "" ? "Audio\u00A0Stopped" : status.errorMessage}&nbsp;&nbsp;</Typography>
</span>
</div>
);
} else {
let underrunError = status.msSinceLastUnderrun < 15 * 1000;
return (
<div style={{ whiteSpace: "nowrap" }}>
<Typography variant="caption" color="inherit">{label}</Typography>
<span style={{ color: underrunError ? RED_COLOR : GREEN_COLOR }}>
<Typography variant="caption" color="inherit">
XRuns:&nbsp;{status.underruns + ""}&nbsp;&nbsp;
</Typography>
</span>
</div>
);
}
}
}; };
+29 -5
View File
@@ -55,14 +55,38 @@ export default class JackServerSettings {
bufferSize = 64; bufferSize = 64;
numberOfBuffers = 3; numberOfBuffers = 3;
/**
* Configure this instance to use the dummy audio device. This mirrors the
* behaviour of the backend JackServerSettings::UseDummyAudioDevice method
* and is used when temporarily releasing ALSA devices.
* Needed when changing devices and when testing new settings with apply button.
*/
useDummyAudioDevice() {
if (this.sampleRate === 0) {
this.sampleRate = 48000;
}
this.valid = true;
this.alsaInputDevice = "__DUMMY_AUDIO__dummy:channels_2";
this.alsaOutputDevice = "__DUMMY_AUDIO__dummy:channels_2";
}
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;
} }
} }
+340 -79
View File
@@ -18,8 +18,6 @@
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import { Component } from 'react';
import { Theme } from '@mui/material/styles'; import { Theme } from '@mui/material/styles';
import WithStyles from './WithStyles'; import WithStyles from './WithStyles';
@@ -42,8 +40,34 @@ import Typography from '@mui/material/Typography';
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
import IconButtonEx from './IconButtonEx'; 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 FormControlLabel from '@mui/material/FormControlLabel';
import AlsaDeviceInfo from './AlsaDeviceInfo'; import AlsaDeviceInfo from './AlsaDeviceInfo';
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 category(d: AlsaDeviceInfo): number {
if (d.supportsCapture && d.supportsPlayback) return 0;
if (d.longName.toLowerCase().includes("usb")) return 1;
return 2;
}
let copy = [...devices];
copy.sort((a,b) => {
let ca = category(a);
let cb = category(b);
if (ca !== cb) return ca - cb;
return a.name.localeCompare(b.name);
});
return copy;
}
const MIN_BUFFER_SIZE = 16; const MIN_BUFFER_SIZE = 16;
const MAX_BUFFER_SIZE = 2048; const MAX_BUFFER_SIZE = 2048;
@@ -54,12 +78,17 @@ 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;
fullScreen: boolean;
compactWidth: boolean;
showDeviceWarning: boolean;
dontShowWarningAgain: boolean;
} }
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@@ -71,6 +100,9 @@ const styles = (theme: Theme) =>
selectEmpty: { selectEmpty: {
marginTop: theme.spacing(2), marginTop: theme.spacing(2),
}, },
inputLabel: {
whiteSpace: "nowrap"
}
}); });
export interface JackServerSettingsDialogProps extends WithStyles<typeof styles> { export interface JackServerSettingsDialogProps extends WithStyles<typeof styles> {
open: boolean; open: boolean;
@@ -80,13 +112,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";
} }
@@ -159,11 +193,6 @@ function getBestBuffers(
{ {
return { bufferSize:bufferSize, numberOfBuffers: numberOfBuffers}; return { bufferSize:bufferSize, numberOfBuffers: numberOfBuffers};
} }
const device = inDevice || outDevice;
if (!device)
{
return { bufferSize:bufferSize, numberOfBuffers:numberOfBuffers};
}
// If the numberOfbuffers is fine as is, don't change the number of Buffers. // If the numberOfbuffers is fine as is, don't change the number of Buffers.
// set default values. Otherwise, choose the best buffer count from available buffer counts. // set default values. Otherwise, choose the best buffer count from available buffer counts.
@@ -199,17 +228,37 @@ function getBestBuffers(
} }
// 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);
if (validBufferSizes.indexOf(32) !== -1) {
validBufferSizes = [32, ...validBufferSizes.filter(v => v !== 32)];
}
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 }; return { bufferSize: 64, numberOfBuffers: 3 };
} }
// if that isn't possible then minBufferSize/2 x 4.
bufferSize = minSize/2; 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 };
@@ -218,12 +267,16 @@ function getBestBuffers(
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;
if (!jackServerSettings.alsaInputDevice || !jackServerSettings.alsaOutputDevice) return false;
const inDevice = alsaDevices.find(d => d.id === jackServerSettings.alsaInputDevice && d.supportsCapture); 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);
@@ -232,54 +285,131 @@ 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;
} }
const JackServerSettingsDialog = withStyles( const JackServerSettingsDialog = withStyles(
class extends Component<JackServerSettingsDialogProps, JackServerSettingsDialogState> { class extends ResizeResponsiveComponent<JackServerSettingsDialogProps, JackServerSettingsDialogState> {
model: PiPedalModel; model: PiPedalModel;
ignoreClose: boolean = false;
constructor(props: JackServerSettingsDialogProps) { constructor(props: JackServerSettingsDialogProps) {
super(props); super(props);
this.model = PiPedalModelFactory.getInstance(); this.model = PiPedalModelFactory.getInstance();
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,
fullScreen: this.getFullScreen(),
compactWidth: document.documentElement.clientWidth < 600,
showDeviceWarning: false,
dontShowWarningAgain: false
}; };
} }
mounted: boolean = false; mounted: boolean = false;
suppressDeviceWarning: boolean;
/**
* Copy of the settings when the dialog is opened. Pressing Apply only tests these settings temporarily. Closing the dialog without OK re-applies the saved copy so the audio driver returns to its previous configuration.
*/
originalJackServerSettings?: JackServerSettings;
getFullScreen() {
return document.documentElement.clientWidth < 420 ||
document.documentElement.clientHeight < 700;
}
onWindowSizeChanged(width: number, height: number): void {
super.onWindowSizeChanged(width, height);
const fullScreen = this.getFullScreen();
const compactWidth = width < 600;
if (fullScreen !== this.state.fullScreen || compactWidth !== this.state.compactWidth) {
this.setState({ fullScreen, compactWidth });
}
}
requestAlsaInfo() { requestAlsaInfo() {
this.model.getAlsaDevices() this.model.getAlsaDevices()
.then((devices) => { .then((devices) => {
if (this.mounted) { if (this.mounted) {
if (this.props.open) { if (this.props.open) {
let settings = this.applyAlsaDevices(this.props.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) });
} }
} }
}) })
.catch((error) => { .catch((error) => {
// Error requesting ALSA info.
}); });
} }
/** Persist the current settings to permanent storage. */
saveSettings(settings?: JackServerSettings): void {
const s = (settings ?? this.state.jackServerSettings).clone();
// Fire and forget. Errors will be handled by PiPedalModel's internal error handling (e.g., showAlert).
this.model.setJackServerSettings(s);
}
/**
* Save the current settings temporarily so they can be restored if the
* dialog is cancelled.
*/
saveSettingsTemporary(settings?: JackServerSettings) {
this.originalJackServerSettings = (settings ?? this.state.jackServerSettings).clone();
}
/**
* Apply the provided settings to the audio system without persisting
* them. Falls back to the regular setter if the temporary apply API is
* unavailable.
*/
applySettings(settings?: JackServerSettings): void {
const s = (settings ?? this.state.jackServerSettings).clone();
s.valid = true;
const ws = this.model.webSocket;
if (ws) {
ws.request<void>("applyJackServerSettings", s)
.catch(() => { }); // Fire and forget
} else {
this.model.setJackServerSettings(s);
}
}
/**
* Revert the dialog state to the persisted settings. The reverted
* settings are applied immediately as well.
*/
revertSettings() {
this.model.getJackServerSettings()
.then((s) => {
const applied = this.applyAlsaDevices(s.clone(), this.state.alsaDevices);
this.setState({
jackServerSettings: applied,
latencyText: getLatencyText(applied),
okEnabled: isOkEnabled(applied, this.state.alsaDevices)
});
this.applySettings(s);
})
.catch(() => { });
}
applyAlsaDevices(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[]) { applyAlsaDevices(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[]) {
let result = jackServerSettings.clone(); let result = jackServerSettings.clone();
if (!alsaDevices || alsaDevices.length === 0) { if (!alsaDevices || alsaDevices.length === 0) {
@@ -289,29 +419,35 @@ 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);
if (!inDevice) { let outDevice = devices.find(d => d.id === result.alsaOutputDevice && d.supportsPlayback);
inDevice = alsaDevices.find(d => d.supportsCapture);
if (inDevice) { if (!inDevice || !outDevice) {
const capture = devices.filter(d => d.supportsCapture);
const playback = devices.filter(d => d.supportsPlayback);
if (capture.length === 1 && playback.length === 1) {
inDevice = capture[0];
outDevice = playback[0];
result.alsaInputDevice = inDevice.id;
result.alsaOutputDevice = outDevice.id;
} else if (capture.length === 1 && capture[0].supportsPlayback && playback.length === 0) {
inDevice = capture[0];
outDevice = capture[0];
result.alsaInputDevice = inDevice.id; result.alsaInputDevice = inDevice.id;
}
}
if (!outDevice) {
outDevice = alsaDevices.find(d => d.supportsPlayback);
if (outDevice) {
result.alsaOutputDevice = outDevice.id; result.alsaOutputDevice = outDevice.id;
} }
} }
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;
} }
if (result.sampleRate === 0) result.sampleRate = 48000;
let sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates); let sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates);
if (sampleRates.length === 0) sampleRates = inDevice.sampleRates; if (sampleRates.length !== 0 && sampleRates.indexOf(result.sampleRate) === -1) {
let bestSr = sampleRates[0]; let bestSr = sampleRates[0];
let bestErr = 1e36; let bestErr = 1e36;
for (let sr of sampleRates) { for (let sr of sampleRates) {
@@ -319,25 +455,35 @@ const JackServerSettingsDialog = withStyles(
if (err < bestErr) { bestErr = err; bestSr = sr; } if (err < bestErr) { bestErr = err; bestSr = sr; }
} }
result.sampleRate = bestSr; result.sampleRate = bestSr;
}
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;
} }
componentDidMount() { componentDidMount() {
super.componentDidMount();
this.mounted = true; this.mounted = true;
this.ignoreClose = false;
if (this.props.open) { if (this.props.open) {
this.requestAlsaInfo(); this.requestAlsaInfo();
this.saveSettingsTemporary(this.props.jackServerSettings);
}
} }
}
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); this.ignoreClose = false;
this.saveSettingsTemporary(this.props.jackServerSettings);
// When opening, preserve the current settings until ALSA device
// information is loaded. If we don't have device info yet, just
// clone the provided settings without applying device defaults.
let settings = this.state.alsaDevices
? this.applyAlsaDevices(this.props.jackServerSettings.clone(), this.state.alsaDevices)
: this.props.jackServerSettings.clone();
this.setState({ this.setState({
jackServerSettings: settings, jackServerSettings: settings,
latencyText: getLatencyText(settings), latencyText: getLatencyText(settings),
@@ -346,13 +492,21 @@ const JackServerSettingsDialog = withStyles(
if (!this.state.alsaDevices) { if (!this.state.alsaDevices) {
this.requestAlsaInfo(); this.requestAlsaInfo();
} }
} else if (!this.props.open && oldProps.open) {
this.originalJackServerSettings = undefined;
} }
} }
componentWillUnmount() { componentWillUnmount() {
super.componentWillUnmount();
this.mounted = false; this.mounted = false;
if (this.originalJackServerSettings) {
// Revert any unapplied changes when the dialog is unmounted
this.applySettings(this.originalJackServerSettings);
this.originalJackServerSettings = undefined;
} }
}
getSelectedDevice(deviceId: string): AlsaDeviceInfo | undefined { getSelectedDevice(deviceId: string): AlsaDeviceInfo | undefined {
if (!this.state.alsaDevices) return undefined; if (!this.state.alsaDevices) return undefined;
for (let i = 0; i < this.state.alsaDevices.length; ++i) { for (let i = 0; i < this.state.alsaDevices.length; ++i) {
@@ -371,6 +525,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,
@@ -386,6 +541,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.bufferSize = size; settings.bufferSize = size;
settings.valid = false;
let bestBufferSetting = getBestBuffers(inDev,outDev,settings.bufferSize,settings.numberOfBuffers,true); 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;
@@ -404,6 +560,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,
@@ -413,18 +570,72 @@ const JackServerSettingsDialog = withStyles(
} }
handleApply() { handleApply() {
if (this.state.okEnabled) if (this.state.okEnabled) {
{ this.applySettings(); // Fire and forget
this.props.onApply(this.state.jackServerSettings.clone());
} }
}; };
handleOk() {
if (!this.state.okEnabled) return;
const proceedWithOk = () => {
const settings = this.state.jackServerSettings.clone();
settings.valid = true;
this.ignoreClose = true; // Indicate that the closing is intentional
this.applySettings(settings); // Fire and forget
this.saveSettings(settings); // Fire and forget
this.originalJackServerSettings = undefined;
if (this.props.onApply) {
this.props.onApply(settings.clone());
}
this.props.onClose(); // Close the dialog
};
if (this.state.jackServerSettings.alsaInputDevice !== this.state.jackServerSettings.alsaOutputDevice
&& !this.suppressDeviceWarning) {
this.setState({ showDeviceWarning: true });
return;
}
proceedWithOk();
};
handleWarningProceed() {
if (this.state.dontShowWarningAgain) {
localStorage.setItem("suppressSeparateDeviceWarning", "1");
this.suppressDeviceWarning = true;
}
this.setState({ showDeviceWarning: false, dontShowWarningAgain: false }, () => {
const settings = this.state.jackServerSettings.clone();
settings.valid = true;
this.ignoreClose = true;
this.applySettings(settings); // Fire and forget
this.saveSettings(settings); // Fire and forget
this.originalJackServerSettings = undefined;
if (this.props.onApply) {
this.props.onApply(settings.clone());
}
this.props.onClose(); // Close the dialog after the warning
});
}
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;
s.valid = false;
let settings = this.applyAlsaDevices(s, this.state.alsaDevices);
this.setState({ this.setState({
jackServerSettings: s, jackServerSettings: settings,
okEnabled: isOkEnabled(s, this.state.alsaDevices) latencyText: getLatencyText(settings),
okEnabled: isOkEnabled(settings, this.state.alsaDevices)
}); });
} }
@@ -432,77 +643,104 @@ 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;
s.valid = false;
let settings = this.applyAlsaDevices(s, this.state.alsaDevices);
this.setState({ this.setState({
jackServerSettings: s, jackServerSettings: settings,
okEnabled: isOkEnabled(s, this.state.alsaDevices) latencyText: getLatencyText(settings),
okEnabled: isOkEnabled(settings, this.state.alsaDevices)
}); });
} }
render() { render() {
const classes = withStyles.getClasses(this.props); const classes = withStyles.getClasses(this.props);
const { onClose, open } = this.props; const { onClose, open } = this.props;
//Ignore close rutine if the ignoreclose is true. (After OK or Proceed or Multi Device Warning)
const handleClose = () => { const handleClose = () => {
if (this.ignoreClose) {
return;
} else {
if (this.originalJackServerSettings) {
// Revert any applied settings
this.applySettings(this.originalJackServerSettings); // Fire and forget
this.originalJackServerSettings = undefined;
}
}
onClose(); onClose();
}; };
const sortedDevices = sortDevices(this.state.alsaDevices ?? []);
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);
let bufferSizes: number[] = getValidBufferSizesMultiple(selectedInputDevice, selectedOutputDevice); const devicesSelected = (selectedInputDevice && selectedOutputDevice);
let bufferCounts = getValidBufferCountsMultiple(this.state.jackServerSettings.bufferSize, selectedInputDevice, selectedOutputDevice);
let bufferSizeDisabled = !(selectedInputDevice || selectedOutputDevice); let bufferSizes: number[] = devicesSelected ?
let bufferCountDisabled = !(selectedInputDevice || selectedOutputDevice); getValidBufferSizesMultiple(selectedInputDevice, selectedOutputDevice) : [];
let sampleRates = selectedInputDevice && selectedOutputDevice ? let bufferCounts = devicesSelected ?
getValidBufferCountsMultiple(this.state.jackServerSettings.bufferSize, selectedInputDevice, selectedOutputDevice) : [];
let bufferSizeDisabled = !devicesSelected;
let bufferCountDisabled = !devicesSelected;
let sampleRates = devicesSelected && selectedInputDevice && selectedOutputDevice ?
intersectArrays(selectedInputDevice.sampleRates, selectedOutputDevice.sampleRates) intersectArrays(selectedInputDevice.sampleRates, selectedOutputDevice.sampleRates)
: (selectedInputDevice ? selectedInputDevice.sampleRates : (selectedOutputDevice ? selectedOutputDevice.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}
> >
<DialogContent> <DialogContent>
<div style={{ display: "flex", alignItems: "center" }}> <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">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 }}
> >
{this.state.alsaDevices?.filter(d => d.supportsCapture).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}>
<InputLabel htmlFor="jsd_outputDevice">Output Device</InputLabel> <InputLabel shrink className={classes.inputLabel} htmlFor="jsd_outputDevice">Output Device</InputLabel>
<Select <Select variant="standard"
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.alsaDevices || this.state.alsaDevices.length === 0}
style={{ width: 220 }} style={{ width: 220 }}
> >
{this.state.alsaDevices?.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>
</div>
<IconButtonEx tooltip="Refresh devices" onClick={() => this.requestAlsaInfo()} aria-label="refresh-audio-devices"> <IconButtonEx tooltip="Refresh devices" onClick={() => this.requestAlsaInfo()} aria-label="refresh-audio-devices">
<RefreshIcon /> <RefreshIcon />
</IconButtonEx> </IconButtonEx>
</div>
</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}
@@ -514,14 +752,14 @@ const JackServerSettingsDialog = withStyles(
} }
}} }}
> >
{sampleRates.map((sr) => { {sampleRateOptions.map((sr) => {
return (<MenuItem value={sr}>{sr}</MenuItem> ); return (<MenuItem value={sr}>{sr}</MenuItem> );
})} })}
</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}
@@ -539,8 +777,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}
@@ -573,13 +811,36 @@ const JackServerSettingsDialog = withStyles(
<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>
)}
</>
); );
} }
}, },