Merge branch 'splitio_base' of https://github.com/rerdavies/pipedal into dev_splitio_merge
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
wpa_cli -ip2p-dev-wlan0 GET device_name pipedal
|
||||
wpa_cli -ip2p-dev-wlan0 GET country CA
|
||||
wpa_cli -ip2p-dev-wlan0 GET device_type 1-0050F204-1
|
||||
|
||||
@@ -79,7 +79,7 @@ https://github.com/user-attachments/assets/9a9fd0c6-78fc-4284-8b44-6a1929c00cc6
|
||||
### [The Build System](https://rerdavies.github.io/pipedal/TheBuildSystem.html)
|
||||
### [How to Debug PiPedal](https://rerdavies.github.io/pipedal/Debugging.html)
|
||||
|
||||
|
||||
|
||||
|
||||
#### [PiPedal Architecture](https://rerdavies.github.io/pipedal/Architecture.html)
|
||||
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@
|
||||
# bundle install
|
||||
#
|
||||
cd docs
|
||||
bundle exec jekyll serve --host
|
||||
bundle exec jekyll serve --host 0.0.0.0
|
||||
@@ -21,6 +21,7 @@ Run the following commands to install dependent libraries required by the PiPeda
|
||||
|
||||
sudo apt update
|
||||
sudo apt upgrade
|
||||
|
||||
sudo apt install -y liblilv-dev libboost-dev \
|
||||
libsystemd-dev catch libasound2-dev uuid-dev \
|
||||
authbind libavahi-client-dev libnm-dev libicu-dev \
|
||||
|
||||
@@ -13,15 +13,16 @@ available through the Code plugins store) has configured itself, build commands
|
||||
If you are not using Visual Studio Code, you can configure, build and install PiPedal using CMake build tools. For your convenience,
|
||||
the following shell scripts have been provided in the root of the project.
|
||||
|
||||
./init.sh # Configure the CMake build for the first time, or if you
|
||||
./init.sh # Configure the CMake build for the first time, or if you
|
||||
# have changed one of the CMakeList.txt files. (release build)
|
||||
|
||||
./mk.sh # Build all targets (release build)
|
||||
|
||||
sudo ./install.sh # Deploy all targets
|
||||
sudo ./install.sh # Deploy all targets
|
||||
|
||||
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
|
||||
to incorporate the PiPedal build procedure into your IDE workflow by using the contents of the build shell scripts as a model.
|
||||
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
cd build
|
||||
cpack -G DEB -C Release -config CPackConfig.cmake
|
||||
cpack -G DEB -C Release -config CPackConfig.cmake
|
||||
cd ..
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#!/bin/bash
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
${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
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#!/bin/bash
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
${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
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
|
||||
#!/bin/bash
|
||||
rm -rf ./lv2/aarch64/*
|
||||
|
||||
cp -R /usr/lib/lv2/ToobAmp.lv2/ ./lv2/aarch64/
|
||||
|
||||
+10
-4
@@ -683,6 +683,7 @@ namespace pipedal
|
||||
|
||||
if (this->captureHandle)
|
||||
{
|
||||
this->alsa_device_name = this->jackServerSettings.GetAlsaInputDevice();
|
||||
AlsaConfigureStream(
|
||||
this->alsa_device_name,
|
||||
"capture",
|
||||
@@ -695,6 +696,7 @@ namespace pipedal
|
||||
}
|
||||
if (this->playbackHandle)
|
||||
{
|
||||
this->alsa_device_name = this->jackServerSettings.GetAlsaOutputDevice();
|
||||
AlsaConfigureStream(
|
||||
this->alsa_device_name,
|
||||
"playback",
|
||||
@@ -1326,7 +1328,9 @@ namespace pipedal
|
||||
|
||||
int err;
|
||||
|
||||
alsa_device_name = jackServerSettings.GetAlsaInputDevice();
|
||||
std::string inputName = jackServerSettings.GetAlsaInputDevice();
|
||||
std::string outputName = jackServerSettings.GetAlsaOutputDevice();
|
||||
|
||||
|
||||
this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers();
|
||||
this->bufferSize = jackServerSettings.GetBufferSize();
|
||||
@@ -1335,7 +1339,8 @@ namespace pipedal
|
||||
try
|
||||
{
|
||||
|
||||
err = snd_pcm_open(&playbackHandle, alsa_device_name.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
|
||||
this->alsa_device_name = outputName;
|
||||
err = snd_pcm_open(&playbackHandle, outputName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
|
||||
if (err < 0)
|
||||
{
|
||||
switch (errno)
|
||||
@@ -1369,8 +1374,9 @@ namespace pipedal
|
||||
{
|
||||
snd_pcm_nonblock(playbackHandle, 0);
|
||||
}
|
||||
|
||||
err = snd_pcm_open(&captureHandle, alsa_device_name.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
|
||||
|
||||
this->alsa_device_name = inputName;
|
||||
err = snd_pcm_open(&captureHandle, inputName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
|
||||
|
||||
if (err < 0)
|
||||
{
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
|
||||
AlsaFormatEncodeDecodeTest(this);
|
||||
|
||||
JackServerSettings serverSettings("hw:M2",48000,32,3);
|
||||
JackServerSettings serverSettings("hw:M2","hw:M2",48000,32,3);
|
||||
|
||||
JackConfiguration jackConfiguration;
|
||||
if (useJack)
|
||||
|
||||
+11
-6
@@ -140,14 +140,19 @@ namespace pipedal
|
||||
overheadTime = readTime;
|
||||
}
|
||||
|
||||
SampleT totalTime = writeTime+ readTime + processingTime;
|
||||
SampleT maxTime = waitTime+processingTime;
|
||||
|
||||
SampleT totalTime = writeTime + readTime + processingTime;
|
||||
SampleT maxTime = waitTime + processingTime;
|
||||
|
||||
float result = 100.0f*(processingTime)/(maxTime);
|
||||
float overhead = 100.0F*(overheadTime*2)/totalTime;
|
||||
float result = 0.0f;
|
||||
float overhead = 0.0f;
|
||||
if (maxTime != 0 && totalTime != 0)
|
||||
{
|
||||
std::lock_guard lock { sync};
|
||||
result = 100.0f * (processingTime) / (maxTime);
|
||||
overhead = 100.0f * (overheadTime * 2) / totalTime;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock{sync};
|
||||
currentCpuUse = result;
|
||||
currentOverhead = overhead;
|
||||
}
|
||||
|
||||
@@ -517,6 +517,8 @@ namespace pipedal
|
||||
result.sampleRates_.push_back(48000);
|
||||
result.minBufferSize_ = 16;
|
||||
result.maxBufferSize_ = 1024;
|
||||
result.supportsCapture_ = true;
|
||||
result.supportsPlayback_ = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,13 @@ void JackServerSettings::ReadJackDaemonConfiguration()
|
||||
this->bufferSize_ = GetJackArg(argv, "-p", "--period");
|
||||
this->numberOfBuffers_ = (uint32_t)GetJackArg(argv, "-n", "--nperiods");
|
||||
this->sampleRate_ = (uint32_t)GetJackArg(argv, "-r", "--rate");
|
||||
this->alsaDevice_ = GetJackStringArg(argv,"-d", "--device");
|
||||
// read new dual device flags, fallback on old -d/--device
|
||||
std::string capDev = GetJackStringArg(argv, "-C", "--capture");
|
||||
std::string playDev = GetJackStringArg(argv, "-P", "--playback");
|
||||
std::string dev = "";
|
||||
try { dev = GetJackStringArg(argv, "-d", "--device"); } catch(...) {}
|
||||
this->alsaInputDevice_ = capDev.empty() ? dev : capDev;
|
||||
this->alsaOutputDevice_ = playDev.empty() ? dev : playDev;
|
||||
this->valid_ = true;
|
||||
}
|
||||
catch (std::exception &)
|
||||
@@ -246,7 +252,9 @@ JSON_MAP_REFERENCE(JackServerSettings, valid)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, isOnboarding)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, rebootRequired)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, isJackAudio)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, alsaDevice)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, alsaDevice) // legacy field
|
||||
JSON_MAP_REFERENCE(JackServerSettings, alsaInputDevice)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, alsaOutputDevice)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, sampleRate)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, bufferSize)
|
||||
JSON_MAP_REFERENCE(JackServerSettings, numberOfBuffers)
|
||||
|
||||
+22
-15
@@ -31,7 +31,9 @@ namespace pipedal
|
||||
bool isOnboarding_ = true;
|
||||
bool isJackAudio_ = JACK_HOST ? true : false;
|
||||
bool rebootRequired_ = false;
|
||||
std::string alsaDevice_;
|
||||
std::string alsaInputDevice_;
|
||||
std::string alsaOutputDevice_;
|
||||
std::string alsaDevice_; // legacy
|
||||
uint64_t sampleRate_ = 0;
|
||||
uint32_t bufferSize_ = 64;
|
||||
uint32_t numberOfBuffers_ = 3;
|
||||
@@ -39,10 +41,14 @@ namespace pipedal
|
||||
public:
|
||||
JackServerSettings();
|
||||
JackServerSettings(
|
||||
const std::string alsaInputDevice,
|
||||
uint64_t sampleRate, uint32_t bufferSize, uint32_t numberOfBuffers)
|
||||
const std::string &alsaInputDevice,
|
||||
const std::string &alsaOutputDevice,
|
||||
uint64_t sampleRate,
|
||||
uint32_t bufferSize,
|
||||
uint32_t numberOfBuffers)
|
||||
: valid_(true),
|
||||
alsaDevice_(alsaInputDevice),
|
||||
alsaInputDevice_(alsaInputDevice),
|
||||
alsaOutputDevice_(alsaOutputDevice),
|
||||
sampleRate_(sampleRate),
|
||||
bufferSize_(bufferSize),
|
||||
numberOfBuffers_(numberOfBuffers),
|
||||
@@ -51,9 +57,13 @@ namespace pipedal
|
||||
}
|
||||
|
||||
uint64_t GetSampleRate() const { return sampleRate_; }
|
||||
|
||||
uint32_t GetBufferSize() const { return bufferSize_; }
|
||||
uint32_t GetNumberOfBuffers() const { return numberOfBuffers_; }
|
||||
const std::string &GetAlsaInputDevice() const { return alsaDevice_; }
|
||||
const std::string &GetAlsaInputDevice() const { return alsaInputDevice_; }
|
||||
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() {
|
||||
this->valid_ = true;
|
||||
if (sampleRate_ == 0) sampleRate_ = 48000;
|
||||
@@ -69,14 +79,6 @@ namespace pipedal
|
||||
|
||||
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 SetRebootRequired(bool value)
|
||||
{
|
||||
@@ -87,9 +89,14 @@ namespace pipedal
|
||||
isOnboarding_ = value;
|
||||
}
|
||||
|
||||
bool Equals(const JackServerSettings &other)
|
||||
bool Equals(const JackServerSettings &other) const
|
||||
{
|
||||
return this->alsaDevice_ == other.alsaDevice_ && 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);
|
||||
|
||||
+25
-16
@@ -57,9 +57,9 @@ void PrintHelp()
|
||||
pp << "Copyright (c) 2022 Robin Davies\n";
|
||||
pp << "\n";
|
||||
pp << Indent(0) << "Syntax\n\n";
|
||||
pp << Indent(2) << "pipedal_latency_test [<options>] <device-name>\n\n";
|
||||
pp << "where <device-name> is the name of an ALSA device. Typically this should be the name of a hardware "
|
||||
"device (a device name starting with 'hw:').\n\n";
|
||||
pp << Indent(2) << "pipedal_latency_test [<options>] <input-device> [<output-device>]\n\n";
|
||||
pp << "where <input-device> is the name of an ALSA capture device and <output-device> is the name of a playback device. "
|
||||
"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(15);
|
||||
|
||||
@@ -95,7 +95,8 @@ void PrintHelp()
|
||||
|
||||
pp << Indent(0) << "Examples\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()
|
||||
@@ -138,7 +139,8 @@ public:
|
||||
private:
|
||||
AudioDriver *audioDriver = nullptr;
|
||||
|
||||
const std::string &deviceId;
|
||||
const std::string &inputDeviceId;
|
||||
const std::string &outputDeviceId;
|
||||
ChannelsT inputChannels;
|
||||
ChannelsT outputChannels;
|
||||
uint32_t sampleRate;
|
||||
@@ -147,11 +149,13 @@ private:
|
||||
|
||||
public:
|
||||
AlsaTester(
|
||||
const std::string &deviceId,
|
||||
const std::string &inputDeviceId,
|
||||
const std::string &outputDeviceId,
|
||||
const ChannelsT &inputChannels,
|
||||
const ChannelsT &outputChannels,
|
||||
uint32_t sampleRate, int bufferSize, int buffers)
|
||||
: deviceId(deviceId),
|
||||
: inputDeviceId(inputDeviceId),
|
||||
outputDeviceId(outputDeviceId),
|
||||
sampleRate(sampleRate),
|
||||
inputChannels(inputChannels),
|
||||
outputChannels(outputChannels),
|
||||
@@ -186,7 +190,7 @@ public:
|
||||
TestResult result;
|
||||
try
|
||||
{
|
||||
JackServerSettings serverSettings(deviceId, sampleRate, bufferSize, buffers);
|
||||
JackServerSettings serverSettings(inputDeviceId, outputDeviceId, sampleRate, bufferSize, buffers);
|
||||
|
||||
JackConfiguration jackConfiguration;
|
||||
jackConfiguration.AlsaInitialize(serverSettings);
|
||||
@@ -403,12 +407,13 @@ public:
|
||||
};
|
||||
|
||||
TestResult RunLatencyTest(
|
||||
const std::string deviceId,
|
||||
const std::string inputDeviceId,
|
||||
const std::string outputDeviceId,
|
||||
const ChannelsT &inputChannels,
|
||||
const ChannelsT &outputChannels,
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -428,13 +433,14 @@ static std::string overheadDisplay(float value)
|
||||
}
|
||||
|
||||
void RunLatencyTest(
|
||||
const std::string &deviceId,
|
||||
const std::string &inputDeviceId,
|
||||
const std::string &outputDeviceId,
|
||||
const ChannelsT &inputChannels,
|
||||
const ChannelsT &outputChannels,
|
||||
uint32_t sampleRate)
|
||||
{
|
||||
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 BUFFERS_COLUMN_WIDTH = 20;
|
||||
@@ -461,7 +467,7 @@ void RunLatencyTest(
|
||||
|
||||
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);
|
||||
column += BUFFERS_COLUMN_WIDTH;
|
||||
@@ -556,11 +562,14 @@ std:
|
||||
{
|
||||
ListDevices();
|
||||
}
|
||||
else if (parser.Arguments().size() == 1)
|
||||
else if (parser.Arguments().size() >= 1 && parser.Arguments().size() <= 2)
|
||||
{
|
||||
inputChannels = ParseChannels(strInputChannels);
|
||||
outputChannels = ParseChannels(strInputChannels);
|
||||
RunLatencyTest(parser.Arguments()[0], inputChannels, outputChannels, sampleRate);
|
||||
outputChannels = ParseChannels(strOutputChannels);
|
||||
|
||||
std::string inDev = parser.Arguments()[0];
|
||||
std::string outDev = parser.Arguments().size() == 2 ? parser.Arguments()[1] : inDev;
|
||||
RunLatencyTest(inDev, outDev, inputChannels, outputChannels, sampleRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+55
-16
@@ -22,6 +22,7 @@
|
||||
#include "alsa/asoundlib.h"
|
||||
#include "Lv2Log.hpp"
|
||||
#include <mutex>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
@@ -111,6 +112,7 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
|
||||
}
|
||||
if (err == 0)
|
||||
{
|
||||
snd_pcm_t *hDevice = captureOk ? captureDevice : playbackDevice; // xxx: HECK NO!
|
||||
snd_pcm_hw_params_t *params = nullptr;
|
||||
err = snd_pcm_hw_params_malloc(¶ms);
|
||||
if (err == 0)
|
||||
@@ -121,29 +123,42 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
|
||||
unsigned int minRate = 0, maxRate = 0;
|
||||
snd_pcm_uframes_t minBufferSize = 0, maxBufferSize = 0;
|
||||
int dir;
|
||||
|
||||
err = snd_pcm_hw_params_get_rate_min(params, &minRate, &dir);
|
||||
if (err == 0)
|
||||
{
|
||||
err = snd_pcm_hw_params_get_rate_max(params, &maxRate, &dir);
|
||||
if (err == 0)
|
||||
{
|
||||
for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i)
|
||||
{
|
||||
uint32_t rate = RATES[i];
|
||||
if (rate >= minRate && rate <= maxRate)
|
||||
{
|
||||
info.sampleRates_.push_back(rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
for (size_t i = 0; i < sizeof(RATES) / sizeof(RATES[0]); ++i)
|
||||
if (err == 0)
|
||||
{
|
||||
uint32_t rate = RATES[i];
|
||||
if (rate >= minRate && rate <= maxRate)
|
||||
{
|
||||
info.sampleRates_.push_back(rate);
|
||||
}
|
||||
err = snd_pcm_hw_params_get_buffer_size_max(params, &maxBufferSize);
|
||||
}
|
||||
}
|
||||
if (err == 0)
|
||||
{
|
||||
if (minBufferSize < 16)
|
||||
{
|
||||
minBufferSize = 16;
|
||||
@@ -158,7 +173,19 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
|
||||
}
|
||||
if (params != nullptr)
|
||||
snd_pcm_hw_params_free(params);
|
||||
snd_pcm_close(hDevice);
|
||||
if (captureOk) // HECK NO!! REVIEW THIS!
|
||||
snd_pcm_close(captureDevice);
|
||||
if (playbackOk && playbackDevice != captureDevice)
|
||||
snd_pcm_close(playbackDevice);
|
||||
if (err == 0)
|
||||
{
|
||||
cacheDevice(info.name_, info);
|
||||
result.push_back(info);
|
||||
}
|
||||
else if (getCachedDevice(info.name_, &info))
|
||||
{
|
||||
result.push_back(info);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -174,12 +201,24 @@ std::vector<AlsaDeviceInfo> PiPedalAlsaDevices::GetAlsaDevices()
|
||||
}
|
||||
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 --");
|
||||
for (auto &device : result)
|
||||
for (auto &device : filtered)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -31,6 +31,8 @@ namespace pipedal {
|
||||
std::string longName_;
|
||||
std::vector<uint32_t> sampleRates_;
|
||||
uint32_t minBufferSize_ = 0,maxBufferSize_ = 0;
|
||||
bool supportsCapture_ = false;
|
||||
bool supportsPlayback_ = false;
|
||||
|
||||
bool isDummyDevice() const {
|
||||
return id_.starts_with("dummy:");
|
||||
|
||||
@@ -1750,6 +1750,14 @@ pipedal::JackServerSettings Storage::GetJackServerSettings()
|
||||
{
|
||||
json_reader reader(f);
|
||||
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
|
||||
result.Initialize();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,6 +9,8 @@ export default class AlsaDeviceInfo {
|
||||
this.sampleRates = input.sampleRates as number[];
|
||||
this.minBufferSize = input.minBufferSize;
|
||||
this.maxBufferSize = input.maxBufferSize;
|
||||
this.supportsCapture = input.supportsCapture ? true : false;
|
||||
this.supportsPlayback = input.supportsPlayback ? true : false;
|
||||
return this;
|
||||
}
|
||||
static deserialize_array(input: any): AlsaDeviceInfo[]
|
||||
|
||||
@@ -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"> </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 </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} </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: {status.underruns + ""}
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
@@ -48,18 +48,44 @@ export default class JackServerSettings {
|
||||
isOnboarding: boolean = true;
|
||||
rebootRequired = false;
|
||||
isJackAudio = false;
|
||||
alsaDevice: string = "";
|
||||
alsaInputDevice: string = "";
|
||||
alsaOutputDevice: string = "";
|
||||
sampleRate = 48000;
|
||||
bufferSize = 64;
|
||||
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() {
|
||||
if (this.valid) {
|
||||
let device = this.alsaDevice;
|
||||
if (device.startsWith("hw:")) device = device.substring(3);
|
||||
return device + " - Rate: " + this.sampleRate + " BufferSize: " + this.bufferSize + " Buffers: " + this.numberOfBuffers;
|
||||
if (!this.valid || !this.alsaInputDevice || !this.alsaOutputDevice) {
|
||||
return "Not selected";
|
||||
}
|
||||
|
||||
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 {
|
||||
return "Not configured";
|
||||
return inDev+" → "+outDev;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
import { Component } from 'react';
|
||||
|
||||
import { Theme } from '@mui/material/styles';
|
||||
|
||||
import WithStyles from './WithStyles';
|
||||
@@ -40,8 +39,36 @@ import DialogContent from '@mui/material/DialogContent';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel';
|
||||
import IconButtonEx from './IconButtonEx';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
|
||||
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 MAX_BUFFER_SIZE = 2048;
|
||||
@@ -52,12 +79,17 @@ interface BufferSetting {
|
||||
numberOfBuffers: number;
|
||||
};
|
||||
|
||||
const INVALID_DEVICE_ID = "_invalid_";
|
||||
// empty string used when no valid device is selected - the default.
|
||||
const INVALID_DEVICE_ID = "";
|
||||
interface JackServerSettingsDialogState {
|
||||
latencyText: string;
|
||||
jackServerSettings: JackServerSettings;
|
||||
alsaDevices?: AlsaDeviceInfo[];
|
||||
okEnabled: boolean;
|
||||
fullScreen: boolean;
|
||||
compactWidth: boolean;
|
||||
showDeviceWarning: boolean;
|
||||
dontShowWarningAgain: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
@@ -69,6 +101,9 @@ const styles = (theme: Theme) =>
|
||||
selectEmpty: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
inputLabel: {
|
||||
whiteSpace: "nowrap"
|
||||
}
|
||||
});
|
||||
export interface JackServerSettingsDialogProps extends WithStyles<typeof styles> {
|
||||
open: boolean;
|
||||
@@ -78,13 +113,15 @@ export interface JackServerSettingsDialogProps extends WithStyles<typeof styles>
|
||||
}
|
||||
|
||||
function getLatencyText(settings?: JackServerSettings ): string {
|
||||
if (!settings)
|
||||
{
|
||||
if (!settings) {
|
||||
return "\u00A0";
|
||||
}
|
||||
|
||||
if (!settings.valid) return "\u00A0";
|
||||
let ms = settings.bufferSize * settings.numberOfBuffers / settings.sampleRate * 1000;
|
||||
if (!settings.sampleRate || !settings.bufferSize || !settings.numberOfBuffers) {
|
||||
return "\u00A0";
|
||||
}
|
||||
|
||||
let ms = (settings.bufferSize * settings.numberOfBuffers) / settings.sampleRate * 1000;
|
||||
return ms.toFixed(1) + "ms";
|
||||
}
|
||||
|
||||
@@ -171,158 +208,286 @@ function getBestBuffers(
|
||||
return {bufferSize: bufferSize, numberOfBuffers:numberOfBuffers};
|
||||
}
|
||||
}
|
||||
// otherwise select a sensible starting value.
|
||||
// otherwise select a sensible starting value.
|
||||
|
||||
// favored default: 64x3.
|
||||
if (64*3 >= alsaDeviceInfo.minBufferSize && 64*3 <= alsaDeviceInfo.maxBufferSize)
|
||||
{
|
||||
return { bufferSize: 64, numberOfBuffers: 3};
|
||||
// Build candidate sizes with 32 first if present.
|
||||
let validBufferSizes = getValidBufferSizesMultiple(inDevice, outDevice);
|
||||
validBufferSizes.sort((a,b)=>a-b);
|
||||
if (validBufferSizes.indexOf(32) !== -1) {
|
||||
validBufferSizes = [32, ...validBufferSizes.filter(v => v !== 32)];
|
||||
}
|
||||
// if that isn't possible then minBufferSize/2 x 4.
|
||||
bufferSize = alsaDeviceInfo.minBufferSize/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;
|
||||
// otherwise, minBufferSize/2 x 2.
|
||||
if (bufferSize*numberOfBuffers > alsaDeviceInfo.maxBufferSize)
|
||||
{
|
||||
if (bufferSize * numberOfBuffers > maxSize) {
|
||||
numberOfBuffers = 2;
|
||||
}
|
||||
return { bufferSize: bufferSize, numberOfBuffers: numberOfBuffers};
|
||||
return { bufferSize: bufferSize, numberOfBuffers: numberOfBuffers };
|
||||
|
||||
};
|
||||
|
||||
function isOkEnabled(jackServerSettings: JackServerSettings, alsaDevices?: AlsaDeviceInfo[])
|
||||
{
|
||||
if (!jackServerSettings.valid) return false;
|
||||
if (!alsaDevices) return false;
|
||||
let alsaDevice: AlsaDeviceInfo | undefined = undefined;
|
||||
for (let i = 0; i < alsaDevices.length; ++i)
|
||||
{
|
||||
if (alsaDevices[i].id === jackServerSettings.alsaDevice)
|
||||
{
|
||||
alsaDevice = alsaDevices[i];
|
||||
}
|
||||
}
|
||||
if (!alsaDevice)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let deviceBufferSize = jackServerSettings.bufferSize * jackServerSettings.numberOfBuffers;
|
||||
if (deviceBufferSize < alsaDevice.minBufferSize || deviceBufferSize > alsaDevice.maxBufferSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let validBufferCounts = getValidBufferCounts(jackServerSettings.bufferSize, alsaDevice);
|
||||
let ix = validBufferCounts.indexOf(jackServerSettings.numberOfBuffers);
|
||||
if (ix === -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
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 minSize = Math.max(inDevice.minBufferSize, outDevice.minBufferSize);
|
||||
const maxSize = Math.min(inDevice.maxBufferSize, outDevice.maxBufferSize);
|
||||
if (deviceBufferSize < minSize || deviceBufferSize > maxSize) return false;
|
||||
|
||||
const validBufferCounts = getValidBufferCountsMultiple(jackServerSettings.bufferSize, inDevice, outDevice);
|
||||
if (validBufferCounts.indexOf(jackServerSettings.numberOfBuffers) === -1) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
const JackServerSettingsDialog = withStyles(
|
||||
class extends Component<JackServerSettingsDialogProps, JackServerSettingsDialogState> {
|
||||
class extends ResizeResponsiveComponent<JackServerSettingsDialogProps, JackServerSettingsDialogState> {
|
||||
|
||||
model: PiPedalModel;
|
||||
ignoreClose: boolean = false;
|
||||
|
||||
constructor(props: JackServerSettingsDialogProps) {
|
||||
super(props);
|
||||
this.model = PiPedalModelFactory.getInstance();
|
||||
|
||||
|
||||
|
||||
this.suppressDeviceWarning = localStorage.getItem("suppressSeparateDeviceWarning") === "1";
|
||||
this.state = {
|
||||
latencyText: getLatencyText(props.jackServerSettings),
|
||||
jackServerSettings: props.jackServerSettings.clone(), // invalid, but not nullish
|
||||
alsaDevices: undefined,
|
||||
okEnabled: false
|
||||
okEnabled: false,
|
||||
fullScreen: this.getFullScreen(),
|
||||
compactWidth: document.documentElement.clientWidth < 600,
|
||||
showDeviceWarning: false,
|
||||
dontShowWarningAgain: 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() {
|
||||
this.model.getAlsaDevices()
|
||||
.then((devices) => {
|
||||
if (this.mounted) {
|
||||
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({
|
||||
alsaDevices: devices,
|
||||
alsaDevices: f,
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings,devices)
|
||||
okEnabled: isOkEnabled(settings, f)
|
||||
});
|
||||
} else {
|
||||
this.setState({ alsaDevices: devices });
|
||||
this.setState({ alsaDevices: filterDevices(devices) });
|
||||
}
|
||||
}
|
||||
})
|
||||
.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[]) {
|
||||
let result = jackServerSettings.clone();
|
||||
if (!alsaDevices) {
|
||||
return result;
|
||||
}
|
||||
result.valid = false;
|
||||
if (alsaDevices.length === 0) {
|
||||
result.alsaDevice = INVALID_DEVICE_ID;
|
||||
|
||||
let devices = filterDevices(alsaDevices);
|
||||
let inDevice = devices.find(d => d.id === result.alsaInputDevice && d.supportsCapture);
|
||||
let outDevice = devices.find(d => d.id === result.alsaOutputDevice && d.supportsPlayback);
|
||||
|
||||
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.alsaOutputDevice = outDevice.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inDevice || !outDevice) {
|
||||
result.valid = false;
|
||||
if (!inDevice) result.alsaInputDevice = INVALID_DEVICE_ID;
|
||||
if (!outDevice) result.alsaOutputDevice = INVALID_DEVICE_ID;
|
||||
return result;
|
||||
}
|
||||
|
||||
let selectedDevice: AlsaDeviceInfo | undefined = undefined;
|
||||
for (let i = 0; i < alsaDevices.length; ++i) {
|
||||
if (alsaDevices[i].id === result.alsaDevice) {
|
||||
selectedDevice = alsaDevices[i]
|
||||
break;
|
||||
let sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates);
|
||||
if (sampleRates.length !== 0 && sampleRates.indexOf(result.sampleRate) === -1) {
|
||||
let bestSr = sampleRates[0];
|
||||
let bestErr = 1e36;
|
||||
for (let sr of sampleRates) {
|
||||
let err = (sr - result.sampleRate) * (sr - result.sampleRate);
|
||||
if (err < bestErr) { bestErr = err; bestSr = sr; }
|
||||
}
|
||||
result.sampleRate = bestSr;
|
||||
}
|
||||
if (!selectedDevice) {
|
||||
selectedDevice = alsaDevices[0];
|
||||
result.alsaDevice = selectedDevice.id;
|
||||
|
||||
}
|
||||
if (result.sampleRate === 0) result.sampleRate = 48000;
|
||||
result.sampleRate = selectedDevice.closestSampleRate(result.sampleRate);
|
||||
|
||||
let bestBuffers = getBestBuffers(selectedDevice,result.bufferSize,result.numberOfBuffers);
|
||||
let bestBuffers = getBestBuffers(inDevice, outDevice, result.bufferSize, result.numberOfBuffers);
|
||||
result.bufferSize = bestBuffers.bufferSize;
|
||||
result.numberOfBuffers = bestBuffers.numberOfBuffers;
|
||||
|
||||
result.valid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
super.componentDidMount();
|
||||
this.mounted = true;
|
||||
this.ignoreClose = false;
|
||||
if (this.props.open) {
|
||||
this.requestAlsaInfo();
|
||||
this.saveSettingsTemporary(this.props.jackServerSettings);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: JackServerSettingsDialogProps) {
|
||||
if ((this.props.open && !oldProps.open) && this.mounted) {
|
||||
let settings = this.applyAlsaDevices(this.props.jackServerSettings.clone(), this.state.alsaDevices);
|
||||
this.setState({
|
||||
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({
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings,this.state.alsaDevices)
|
||||
});
|
||||
okEnabled: isOkEnabled(settings, this.state.alsaDevices)
|
||||
});
|
||||
if (!this.state.alsaDevices) {
|
||||
this.requestAlsaInfo();
|
||||
}
|
||||
}
|
||||
} else if (!this.props.open && oldProps.open) {
|
||||
this.originalJackServerSettings = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount();
|
||||
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 | null {
|
||||
if (!this.state.alsaDevices) return null;
|
||||
|
||||
getSelectedDevice(deviceId: string): AlsaDeviceInfo | undefined {
|
||||
if (!this.state.alsaDevices) return undefined;
|
||||
for (let i = 0; i < this.state.alsaDevices.length; ++i) {
|
||||
if (this.state.alsaDevices[i].id === deviceId) {
|
||||
return this.state.alsaDevices[i];
|
||||
@@ -354,6 +519,7 @@ const JackServerSettingsDialog = withStyles(
|
||||
if (!selectedDevice) return;
|
||||
let settings = this.state.jackServerSettings.clone();
|
||||
settings.sampleRate = rate;
|
||||
settings.valid = false;
|
||||
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
@@ -368,7 +534,8 @@ const JackServerSettingsDialog = withStyles(
|
||||
if (!selectedDevice) return;
|
||||
let settings = this.state.jackServerSettings.clone();
|
||||
settings.bufferSize = size;
|
||||
let bestBufferSetting = getBestBuffers(selectedDevice,settings.bufferSize,settings.numberOfBuffers,true);
|
||||
settings.valid = false;
|
||||
let bestBufferSetting = getBestBuffers(inDev,outDev,settings.bufferSize,settings.numberOfBuffers,true);
|
||||
settings.bufferSize = bestBufferSetting.bufferSize;
|
||||
settings.numberOfBuffers = bestBufferSetting.numberOfBuffers;
|
||||
|
||||
@@ -385,6 +552,7 @@ const JackServerSettingsDialog = withStyles(
|
||||
if (!selectedDevice) return;
|
||||
let settings = this.state.jackServerSettings.clone();
|
||||
settings.numberOfBuffers = bufferCount;
|
||||
settings.valid = false;
|
||||
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
@@ -394,73 +562,177 @@ const JackServerSettingsDialog = withStyles(
|
||||
}
|
||||
|
||||
handleApply() {
|
||||
if (this.state.okEnabled)
|
||||
{
|
||||
this.props.onApply(this.state.jackServerSettings.clone());
|
||||
if (this.state.okEnabled) {
|
||||
this.applySettings(); // Fire and forget
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
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) {
|
||||
const d = e.target.value as string;
|
||||
let s = this.state.jackServerSettings.clone();
|
||||
s.alsaInputDevice = d;
|
||||
s.valid = false;
|
||||
let settings = this.applyAlsaDevices(s, this.state.alsaDevices);
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings, this.state.alsaDevices)
|
||||
});
|
||||
}
|
||||
|
||||
handleOutputDeviceChanged(e: any) {
|
||||
const d = e.target.value as string;
|
||||
let s = this.state.jackServerSettings.clone();
|
||||
s.alsaOutputDevice = d;
|
||||
s.valid = false;
|
||||
let settings = this.applyAlsaDevices(s, this.state.alsaDevices);
|
||||
this.setState({
|
||||
jackServerSettings: settings,
|
||||
latencyText: getLatencyText(settings),
|
||||
okEnabled: isOkEnabled(settings, this.state.alsaDevices)
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const classes = withStyles.getClasses(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 = () => {
|
||||
onClose();
|
||||
};
|
||||
let waitingForDevices = !this.state.alsaDevices
|
||||
let noDevices = this.state.alsaDevices && this.state.alsaDevices.length === 0;
|
||||
let selectedDevice: AlsaDeviceInfo | undefined = undefined;
|
||||
if (this.state.alsaDevices) {
|
||||
for (let device of this.state.alsaDevices) {
|
||||
if (device.id === this.state.jackServerSettings.alsaDevice) {
|
||||
selectedDevice = device;
|
||||
break;
|
||||
if (this.ignoreClose) {
|
||||
return;
|
||||
} else {
|
||||
if (this.originalJackServerSettings) {
|
||||
// Revert any applied settings
|
||||
this.applySettings(this.originalJackServerSettings); // Fire and forget
|
||||
this.originalJackServerSettings = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
let bufferSizes: number[] = getValidBufferSizes(selectedDevice);
|
||||
let bufferCounts = getValidBufferCounts(this.state.jackServerSettings.bufferSize,selectedDevice);
|
||||
let bufferSizeDisabled = !selectedDevice;
|
||||
let bufferCountDisabled = !selectedDevice;
|
||||
onClose();
|
||||
};
|
||||
|
||||
const sortedDevices = sortDevices(this.state.alsaDevices ?? []);
|
||||
|
||||
let selectedInputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaInputDevice);
|
||||
let selectedOutputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaOutputDevice);
|
||||
|
||||
const devicesSelected = (selectedInputDevice && selectedOutputDevice);
|
||||
|
||||
let bufferSizes: number[] = devicesSelected ?
|
||||
getValidBufferSizesMultiple(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)
|
||||
: [];
|
||||
let sampleRateOptions = sampleRates.length === 0 && this.state.jackServerSettings.sampleRate ? [this.state.jackServerSettings.sampleRate] : sampleRates;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogEx tag="jack" onClose={handleClose} aria-labelledby="select-channels-title" open={open}
|
||||
onEnterKey={() => {
|
||||
this.handleApply();
|
||||
this.handleOk();
|
||||
}}
|
||||
fullScreen={this.state.fullScreen}
|
||||
|
||||
>
|
||||
<DialogContent>
|
||||
<div>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="jsd_device">Device</InputLabel>
|
||||
<Select variant="standard" onChange={(e) => this.handleDeviceChanged(e)}
|
||||
value={this.state.jackServerSettings.alsaDevice}
|
||||
style={{width: 220}}
|
||||
inputProps={{
|
||||
name: "Device",
|
||||
id: "jsd_device",
|
||||
}}
|
||||
<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 */}
|
||||
<FormControl variant="standard" className={classes.formControl}>
|
||||
<InputLabel shrink className={classes.inputLabel} htmlFor="jsd_inputDevice">Input Device</InputLabel>
|
||||
<Select variant="standard"
|
||||
id="jsd_inputDevice"
|
||||
value={this.state.jackServerSettings.alsaInputDevice}
|
||||
onChange={e => this.handleInputDeviceChanged(e)}
|
||||
disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0}
|
||||
style={{ width: 220 }}
|
||||
>
|
||||
{(noDevices && !waitingForDevices) &&
|
||||
(
|
||||
<MenuItem value={INVALID_DEVICE_ID}>No suitable devices.</MenuItem>
|
||||
)
|
||||
}
|
||||
{((!noDevices) && !waitingForDevices) && (
|
||||
this.state.alsaDevices!.map((device) =>
|
||||
(
|
||||
<MenuItem key={device.id} value={device.id}>{device.name}</MenuItem>
|
||||
)
|
||||
|
||||
)
|
||||
)}
|
||||
{sortedDevices.filter(d => d.supportsCapture).map(d => (
|
||||
<MenuItem key={d.id} value={d.id}>{d.name}</MenuItem>
|
||||
)) || <MenuItem value="" disabled>Loading...</MenuItem>}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{/* Audio Output Device */}
|
||||
<FormControl variant="standard" className={classes.formControl}>
|
||||
<InputLabel shrink className={classes.inputLabel} htmlFor="jsd_outputDevice">Output Device</InputLabel>
|
||||
<Select variant="standard"
|
||||
id="jsd_outputDevice"
|
||||
value={this.state.jackServerSettings.alsaOutputDevice}
|
||||
onChange={e => this.handleOutputDeviceChanged(e)}
|
||||
disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0}
|
||||
style={{ width: 220 }}
|
||||
>
|
||||
{sortedDevices.filter(d => d.supportsPlayback).map(d => (
|
||||
<MenuItem key={d.id} value={d.id}>{d.name}</MenuItem>
|
||||
)) || <MenuItem value="" disabled>Loading...</MenuItem>}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<IconButtonEx tooltip="Refresh devices" onClick={() => this.requestAlsaInfo()} aria-label="refresh-audio-devices">
|
||||
<RefreshIcon />
|
||||
</IconButtonEx>
|
||||
</div>
|
||||
</div><div>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="jsd_sampleRate">Sample rate</InputLabel>
|
||||
<FormControl variant="standard" className={classes.formControl}>
|
||||
<InputLabel shrink className={classes.inputLabel} htmlFor="jsd_sampleRate">Sample rate</InputLabel>
|
||||
<Select variant="standard"
|
||||
onChange={(e) => this.handleRateChanged(e)}
|
||||
value={this.state.jackServerSettings.sampleRate}
|
||||
@@ -472,16 +744,14 @@ const JackServerSettingsDialog = withStyles(
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedDevice &&
|
||||
selectedDevice.sampleRates.map((sr) => {
|
||||
return ( <MenuItem value={sr}>{sr}</MenuItem> );
|
||||
})
|
||||
}
|
||||
{sampleRateOptions.map((sr) => {
|
||||
return (<MenuItem value={sr}>{sr}</MenuItem> );
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<div style={{ display: "inline", whiteSpace: "nowrap" }}>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="bufferSize">Buffer size</InputLabel>
|
||||
<FormControl variant="standard" className={classes.formControl}>
|
||||
<InputLabel shrink className={classes.inputLabel} htmlFor="bufferSize">Buffer size</InputLabel>
|
||||
<Select variant="standard"
|
||||
onChange={(e) => this.handleSizeChanged(e)}
|
||||
value={this.state.jackServerSettings.bufferSize}
|
||||
@@ -499,8 +769,8 @@ const JackServerSettingsDialog = withStyles(
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormControl className={classes.formControl}>
|
||||
<InputLabel htmlFor="numberofBuffers">Buffers</InputLabel>
|
||||
<FormControl variant="standard" className={classes.formControl}>
|
||||
<InputLabel shrink className={classes.inputLabel} htmlFor="numberofBuffers">Buffers</InputLabel>
|
||||
<Select variant="standard"
|
||||
onChange={(e) => this.handleNumberOfBuffersChanged(e)}
|
||||
value={this.state.jackServerSettings.numberOfBuffers}
|
||||
@@ -530,16 +800,39 @@ const JackServerSettingsDialog = withStyles(
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="dialogSecondary" onClick={handleClose} >
|
||||
<Button variant="dialogSecondary" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="dialogPrimary" onClick={() => this.handleApply()}
|
||||
disabled={!this.state.okEnabled}>
|
||||
<Button variant="dialogSecondary" onClick={() => this.handleApply()} disabled={!this.state.okEnabled}>Apply</Button>
|
||||
<Button variant="dialogPrimary" onClick={() => this.handleOk()} disabled={!this.state.okEnabled}>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user