diff --git a/NetworkManagerP2P/src/wpa_init b/NetworkManagerP2P/src/wpa_init index faf8e15..d9a3eb3 100755 --- a/NetworkManagerP2P/src/wpa_init +++ b/NetworkManagerP2P/src/wpa_init @@ -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 diff --git a/README.md b/README.md index 942c565..addc9f1 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/debugDocs.sh b/debugDocs.sh index 2261a47..bebd254 100755 --- a/debugDocs.sh +++ b/debugDocs.sh @@ -6,4 +6,4 @@ # bundle install # cd docs -bundle exec jekyll serve --host \ No newline at end of file +bundle exec jekyll serve --host 0.0.0.0 \ No newline at end of file diff --git a/docs/BuildPrerequisites.md b/docs/BuildPrerequisites.md index 78ab8af..3f1a9b6 100644 --- a/docs/BuildPrerequisites.md +++ b/docs/BuildPrerequisites.md @@ -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 \ diff --git a/docs/TheBuildSystem.md b/docs/TheBuildSystem.md index 7f69410..ad271a9 100644 --- a/docs/TheBuildSystem.md +++ b/docs/TheBuildSystem.md @@ -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. diff --git a/makePackage.sh b/makePackage.sh index 617a733..0ede2d0 100755 --- a/makePackage.sh +++ b/makePackage.sh @@ -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 .. diff --git a/profiler_tools/ToobNam_profile b/profiler_tools/ToobNam_profile index c616390..db5b384 100755 --- a/profiler_tools/ToobNam_profile +++ b/profiler_tools/ToobNam_profile @@ -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 + diff --git a/profiler_tools/nam_profile_web b/profiler_tools/nam_profile_web index 70827d4..13b6462 100755 --- a/profiler_tools/nam_profile_web +++ b/profiler_tools/nam_profile_web @@ -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 diff --git a/provisionLv2 b/provisionLv2 index dc8b9ea..29f07b2 100755 --- a/provisionLv2 +++ b/provisionLv2 @@ -1,4 +1,4 @@ - +#!/bin/bash rm -rf ./lv2/aarch64/* cp -R /usr/lib/lv2/ToobAmp.lv2/ ./lv2/aarch64/ diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index a374068..503a125 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -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) { diff --git a/src/AlsaDriverTest.cpp b/src/AlsaDriverTest.cpp index 29b1f2c..2508570 100644 --- a/src/AlsaDriverTest.cpp +++ b/src/AlsaDriverTest.cpp @@ -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) diff --git a/src/CpuUse.hpp b/src/CpuUse.hpp index e2dedac..d5b96d2 100644 --- a/src/CpuUse.hpp +++ b/src/CpuUse.hpp @@ -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; } diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index a03e034..185e99e 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -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; } diff --git a/src/JackServerSettings.cpp b/src/JackServerSettings.cpp index 93c2de1..15ef696 100644 --- a/src/JackServerSettings.cpp +++ b/src/JackServerSettings.cpp @@ -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) diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp index da1f511..645e0e4 100644 --- a/src/JackServerSettings.hpp +++ b/src/JackServerSettings.hpp @@ -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); diff --git a/src/PiLatencyMain.cpp b/src/PiLatencyMain.cpp index c7e7147..e1616ac 100644 --- a/src/PiLatencyMain.cpp +++ b/src/PiLatencyMain.cpp @@ -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 [] \n\n"; - pp << "where 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 [] []\n\n"; + pp << "where is the name of an ALSA capture device and is the name of a playback device. " + "If 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 { diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp index af1be7e..2c02437 100644 --- a/src/PiPedalAlsa.cpp +++ b/src/PiPedalAlsa.cpp @@ -22,6 +22,7 @@ #include "alsa/asoundlib.h" #include "Lv2Log.hpp" #include +#include using namespace pipedal; @@ -111,6 +112,7 @@ std::vector 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 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 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 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 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 *result) diff --git a/src/PiPedalAlsa.hpp b/src/PiPedalAlsa.hpp index cf602b2..7a131a8 100644 --- a/src/PiPedalAlsa.hpp +++ b/src/PiPedalAlsa.hpp @@ -31,6 +31,8 @@ namespace pipedal { std::string longName_; std::vector sampleRates_; uint32_t minBufferSize_ = 0,maxBufferSize_ = 0; + bool supportsCapture_ = false; + bool supportsPlayback_ = false; bool isDummyDevice() const { return id_.starts_with("dummy:"); diff --git a/src/Storage.cpp b/src/Storage.cpp index 4498aa1..f503ed7 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -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(); diff --git a/valgrind.sh b/valgrind.sh index 58d1723..e721ccb 100755 --- a/valgrind.sh +++ b/valgrind.sh @@ -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 diff --git a/vite/src/pipedal/AlsaDeviceInfo.tsx b/vite/src/pipedal/AlsaDeviceInfo.tsx index 0cd10ff..360b9a5 100644 --- a/vite/src/pipedal/AlsaDeviceInfo.tsx +++ b/vite/src/pipedal/AlsaDeviceInfo.tsx @@ -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[] diff --git a/vite/src/pipedal/JackHostStatus.tsx b/vite/src/pipedal/JackHostStatus.tsx index 1f5c1f5..67239e9 100644 --- a/vite/src/pipedal/JackHostStatus.tsx +++ b/vite/src/pipedal/JackHostStatus.tsx @@ -189,4 +189,46 @@ export default class JackHostStatus { } } + static getDisplayViewNoCpu(label: string, status?: JackHostStatus): React.ReactNode { + if (!status) { + return (
+ {label} +   +
); + } + if (status.restarting) { + return ( +
+ {label} + + Restarting   + +
+ ); + + } else if (!status.active) { + return ( +
+ {label} + + + {status.errorMessage === "" ? "Audio\u00A0Stopped" : status.errorMessage}   + +
+ ); + } else { + let underrunError = status.msSinceLastUnderrun < 15 * 1000; + return ( +
+ {label} + + + XRuns: {status.underruns + ""}   + + +
+ ); + } + } + }; \ No newline at end of file diff --git a/vite/src/pipedal/JackServerSettings.tsx b/vite/src/pipedal/JackServerSettings.tsx index 9ece894..9924d4f 100644 --- a/vite/src/pipedal/JackServerSettings.tsx +++ b/vite/src/pipedal/JackServerSettings.tsx @@ -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; } } diff --git a/vite/src/pipedal/JackServerSettingsDialog.tsx b/vite/src/pipedal/JackServerSettingsDialog.tsx index ceff6e9..3a314fa 100644 --- a/vite/src/pipedal/JackServerSettingsDialog.tsx +++ b/vite/src/pipedal/JackServerSettingsDialog.tsx @@ -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 { open: boolean; @@ -78,13 +113,15 @@ export interface JackServerSettingsDialogProps extends WithStyles } 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 { + class extends ResizeResponsiveComponent { 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("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 ( + <> { - this.handleApply(); + this.handleOk(); }} + fullScreen={this.state.fullScreen} > -
- - Device - this.handleInputDeviceChanged(e)} + disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0} + style={{ width: 220 }} > - {(noDevices && !waitingForDevices) && - ( - No suitable devices. - ) - } - {((!noDevices) && !waitingForDevices) && ( - this.state.alsaDevices!.map((device) => - ( - {device.name} - ) - - ) - )} + {sortedDevices.filter(d => d.supportsCapture).map(d => ( + {d.name} + )) || Loading...} + + {/* Audio Output Device */} + + Output Device + + +
+ this.requestAlsaInfo()} aria-label="refresh-audio-devices"> + + +
- - Sample rate + + Sample rate
- - Buffer size + + Buffer size - - Buffers + + Buffers