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 4645e27..4a89bcd 100644 --- a/README.md +++ b/README.md @@ -57,20 +57,21 @@ https://github.com/user-attachments/assets/9a9fd0c6-78fc-4284-8b44-6a1929c00cc6 ### [Command-Line Configuration of PiPedal](https://rerdavies.github.io/pipedal/CommandLine.html) ### [Changing the Web Server Port](https://rerdavies.github.io/pipedal/ChangingTheWebServerPort.html) -%nbsp; +  ### [Using LV2 Audio Plugins](https://rerdavies.github.io/pipedal/UsingLv2Plugins.md) ### [Which LV2 Plugins does PiPedal support?](https://rerdavies.github.io/pipedal/WhichLv2PluginsAreSupported.html) ### [Support for LV2 Plugins with MOD User Interfaces](https://rerdavies.github.io/pipedal/ModUiSupport.html) -   +  ### [Building PiPedal from Source](https://rerdavies.github.io/pipedal/BuildingPiPedalFromSource.html) ### [Build Prerequisites](https://rerdavies.github.io/pipedal/BuildPrerequisites.html) ### [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 cd8e741..52a74e1 100644 --- a/docs/BuildPrerequisites.md +++ b/docs/BuildPrerequisites.md @@ -21,13 +21,13 @@ 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 \ libsdbus-c++-dev libzip-dev google-perftools \ libgoogle-perftools-dev \ - libpipewire-0.3-dev - + libpipewire-0.3-dev ### Installing Sources 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/signPackage.sh b/signPackage.sh index c26d857..4bfa09a 100755 --- a/signPackage.sh +++ b/signPackage.sh @@ -11,4 +11,8 @@ # to NOT do auto-updating (probably best). The build doesn't currently have a procedure for # knocking out Auto-Update from UpdaterSecurity.hpp. But I would happily accept a pull request # to bring such a feature back into mainline if you do it. -build/src/makeRelease + +# Build the release package and create a detached GPG signature. This +# command assumes you have already run makePackage.sh so the .deb file +# exists in the build directory. +./build/src/makeRelease diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index f285ac2..b911c59 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -618,6 +618,7 @@ namespace pipedal if (this->captureHandle) { + this->alsa_device_name = this->jackServerSettings.GetAlsaInputDevice(); AlsaConfigureStream( this->alsa_device_name, "capture", @@ -629,6 +630,7 @@ namespace pipedal } if (this->playbackHandle) { + this->alsa_device_name = this->jackServerSettings.GetAlsaOutputDevice(); AlsaConfigureStream( this->alsa_device_name, "playback", @@ -1224,7 +1226,7 @@ namespace pipedal int err; std::string inputName = jackServerSettings.GetAlsaInputDevice(); - std::string outputName = jackServerSettings.GetAlsaOutputDevice(); + std::string outputName = jackServerSettings.GetAlsaOutputDevice(); this->numberOfBuffers = jackServerSettings.GetNumberOfBuffers(); @@ -1234,6 +1236,7 @@ namespace pipedal try { + this->alsa_device_name = outputName; err = snd_pcm_open(&playbackHandle, outputName.c_str(), SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); if (err < 0) { @@ -1268,7 +1271,8 @@ namespace pipedal { snd_pcm_nonblock(playbackHandle, 0); } - + + this->alsa_device_name = inputName; err = snd_pcm_open(&captureHandle, inputName.c_str(), SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK); 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 de8a9dd..185e99e 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -517,7 +517,7 @@ namespace pipedal result.sampleRates_.push_back(48000); result.minBufferSize_ = 16; result.maxBufferSize_ = 1024; - result.supportsCapture_ = true; + result.supportsCapture_ = true; result.supportsPlayback_ = true; return result; } diff --git a/src/JackServerSettings.cpp b/src/JackServerSettings.cpp index bfbe8e0..8da634f 100644 --- a/src/JackServerSettings.cpp +++ b/src/JackServerSettings.cpp @@ -148,7 +148,7 @@ 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"); - // read new dual device flags, fallback on old -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 = ""; @@ -254,6 +254,7 @@ 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) // legacy field JSON_MAP_REFERENCE(JackServerSettings, alsaInputDevice) JSON_MAP_REFERENCE(JackServerSettings, alsaOutputDevice) JSON_MAP_REFERENCE(JackServerSettings, sampleRate) diff --git a/src/JackServerSettings.hpp b/src/JackServerSettings.hpp index 5fd9aad..d5e9058 100644 --- a/src/JackServerSettings.hpp +++ b/src/JackServerSettings.hpp @@ -31,8 +31,9 @@ namespace pipedal bool isOnboarding_ = true; bool isJackAudio_ = JACK_HOST ? true : false; bool rebootRequired_ = false; - std::string alsaInputDevice_; + std::string alsaInputDevice_; std::string alsaOutputDevice_; + std::string alsaDevice_; // legacy uint64_t sampleRate_ = 0; uint32_t bufferSize_ = 64; uint32_t numberOfBuffers_ = 3; @@ -41,14 +42,15 @@ namespace pipedal void SetAlsaInputDevice(const std::string &d){ alsaInputDevice_ = d; } void SetAlsaOutputDevice(const std::string &d){ alsaOutputDevice_ = d; } JackServerSettings(); - JackServerSettings( - const std::string alsaInputDevice, + JackServerSettings( + const std::string &alsaInputDevice, + const std::string &alsaOutputDevice, uint64_t sampleRate, uint32_t bufferSize, uint32_t numberOfBuffers) : valid_(true), alsaInputDevice_(alsaInputDevice), - alsaOutputDevice_(alsaInputDevice), // default same + alsaOutputDevice_(alsaOutputDevice), sampleRate_(sampleRate), bufferSize_(bufferSize), numberOfBuffers_(numberOfBuffers), @@ -57,10 +59,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 alsaInputDevice_; } - const std::string &GetAlsaOutputDevice() const { return alsaOutputDevice_; } + 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; @@ -76,14 +81,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) { @@ -94,9 +91,14 @@ namespace pipedal isOnboarding_ = value; } - bool Equals(const JackServerSettings &other) + bool Equals(const JackServerSettings &other) const { - return this->alsaInputDevice_ == other.alsaInputDevice_ && this->alsaOutputDevice_ == other.alsaOutputDevice_ && this->sampleRate_ == other.sampleRate_ && this->bufferSize_ == other.bufferSize_ && this->numberOfBuffers_ == other.numberOfBuffers_; + return this->alsaInputDevice_ == other.alsaInputDevice_ && + this->alsaOutputDevice_ == other.alsaOutputDevice_ && + this->alsaDevice_ == other.alsaDevice_ && + this->sampleRate_ == other.sampleRate_ && + this->bufferSize_ == other.bufferSize_ && + this->numberOfBuffers_ == other.numberOfBuffers_; } DECLARE_JSON_MAP(JackServerSettings); 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 b2f7f1a..9c4d3fe 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; @@ -106,7 +107,7 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() if (captureOk || playbackOk) { - snd_pcm_t *hDevice = captureOk ? captureDevice : playbackDevice; + snd_pcm_t *hDevice = captureOk ? captureDevice : playbackDevice; snd_pcm_hw_params_t *params = nullptr; err = snd_pcm_hw_params_malloc(¶ms); if (err == 0) @@ -117,29 +118,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; @@ -165,7 +179,7 @@ std::vector PiPedalAlsaDevices::GetAlsaDevices() { result.push_back(info); } - } + } else { if (getCachedDevice(info.name_, &info)) @@ -180,12 +194,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 5259837..7a131a8 100644 --- a/src/PiPedalAlsa.hpp +++ b/src/PiPedalAlsa.hpp @@ -31,7 +31,7 @@ namespace pipedal { std::string longName_; std::vector sampleRates_; uint32_t minBufferSize_ = 0,maxBufferSize_ = 0; - bool supportsCapture_ = false; + bool supportsCapture_ = false; bool supportsPlayback_ = false; bool isDummyDevice() const { diff --git a/src/Storage.cpp b/src/Storage.cpp index 29872c5..eb92448 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -1598,6 +1598,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/src/Uri.cpp b/src/Uri.cpp index e235b6a..8eb94db 100644 --- a/src/Uri.cpp +++ b/src/Uri.cpp @@ -457,7 +457,7 @@ std::string uri_builder::str() const { s << '?'; for (size_t i = 0; i < queries_.size(); ++i) { - if (i != 0) s << '&'; + if (i != 0) s << '&'; s << queries_[i].key << "="; HtmlHelper::encode_url_segment(s,queries_[i].value,true); } diff --git a/uninstall.sh b/uninstall.sh index d3b937f..7eedda3 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,4 +1,3 @@ -#/usr/bin/sudo /usr/bin/bash # CMake doesn't have an uninstall option, so remove files manually. export PREFIX=/usr 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/AboutDialog.tsx b/vite/src/pipedal/AboutDialog.tsx index 5dfd860..cf97669 100644 --- a/vite/src/pipedal/AboutDialog.tsx +++ b/vite/src/pipedal/AboutDialog.tsx @@ -62,7 +62,7 @@ const AboutDialog = class extends Component .then(jackStatus => { this.setState({ jackStatus: jackStatus }); }) - .catch(_error => { /* ignore*/ }); + .catch(_error => { /* ignore*/ }); } } @@ -116,12 +116,12 @@ const AboutDialog = class extends Component this.mounted = false; this.updateNotifications(); } - componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot: any): void { + componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot: any): void { super.componentDidUpdate?.(prevProps, prevState, snapshot); this.updateNotifications(); } - handleDialogClose(_e: SyntheticEvent) { + handleDialogClose(_e: SyntheticEvent) { this.props.onClose(); } diff --git a/vite/src/pipedal/AlsaDeviceInfo.tsx b/vite/src/pipedal/AlsaDeviceInfo.tsx index d1f97e0..4db9532 100644 --- a/vite/src/pipedal/AlsaDeviceInfo.tsx +++ b/vite/src/pipedal/AlsaDeviceInfo.tsx @@ -9,7 +9,7 @@ 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.supportsCapture = input.supportsCapture ? true : false; this.supportsPlayback = input.supportsPlayback ? true : false; return this; } diff --git a/vite/src/pipedal/AndroidHost.tsx b/vite/src/pipedal/AndroidHost.tsx index 18556ac..907b2b1 100644 --- a/vite/src/pipedal/AndroidHost.tsx +++ b/vite/src/pipedal/AndroidHost.tsx @@ -60,6 +60,7 @@ export class FakeAndroidHost implements AndroidHostInterface launchExternalUrl(_url:string): boolean { + return false; } private theme = 1; @@ -71,6 +72,7 @@ export class FakeAndroidHost implements AndroidHostInterface } setServerVersion(_serverVersion: string): void { // No-op for fake host + } private keepScreenOn = false; diff --git a/vite/src/pipedal/App.tsx b/vite/src/pipedal/App.tsx index 396d281..7686d2e 100644 --- a/vite/src/pipedal/App.tsx +++ b/vite/src/pipedal/App.tsx @@ -154,7 +154,7 @@ const theme = createTheme( /* make the selection state for MuiListItemButtons a smidgen darker (light theme only) */ MuiListItemButton: { styleOverrides: { - root: ({ theme }) => ({ + root: ({ theme }) => ({ '&.Mui-selected': { backgroundColor: 'rgba(0, 0, 0, 0.2)', // Adjust for desired darkness '&:hover': { 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 1263ad8..b69bd72 100644 --- a/vite/src/pipedal/JackServerSettings.tsx +++ b/vite/src/pipedal/JackServerSettings.tsx @@ -49,20 +49,44 @@ export default class JackServerSettings { isOnboarding: boolean = true; rebootRequired = false; isJackAudio = false; - alsaInputDevice: string = ""; - alsaOutputDevice: 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 inDev = this.alsaInputDevice.startsWith("hw:") ? this.alsaInputDevice .substring(3) : this.alsaInputDevice; - let outDev = this.alsaOutputDevice.startsWith("hw:") ? this.alsaOutputDevice.substring(3) : this.alsaOutputDevice; - return "In: "+inDev+" Out: "+outDev+" — Rate "+this.sampleRate+", "+this.bufferSize+"×"+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 a295bb5..2564c49 100644 --- a/vite/src/pipedal/JackServerSettingsDialog.tsx +++ b/vite/src/pipedal/JackServerSettingsDialog.tsx @@ -15,11 +15,9 @@ // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// 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,10 +38,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 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; @@ -54,12 +78,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) => @@ -71,6 +100,9 @@ const styles = (theme: Theme) => selectEmpty: { marginTop: theme.spacing(2), }, + inputLabel: { + whiteSpace: "nowrap" + } }); export interface JackServerSettingsDialogProps extends WithStyles { open: boolean; @@ -80,13 +112,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"; } @@ -159,11 +193,6 @@ function getBestBuffers( { return { bufferSize:bufferSize, numberOfBuffers: numberOfBuffers}; } - const device = inDevice || outDevice; - if (!device) - { - return { bufferSize:bufferSize, numberOfBuffers:numberOfBuffers}; - } // If the numberOfbuffers is fine as is, don't change the number of Buffers. // set default values. Otherwise, choose the best buffer count from available buffer counts. @@ -197,33 +226,57 @@ 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 >= minSize && 64*3 <= maxSize) - { - 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 = minSize/2; + + function tryPick(count: number): BufferSetting | undefined { + for (let bs of validBufferSizes) { + const counts = getValidBufferCountsMultiple(bs, inDevice, outDevice); + if (counts.indexOf(count) !== -1) { + if (bs * count >= minSize && bs * count <= maxSize) { + return { bufferSize: bs, numberOfBuffers: count }; + } + } + } + return undefined; + } + + let result = tryPick(4); + if (!result) result = tryPick(3); + if (!result) result = tryPick(2); + if (result) return result; + + // Fallback to previous behaviour. + if (64*3 >= minSize && 64*3 <= maxSize) { + return { bufferSize: 64, numberOfBuffers: 3 }; + } + bufferSize = minSize/2; numberOfBuffers = 4; - // otherwise, minBufferSize/2 x 2. - if (bufferSize*numberOfBuffers > maxSize) - { + 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; + 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); @@ -232,53 +285,130 @@ function isOkEnabled(jackServerSettings: JackServerSettings, alsaDevices?: AlsaD const validBufferCounts = getValidBufferCountsMultiple(jackServerSettings.bufferSize, inDevice, outDevice); if (validBufferCounts.indexOf(jackServerSettings.numberOfBuffers) === -1) return false; - const sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates); - if (sampleRates.indexOf(jackServerSettings.sampleRate) === -1) return false; - return true; } 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(); @@ -289,70 +419,94 @@ const JackServerSettingsDialog = withStyles( return result; } - let inDevice = alsaDevices.find(d => d.id === result.alsaInputDevice && d.supportsCapture); - let outDevice = alsaDevices.find(d => d.id === result.alsaOutputDevice && d.supportsPlayback); - if (!inDevice) { - inDevice = alsaDevices.find(d => d.supportsCapture); - if (inDevice) { + 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; - } - } - if (!outDevice) { - outDevice = alsaDevices.find(d => d.supportsPlayback); - if (outDevice) { 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; } - - if (result.sampleRate === 0) result.sampleRate = 48000; - let sampleRates = intersectArrays(inDevice.sampleRates, outDevice.sampleRates); - if (sampleRates.length === 0) sampleRates = inDevice.sampleRates; - 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; } + + 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; } - result.sampleRate = bestSr; let bestBuffers = getBestBuffers(inDevice, outDevice, result.bufferSize, result.numberOfBuffers); result.bufferSize = bestBuffers.bufferSize; result.numberOfBuffers = bestBuffers.numberOfBuffers; - - result.valid = !!result.alsaInputDevice && !!result.alsaOutputDevice; 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 | undefined { if (!this.state.alsaDevices) return undefined; for (let i = 0; i < this.state.alsaDevices.length; ++i) { @@ -371,6 +525,7 @@ const JackServerSettingsDialog = withStyles( if (!inDev && !outDev) return; let settings = this.state.jackServerSettings.clone(); settings.sampleRate = rate; + settings.valid = false; this.setState({ jackServerSettings: settings, @@ -386,7 +541,8 @@ const JackServerSettingsDialog = withStyles( if (!inDev && !outDev) return; let settings = this.state.jackServerSettings.clone(); settings.bufferSize = size; - let bestBufferSetting = getBestBuffers(inDev,outDev,settings.bufferSize,settings.numberOfBuffers,true); + settings.valid = false; + let bestBufferSetting = getBestBuffers(inDev,outDev,settings.bufferSize,settings.numberOfBuffers,true); settings.bufferSize = bestBufferSetting.bufferSize; settings.numberOfBuffers = bestBufferSetting.numberOfBuffers; @@ -404,6 +560,7 @@ const JackServerSettingsDialog = withStyles( if (!inDev && !outDev) return; let settings = this.state.jackServerSettings.clone(); settings.numberOfBuffers = bufferCount; + settings.valid = false; this.setState({ jackServerSettings: settings, @@ -413,96 +570,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 } }; - handleInputDeviceChanged(e: any) { - const d = e.target.value as string; - let s = this.state.jackServerSettings.clone(); - s.alsaInputDevice = d; - this.setState({ - jackServerSettings: s, - okEnabled: isOkEnabled(s, this.state.alsaDevices) - }); - } - - handleOutputDeviceChanged(e: any) { - const d = e.target.value as string; - let s = this.state.jackServerSettings.clone(); - s.alsaOutputDevice = d; - this.setState({ - jackServerSettings: s, - okEnabled: isOkEnabled(s, this.state.alsaDevices) - }); - } - 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 = () => { + if (this.ignoreClose) { + return; + } else { + if (this.originalJackServerSettings) { + // Revert any applied settings + this.applySettings(this.originalJackServerSettings); // Fire and forget + this.originalJackServerSettings = undefined; + } + } onClose(); }; - - let selectedInputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaInputDevice); + + const sortedDevices = sortDevices(this.state.alsaDevices ?? []); + + let selectedInputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaInputDevice); let selectedOutputDevice = this.getSelectedDevice(this.state.jackServerSettings.alsaOutputDevice); - let bufferSizes: number[] = getValidBufferSizesMultiple(selectedInputDevice, selectedOutputDevice); - let bufferCounts = getValidBufferCountsMultiple(this.state.jackServerSettings.bufferSize, selectedInputDevice, selectedOutputDevice); - let bufferSizeDisabled = !(selectedInputDevice || selectedOutputDevice); - let bufferCountDisabled = !(selectedInputDevice || selectedOutputDevice); - let sampleRates = selectedInputDevice && selectedOutputDevice ? + 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) - : (selectedInputDevice ? selectedInputDevice.sampleRates : (selectedOutputDevice ? 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} > -
- {/* Audio Input Device */} - - Input Device - this.handleInputDeviceChanged(e)} + disabled={!this.state.alsaDevices || this.state.alsaDevices.length === 0} + style={{ width: 220 }} + > + {sortedDevices.filter(d => d.supportsCapture).map(d => ( + {d.name} )) || Loading...} - - + + {/* Audio Output Device */} - - Output Device - - - this.requestAlsaInfo()} aria-label="refresh-audio-devices"> - - + + Output Device + + +
+ this.requestAlsaInfo()} aria-label="refresh-audio-devices"> + + +
- - Sample rate + + Sample rate
- - Buffer size + + Buffer size - - Buffers + + Buffers