diff --git a/PiPedalCommon/src/AlsaSequencer.cpp b/PiPedalCommon/src/AlsaSequencer.cpp index 71a636d..e4c11d1 100644 --- a/PiPedalCommon/src/AlsaSequencer.cpp +++ b/PiPedalCommon/src/AlsaSequencer.cpp @@ -34,6 +34,7 @@ #include "Finally.hpp" #include "Lv2Log.hpp" #include +#include // enumerate alsa sequencer ports @@ -96,6 +97,26 @@ namespace pipedal int queueId = -1; // Queue for real-time timestamps }; + class AlsaSequencerDeviceMonitorImpl : public AlsaSequencerDeviceMonitor + { + + public: + virtual ~AlsaSequencerDeviceMonitorImpl() override; + + virtual void StartMonitoring( + Callback &&onChangeCallback) override; + virtual void StopMonitoring() override; + + private: + int CreateInputQueue(snd_seq_t *seqHandle, int inPort); + + bool started = false; + void ServiceProc(); + std::unique_ptr serviceThread; + std::atomic terminateThread{false}; + Callback callback; + }; + }; using namespace impl; @@ -247,9 +268,10 @@ namespace pipedal size_t inputBufferSize = snd_seq_get_input_buffer_size(seqHandle); (void)inputBufferSize; - rc = snd_seq_set_input_buffer_size(seqHandle, 128*1024); - if (rc < 0) { - Lv2Log::warning("Failed resize the ALSA sequencer input buffer: %s",snd_strerror(rc)); + rc = snd_seq_set_input_buffer_size(seqHandle, 128 * 1024); + if (rc < 0) + { + Lv2Log::warning("Failed resize the ALSA sequencer input buffer: %s", snd_strerror(rc)); } snd_seq_set_client_name(seqHandle, "PiPedal"); @@ -584,12 +606,12 @@ namespace pipedal message.Set(0xF8); // MIDI Real Time Clock Tick break; #ifndef NDEBUG -#define MSG_DEBUG_LOG(x) \ - case x:\ - Lv2Log::debug("ALSA Sequencer Message" #x);\ - break; +#define MSG_DEBUG_LOG(x) \ + case x: \ + Lv2Log::debug("ALSA Sequencer Message" #x); \ + break; #else -#define MSG_DEBUG_LOG(x) +#define MSG_DEBUG_LOG(x) #endif MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_START) MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_EXIT) @@ -733,6 +755,7 @@ namespace pipedal Finally seq_finally([seq]() { snd_seq_close(seq); }); + snd_seq_addr_t sender, dest; dest.client = myClientId; dest.port = 0; @@ -797,6 +820,208 @@ namespace pipedal } } + AlsaSequencerDeviceMonitor::ptr AlsaSequencerDeviceMonitor::Create() + { + return std::make_shared(); + } + + void AlsaSequencerDeviceMonitorImpl::StartMonitoring( + Callback &&onChangeCallback) + { + started = true; + this->callback = std::move(onChangeCallback); + this->serviceThread = std::make_unique( + [this]() + { + ServiceProc(); + }); + } + void AlsaSequencerDeviceMonitorImpl::ServiceProc() + { + int err; + snd_seq_t *seqHandle; + + err = snd_seq_open(&seqHandle, "default", SND_SEQ_OPEN_DUPLEX, 0); + + Finally seq_finally( + [seqHandle]() + { + snd_seq_close(seqHandle); + }); + if (err < 0) + { + Lv2Log::error("Error opening ALSA Device Monitor sequencer: %s", snd_strerror(err)); + return; + } + snd_seq_set_client_name(seqHandle, "Device Monitor"); + + int inPort = snd_seq_create_simple_port( + seqHandle, "PiPedal:portMonitor", + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_APPLICATION); + if (inPort < 0) + { + Lv2Log::error("Error creating ALSA Device Monitor port: %s", snd_strerror(inPort)); + return; + } + Finally inPort_finaly { + [seqHandle, inPort]() + { + snd_seq_delete_port(seqHandle, inPort); + } + }; + + // Set client name + + int queueId = CreateInputQueue(seqHandle,inPort); + if (queueId < 0) + { + Lv2Log::error("Error creating ALSA Device Monitor queue: %s", snd_strerror(queueId)); + return; + } + Finally queue_finally( + [seqHandle, queueId]() + { + snd_seq_free_queue(seqHandle, queueId); + }); + + // Subscribe to system announcements + snd_seq_port_subscribe_t *subscription; + snd_seq_port_subscribe_alloca(&subscription); + snd_seq_addr_t sender, dest; + + sender.client = SND_SEQ_CLIENT_SYSTEM; + sender.port = SND_SEQ_PORT_SYSTEM_ANNOUNCE; + dest.client = snd_seq_client_id(seqHandle); + dest.port = inPort; + + snd_seq_port_subscribe_set_sender(subscription, &sender); + snd_seq_port_subscribe_set_dest(subscription, &dest); + err = snd_seq_subscribe_port(seqHandle, subscription); + if (err < 0) + { + Lv2Log::error("Failed to subscribe to ALSA sequencer announcements: %s", snd_strerror(err)); + return; + } + + // Create poll descriptors + std::vector pollFds; + + snd_seq_nonblock(seqHandle, 1); // Set sequencer to non-blocking mode + while (!terminateThread) + { + int nPollFds = snd_seq_poll_descriptors_count(seqHandle, POLLIN); + pollFds.resize(nPollFds); + snd_seq_poll_descriptors(seqHandle, pollFds.data(), nPollFds, POLLIN); + + // Poll for events + if (poll(pollFds.data(), nPollFds, 100) > 0) + { + snd_seq_event_t *event; + while (snd_seq_event_input(seqHandle, &event) > 0) + { + if (event->type == SND_SEQ_EVENT_CLIENT_START) + { + // Get the client name for logging/debugging + snd_seq_client_info_t *client_info; + snd_seq_client_info_alloca(&client_info); + if (snd_seq_get_any_client_info(seqHandle, event->data.addr.client, client_info) >= 0) { + std::string clientName = snd_seq_client_info_get_name(client_info); + callback(MonitorAction::DeviceAdded, event->data.addr.client, clientName); + } + } + else if (event->type == SND_SEQ_EVENT_CLIENT_EXIT) + { + callback(MonitorAction::DeviceRemoved, event->data.addr.client,""); + } + snd_seq_free_event(event); + } + } + } + + return; + } + + void AlsaSequencerDeviceMonitorImpl::StopMonitoring() + { + if (started) + { + started = false; + terminateThread = true; + serviceThread = nullptr; // (joins) + } + } + + AlsaSequencerDeviceMonitorImpl::~AlsaSequencerDeviceMonitorImpl() + { + StopMonitoring(); + } + + int AlsaSequencerDeviceMonitorImpl::CreateInputQueue(snd_seq_t *seqHandle, int inPort) + { + if (!seqHandle) + { + throw std::runtime_error("ALSA sequencer not initialized"); + } + + // Create a new queue if we don't have one yet + int queueId = -1; + { + queueId = snd_seq_alloc_named_queue(seqHandle, "PiPedal Device Monitor Queue"); + if (queueId < 0) + { + throw std::runtime_error(SS("Failed to create ALSA queue: " << snd_strerror(queueId))); + } + + // Set queue timing to real-time mode + snd_seq_queue_tempo_t *tempo; + snd_seq_queue_tempo_alloca(&tempo); + snd_seq_queue_tempo_set_tempo(tempo, 120); // 120 BPM default + snd_seq_queue_tempo_set_ppq(tempo, 96); // 96 ticks per quarter note + + int rc = snd_seq_set_queue_tempo(seqHandle, queueId, tempo); + if (rc < 0) + { + snd_seq_free_queue(seqHandle, queueId); + throw std::runtime_error(SS("Failed to set queue tempo: " << snd_strerror(rc))); + } + + // Start the queue + rc = snd_seq_start_queue(seqHandle, queueId, nullptr); + if (rc < 0) + { + snd_seq_free_queue(seqHandle, queueId); + throw std::runtime_error(SS("Failed to start queue: " << snd_strerror(rc))); + } + + // Set the queue for input timestamping + snd_seq_port_info_t *port_info; + snd_seq_port_info_alloca(&port_info); + + rc = snd_seq_get_port_info(seqHandle, inPort, port_info); + if (rc < 0) + { + snd_seq_free_queue(seqHandle, queueId); + throw std::runtime_error(SS("Failed to get port info: " << snd_strerror(rc))); + } + + // Enable timestamping on the input port + snd_seq_port_info_set_timestamping(port_info, 1); + snd_seq_port_info_set_timestamp_real(port_info, 1); + snd_seq_port_info_set_timestamp_queue(port_info, queueId); + + rc = snd_seq_set_port_info(seqHandle, inPort, port_info); + if (rc < 0) + { + snd_seq_free_queue(seqHandle, queueId); + throw std::runtime_error(SS("Failed to set port timestamping: " << snd_strerror(rc))); + } + return queueId; + } + + return queueId; + } + JSON_MAP_BEGIN(AlsaSequencerPortSelection) JSON_MAP_REFERENCE(AlsaSequencerPortSelection, id) JSON_MAP_REFERENCE(AlsaSequencerPortSelection, name) diff --git a/PiPedalCommon/src/WifiConfigSettings.cpp b/PiPedalCommon/src/WifiConfigSettings.cpp index ddeb005..9b574ac 100644 --- a/PiPedalCommon/src/WifiConfigSettings.cpp +++ b/PiPedalCommon/src/WifiConfigSettings.cpp @@ -89,7 +89,6 @@ bool WifiConfigSettings::ValidateCountryCode(const std::string &text) return std::isalpha(text[0]) && std::isalpha(text[1]); } - void WifiConfigSettings::Load() { try @@ -115,9 +114,9 @@ void WifiConfigSettings::Load() } static void openWithPerms( - pipedal::ofstream_synced &f, - const std::filesystem::path &path, - std::filesystem::perms perms = + pipedal::ofstream_synced &f, + const std::filesystem::path &path, + std::filesystem::perms perms = std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | std::filesystem::perms::group_write) { @@ -132,13 +131,16 @@ static void openWithPerms( f.open(path); f.close(); } - try { + try + { // set the perms. std::filesystem::permissions( path, perms, - std::filesystem::perm_options::replace); - } catch (const std::exception&) { + std::filesystem::perm_options::replace); + } + catch (const std::exception &) + { Lv2Log::warning(SS("Failed to set permissions on" << path << ".")); } } @@ -147,10 +149,10 @@ static void openWithPerms( f.open(path); } -void WifiConfigSettings::Save() +void WifiConfigSettings::Save() { - WifiConfigSettings newSettings {*this}; - + WifiConfigSettings newSettings{*this}; + // sync legacy settings, just in case i don't know what. newSettings.mdnsName_ = newSettings.hotspotName_; newSettings.enable_ = newSettings.IsEnabled(); @@ -163,12 +165,12 @@ void WifiConfigSettings::Save() newSettings.password_ = oldSettings.password_; } newSettings.hasSavedPassword_ = newSettings.hasPassword_; - + try { ofstream_synced f; - openWithPerms(f,CONFIG_PATH); - json_writer writer(f,false); + openWithPerms(f, CONFIG_PATH); + json_writer writer(f, false); writer.write(&newSettings); } catch (const std::exception &e) @@ -300,17 +302,14 @@ namespace pipedal::priv connection["ipv4"]["method"] = sdbus::Variant(std::string("shared")); connection["ipv6"]["method"] = sdbus::Variant(std::string("ignore")); - - connection["ipv4"]["address-data"] = sdbus::Variant(std::vector>{{ - {"address", sdbus::Variant("192.168.4.1")}, - {"prefix", sdbus::Variant(uint32_t(24))} - }}); + connection["ipv4"]["address-data"] = sdbus::Variant(std::vector>{{{"address", sdbus::Variant("192.168.4.1")}, + {"prefix", sdbus::Variant(uint32_t(24))}}}); connection["ipv4"]["dhcp-send-hostname"] = sdbus::Variant(true); connection["ipv4"]["dhcp-hostname"] = sdbus::Variant("raspberrypi"); connection["ipv4"]["dhcp-options"] = sdbus::Variant(std::vector{ "option:classless-static-route,192.168.4.0/24,0.0.0.0,0.0.0.0/0,192.168.4.1" //"option:classless-static-route,192.168.4.0/24,192.168.4.1" - }); + }); auto connection_path = AddConnection(connection); return connection_path; @@ -326,7 +325,6 @@ namespace pipedal::priv return active_connection_path; } - void RemoveConnection(const std::string &connection) { auto connection_proxy = sdbus::createProxy( @@ -429,7 +427,6 @@ namespace pipedal::priv }; } - JSON_MAP_BEGIN(WifiConfigSettings) // v0 JSON_MAP_REFERENCE(WifiConfigSettings, valid) @@ -560,52 +557,51 @@ bool WifiConfigSettings::ValidateChannel(const std::string &countryCode, const s return true; } -static const char* trueValues[] { +static const char *trueValues[]{ "true", "on", "yes" "1", - nullptr -}; -static const char* falseValues[] { + nullptr}; +static const char *falseValues[]{ "false", "off", "no", "0", - nullptr -}; + nullptr}; -static bool Matches(const std::string&value, const char**matches) +static bool Matches(const std::string &value, const char **matches) { - while(*matches) + while (*matches) { - if (value == *matches) return true; + if (value == *matches) + return true; ++matches; } return false; } -static bool TryStringToBool(const std::string &value, bool*outputValue) +static bool TryStringToBool(const std::string &value, bool *outputValue) { - if (Matches(value,trueValues)) + if (Matches(value, trueValues)) { *outputValue = true; return true; } - if (Matches(value, falseValues)) { + if (Matches(value, falseValues)) + { *outputValue = false; return true; } *outputValue = false; return false; - } -static WifiConfigSettings::ssid_t readSsid(std::istream&ss) +static WifiConfigSettings::ssid_t readSsid(std::istream &ss) { using ssid_t = WifiConfigSettings::ssid_t; char c; - + ssid_t result; while (ss.peek() == ' ') { @@ -624,28 +620,33 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss) break; } ss >> c; - switch (c) { - case 'n': - result.push_back((uint8_t)'\n'); - break; - case 'r': - result.push_back((uint8_t)'\r'); - break; - case 't': - result.push_back((uint8_t)'\t'); - break; - case 'b': - result.push_back((uint8_t)'\b'); - break; - default: - result.push_back((uint8_t)c); - break; + switch (c) + { + case 'n': + result.push_back((uint8_t)'\n'); + break; + case 'r': + result.push_back((uint8_t)'\r'); + break; + case 't': + result.push_back((uint8_t)'\t'); + break; + case 'b': + result.push_back((uint8_t)'\b'); + break; + default: + result.push_back((uint8_t)c); + break; } - } else { + } + else + { result.push_back((uint8_t)c); } } - } else { + } + else + { while (!ss.eof()) { if (c == ':') @@ -662,7 +663,7 @@ static WifiConfigSettings::ssid_t readSsid(std::istream&ss) } return result; } -static std::vector stringToSsidArray(const std::string&value) +static std::vector stringToSsidArray(const std::string &value) { using ssid_t = WifiConfigSettings::ssid_t; std::istringstream ss(value); @@ -671,7 +672,7 @@ static std::vector stringToSsidArray(const std::stri while (true) { ssid_t ssid = readSsid(ss); - if (ssid.size() == 0) + if (ssid.size() == 0) { break; } @@ -683,16 +684,15 @@ static std::vector stringToSsidArray(const std::stri } } return result; - } void WifiConfigSettings::ParseArguments( const std::vector &argv, HotspotAutoStartMode startMode, const std::string homeNetworkSsid - ) +) { - this->valid_ = false ; + this->valid_ = false; if (argv.size() != 4) { throw invalid_argument("Invalid number of arguments."); @@ -714,7 +714,6 @@ void WifiConfigSettings::ParseArguments( this->hasPassword_ = this->password_.length() != 0; this->hasSavedPassword_ = oldSettings.hasSavedPassword_; - if (!ValidateCountryCode(this->countryCode_)) { throw invalid_argument("Invalid country code."); @@ -723,7 +722,7 @@ void WifiConfigSettings::ParseArguments( throw invalid_argument("Hotspot name is too long."); if (this->hotspotName_.length() < 1) throw invalid_argument("Hotspot name is too short."); - + if (this->password_.size() != 0 && this->password_.size() < 8) throw invalid_argument("Passphrase must be at least 8 characters long."); @@ -742,12 +741,12 @@ void WifiConfigSettings::ParseArguments( this->valid_ = true; } -bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) const +bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings &other) const { return !( this->valid_ == other.valid_ && - //this->rebootRequired_ == other.rebootRequired_ && - //this->enable_ == other.enable_ && + // this->rebootRequired_ == other.rebootRequired_ && + // this->enable_ == other.enable_ && this->countryCode_ == other.countryCode_ && this->hotspotName_ == other.hotspotName_ && this->hasPassword_ == other.hasPassword_ && @@ -756,20 +755,18 @@ bool WifiConfigSettings::ConfigurationChanged(const WifiConfigSettings&other) co this->homeNetwork_ == other.homeNetwork_ && this->autoStartMode_ == other.autoStartMode_ && - this->hasSavedPassword_ == other.hasSavedPassword_ - ); - + this->hasSavedPassword_ == other.hasSavedPassword_); } -bool WifiConfigSettings::operator==(const WifiConfigSettings&other) const +bool WifiConfigSettings::operator==(const WifiConfigSettings &other) const { return !ConfigurationChanged(other) && this->wifiWarningGiven_ == other.wifiWarningGiven_; } -static bool CanSeeHomeNetwork(const std::string&home, const std::vector&availableNetworks) +static bool CanSeeHomeNetwork(const std::string &home, const std::vector &availableNetworks) { - for (const auto &availableNetwork: availableNetworks) + for (const auto &availableNetwork : availableNetworks) { - if (availableNetwork == home) + if (availableNetwork == home) { return true; } @@ -778,38 +775,39 @@ static bool CanSeeHomeNetwork(const std::string&home, const std::vector &availableRememberedNetworks, const std::vector &availableNetworks) { - if ((!this->valid_)) + if ((!this->valid_)) return false; - + HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_; switch (autoStartMode) { - case HotspotAutoStartMode::Never: - default: - return false; + case HotspotAutoStartMode::Never: + default: + return false; - case HotspotAutoStartMode::NoEthernetConnection: - return !ethernetConnected; - case HotspotAutoStartMode::NotAtHome: - return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks); - case HotspotAutoStartMode::NoRememberedWifiConections: - return availableRememberedNetworks.size() == 0; - case HotspotAutoStartMode::Always: - return true; + case HotspotAutoStartMode::NoEthernetConnection: + return !ethernetConnected; + case HotspotAutoStartMode::NotAtHome: + return !CanSeeHomeNetwork(this->homeNetwork_, availableNetworks); + case HotspotAutoStartMode::NoRememberedWifiConections: + return availableRememberedNetworks.size() == 0; + case HotspotAutoStartMode::Always: + return true; } } std::string pipedal::ssidToString(const std::vector &ssid) { std::stringstream s; - for (auto v: ssid) + for (auto v : ssid) { - if (v == 0) break; // breaks for some arguably legal ssids, but I can live with that. + if (v == 0) + break; // breaks for some arguably legal ssids, but I can live with that. s << (char)v; } return s.str(); @@ -819,21 +817,43 @@ std::vector pipedal::ssidToStringVector(const std::vector result; result.reserve(ssids.size()); - for (const std::vector &ssid: ssids) + for (const std::vector &ssid : ssids) { result.push_back(ssidToString(ssid)); } return result; } bool WifiConfigSettings::WantsHotspot( - bool ethernetConnected, + bool ethernetConnected, const std::vector> &availableRememberedNetworks, // remembered networks that are currently visible - const std::vector> &availableNetworks // all visible networks. - ) + const std::vector> &availableNetworks // all visible networks. +) { std::vector sAvailableRememberedNetworks = ssidToStringVector(availableRememberedNetworks); std::vector sAvailableNetworks = ssidToStringVector(availableNetworks); - return WantsHotspot(ethernetConnected,sAvailableRememberedNetworks,sAvailableNetworks); + return WantsHotspot(ethernetConnected, sAvailableRememberedNetworks, sAvailableNetworks); +} + +bool WifiConfigSettings::NeedsWifi() const +{ + HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_; + return autoStartMode != HotspotAutoStartMode::Never; } +bool WifiConfigSettings::NeedsScan() const +{ + HotspotAutoStartMode autoStartMode = (HotspotAutoStartMode)this->autoStartMode_; + switch (autoStartMode) + { + case HotspotAutoStartMode::Never: + case HotspotAutoStartMode::NoEthernetConnection: + case HotspotAutoStartMode::Always: + default: + return false; + + case HotspotAutoStartMode::NotAtHome: + case HotspotAutoStartMode::NoRememberedWifiConections: + return true; + } +} diff --git a/PiPedalCommon/src/include/AlsaSequencer.hpp b/PiPedalCommon/src/include/AlsaSequencer.hpp index c8cbbca..f9a9a78 100644 --- a/PiPedalCommon/src/include/AlsaSequencer.hpp +++ b/PiPedalCommon/src/include/AlsaSequencer.hpp @@ -26,6 +26,7 @@ #include #include #include +#include #include "json.hpp" namespace pipedal @@ -331,6 +332,26 @@ namespace pipedal virtual void RemoveAllConnections() = 0; }; + + class AlsaSequencerDeviceMonitor { + protected: + AlsaSequencerDeviceMonitor() {} + public: + virtual ~AlsaSequencerDeviceMonitor() {} + + using self = AlsaSequencerDeviceMonitor; + using ptr = std::shared_ptr; + static ptr Create(); + enum class MonitorAction { + DeviceAdded, + DeviceRemoved + }; + using Callback = std::function; + virtual void StartMonitoring(Callback &&onChangeCallback) = 0; + virtual void StopMonitoring() = 0; + + }; + std::string RawMidiIdToSequencerId(const std::vector &seqDevices, const std::string &rawMidiId); } \ No newline at end of file diff --git a/PiPedalCommon/src/include/WifiConfigSettings.hpp b/PiPedalCommon/src/include/WifiConfigSettings.hpp index 44492aa..ace67af 100644 --- a/PiPedalCommon/src/include/WifiConfigSettings.hpp +++ b/PiPedalCommon/src/include/WifiConfigSettings.hpp @@ -95,6 +95,10 @@ namespace pipedal { bool operator==(const WifiConfigSettings&other) const; bool ConfigurationChanged(const WifiConfigSettings&other) const; + bool WantsHotspot(bool ethernetConnected) + { + return WantsHotspot(ethernetConnected, std::vector{}, std::vector{}); + } bool WantsHotspot( bool ethernetConnected, const std::vector &availableRememberedNetworks, // remembered networks that are currently visible @@ -105,7 +109,8 @@ namespace pipedal { const std::vector> &availableRememberedNetworks, // remembered networks that are currently visible const std::vector> &availableNetworks // all visible networks. ); - + bool NeedsScan() const; + bool NeedsWifi() const; public: DECLARE_JSON_MAP(WifiConfigSettings); }; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 507ec8d..a255b9f 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -390,6 +390,7 @@ class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchW { private: AlsaSequencer::ptr alsaSequencer; + AlsaSequencerDeviceMonitor::ptr alsaDeviceMonitor; void OnWritePatchPropertyBuffer( PatchPropertyWriter::Buffer *); @@ -1252,9 +1253,21 @@ public: cpuTemperatureMonitor = CpuTemperatureMonitor::Get(); this->alsaSequencer = AlsaSequencer::Create(); + this->alsaDeviceMonitor = AlsaSequencerDeviceMonitor::Create(); + + this->alsaDeviceMonitor->StartMonitoring( + [this]( + AlsaSequencerDeviceMonitor::MonitorAction action, + int client, + const std::string &clientName) + { + HandleAlsaSequencerDevicesChanged(action, client, clientName); + } + ); } virtual ~AudioHostImpl() { + alsaDeviceMonitor->StopMonitoring(); Close(); CleanRestartThreads(true); audioDriver = nullptr; @@ -1273,7 +1286,21 @@ public: { return this->sampleRate; } - + void HandleAlsaSequencerDevicesChanged( + AlsaSequencerDeviceMonitor::MonitorAction action, int client, const std::string &clientName) + { + if (pNotifyCallbacks) + { + if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceRemoved) + { + pNotifyCallbacks->OnAlsaSequencerDeviceRemoved(client); + } + else if (action == AlsaSequencerDeviceMonitor::MonitorAction::DeviceAdded) + { + pNotifyCallbacks->OnAlsaSequencerDeviceAdded(client,clientName); + } + } + } void HandleAudioTerminatedAbnormally() { Lv2Log::error("Audio processing terminated unexpectedly."); diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index a7edf89..16a9fb3 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -179,6 +179,8 @@ namespace pipedal virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) = 0; virtual void OnAlsaDriverTerminatedAbnormally() = 0; + virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) = 0; + virtual void OnAlsaSequencerDeviceRemoved(int client) = 0; }; class JackHostStatus diff --git a/src/HotspotManager.cpp b/src/HotspotManager.cpp index 0e055c6..5a936d2 100644 --- a/src/HotspotManager.cpp +++ b/src/HotspotManager.cpp @@ -80,7 +80,7 @@ namespace pipedal::impl bool hasWifi = false; HasWifiListener hasWifiListener; void SetHasWifi(bool hasWifi); - virtual bool GetHasWifi() override ; + virtual bool GetHasWifi() override; void onClose(); void onError(const std::string &message); @@ -347,7 +347,7 @@ void HotspotManagerImpl::onStartMonitoring() this->networkManager = NetworkManager::Create(dbusDispatcher); - if (!networkManager->WirelessEnabled()) + if (!networkManager->WirelessEnabled() && wifiConfigSettings.NeedsWifi()) { networkManager->WirelessEnabled(true); } @@ -483,6 +483,7 @@ void HotspotManagerImpl::onReload() // force a reload. StopHotspot(); SetState(State::Monitoring); + StartScanTimer(); // or stop it as the case may be. MaybeStartHotspot(); return; } @@ -758,16 +759,26 @@ void HotspotManagerImpl::MaybeStartHotspot() // devices are transitioning. Do nothing. return; } - std::vector allAccessPoints = GetAllAccessPoints(); - std::vector> allAccessPointSsids = GetAccessPointSsids(allAccessPoints); - std::vector> connectableSsids = GetKnownVisibleAccessPoints(allAccessPointSsids); // all the ssids currently visible for which we have credentials. - this->UpdateKnownNetworks(connectableSsids, allAccessPoints); + bool wantsHotspot; - bool wantsHotspot = this->wifiConfigSettings.WantsHotspot( + if (wifiConfigSettings.NeedsScan()) { - this->ethernetConnected, connectableSsids, allAccessPointSsids); + std::vector allAccessPoints = GetAllAccessPoints(); + std::vector> allAccessPointSsids = GetAccessPointSsids(allAccessPoints); + std::vector> connectableSsids = GetKnownVisibleAccessPoints(allAccessPointSsids); // all the ssids currently visible for which we have credentials. + this->UpdateKnownNetworks(connectableSsids, allAccessPoints); + + wantsHotspot = this->wifiConfigSettings.WantsHotspot( + this->ethernetConnected, connectableSsids, allAccessPointSsids); + + + } else { + wantsHotspot = this->wifiConfigSettings.WantsHotspot( + this->ethernetConnected); + } + if (this->state == State::Monitoring && wantsHotspot) { StartHotspot(); @@ -996,8 +1007,14 @@ void HotspotManagerImpl::ScanNow() try { - Lv2Log::debug("Scanning"); - wlanWirelessDevice->RequestScan(options); + if (this->wifiConfigSettings.NeedsScan()) + { + + Lv2Log::debug("Scanning"); + wlanWirelessDevice->RequestScan(options); + } else { + return; + } } catch (const std::exception &e) { @@ -1036,7 +1053,8 @@ void HotspotManagerImpl::SetHasWifiListener(HasWifiListener &&listener) this->hasWifiListener(this->hasWifi); } } -bool HotspotManagerImpl::GetHasWifi() { +bool HotspotManagerImpl::GetHasWifi() +{ std::lock_guard lock{this->networkChangingListenerMutex}; return hasWifi; } @@ -1112,17 +1130,20 @@ static bool IsNetworkManagerRunning() return gUsingNetworkManager; } bool bResult = false; - auto result = sysExecForOutput("systemctl","is-active NetworkManager"); + auto result = sysExecForOutput("systemctl", "is-active NetworkManager"); if (result.exitCode == EXIT_SUCCESS) { - std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t")+1); + std::string text = result.output.erase(result.output.find_last_not_of(" \n\r\t") + 1); if (text == "active") { bResult = true; - } else if (text == "inactive") + } + else if (text == "inactive") { bResult = false; - } else { + } + else + { throw std::runtime_error(SS("UsingNetworkManager: unexpected result (" << text << ")")); } gNetworkManagerTestExecuted = true; @@ -1134,19 +1155,17 @@ static bool IsNetworkManagerRunning() return false; } - bool HotspotManager::HasWifiDevice() { // use procfs to decide this, as NetworkManager may not be available yet. - if ( !(get_wireless_interfaces_sysfs().empty()) ) + if (!(get_wireless_interfaces_sysfs().empty())) { return false; } - if (!IsNetworkManagerRunning()) + if (!IsNetworkManagerRunning()) { return false; } return true; - } diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 4ae775c..b5d307b 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -292,16 +292,12 @@ void PiPedalModel::Load() this->audioHost->SetNotificationCallbacks(this); - - this->systemMidiBindings = storage.GetSystemMidiBindings(); this->audioHost->SetSystemMidiBindings(this->systemMidiBindings); audioHost->SetAlsaSequencerConfiguration(storage.GetAlsaSequencerConfiguration()); - - if (configuration.GetMLock()) { #ifndef NO_MLOCK @@ -515,7 +511,6 @@ void PiPedalModel::FireBanksChanged(int64_t clientId) } } - void PiPedalModel::FirePedalboardChanged(int64_t clientId, bool loadAudioThread) { if (loadAudioThread) @@ -601,14 +596,11 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo UpdateVst3Settings(pedalboard); - - Lv2PedalboardErrorList errorMessages; std::shared_ptr lv2Pedalboard{ - this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages) - }; + this->pluginHost.UpdateLv2PedalboardStructure(pedalboard, this->lv2Pedalboard.get(), errorMessages)}; this->lv2Pedalboard = lv2Pedalboard; - + // apply the error messages to the lv2Pedalboard. // return true if the error messages have changed audioHost->SetPedalboard(lv2Pedalboard); @@ -620,9 +612,7 @@ void PiPedalModel::UpdateCurrentPedalboard(int64_t clientId, Pedalboard &pedalbo UpdateRealtimeVuSubscriptions(); UpdateRealtimeMonitorPortSubscriptions(); - - - this->FirePedalboardChanged(clientId,false); + this->FirePedalboardChanged(clientId, false); this->SetPresetChanged(clientId, true); } } @@ -1406,6 +1396,38 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver) } } +void PiPedalModel::OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) +{ + std::lock_guard lock(mutex); + auto alsaSequencerConfiguration = this->storage.GetAlsaSequencerConfiguration(); + std::string key = "seq:" + clientName; + bool interested = false; + for (const auto &port : alsaSequencerConfiguration.connections()) + { + if (port.id().starts_with(key)) + { + interested = true; + break; + } + } + if (interested) + { + Post( + [this] + { + // reconfigure connections. + std::lock_guard lock(this->mutex); + if (this->audioHost) { + this->audioHost->SetAlsaSequencerConfiguration(this->storage.GetAlsaSequencerConfiguration()); + } + }); + } +} +void PiPedalModel::OnAlsaSequencerDeviceRemoved(int client) +{ + // no action required. +} + void PiPedalModel::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) { std::lock_guard lock(mutex); @@ -1424,7 +1446,6 @@ void PiPedalModel::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguratio subscriber->OnAlsaSequencerConfigurationChanged(alsaSequencerConfiguration); } } - } AlsaSequencerConfiguration PiPedalModel::GetAlsaSequencerConfiguration() { @@ -1441,14 +1462,11 @@ std::vector PiPedalModel::GetAlsaSequencerPorts() result.push_back(AlsaSequencerPortSelection{ port.id, port.name, - port.displaySortOrder - }); + port.displaySortOrder}); } return result; } - - void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection) { { @@ -1667,7 +1685,7 @@ void PiPedalModel::SendSetPatchProperty( std::lock_guard lock(mutex); if (!audioHost) { - onError("Audio not running."); + onError("Audio not running."); return; } @@ -1678,8 +1696,9 @@ void PiPedalModel::SendSetPatchProperty( std::shared_ptr pluginInfo = GetPluginInfo(pedalboardItem->uri_); auto pipedalUi = pluginInfo->piPedalUI(); auto fileProperty = pipedalUi->GetFileProperty(propertyUri); - if (fileProperty && value.is_string()) { - + if (fileProperty && value.is_string()) + { + json_variant abstractPath = pluginHost.AbstractPath(value); std::ostringstream ss; json_writer writer(ss); @@ -1729,12 +1748,11 @@ void PiPedalModel::SendSetPatchProperty( }}; LV2_URID urid = this->pluginHost.GetLv2Urid(propertyUri.c_str()); - size_t sampleTimeout = 0.5*audioHost->GetSampleRate(); + size_t sampleTimeout = 0.5 * audioHost->GetSampleRate(); RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( onRequestComplete, clientId, instanceId, urid, atomValue, nullptr, onError, - sampleTimeout - ); + sampleTimeout); outstandingParameterRequests.push_back(request); if (this->audioHost) @@ -1802,10 +1820,10 @@ void PiPedalModel::SendGetPatchProperty( { onError("Audio stopped."); } - size_t sampleTimeout = 0.3*audioHost->GetSampleRate(); + size_t sampleTimeout = 0.3 * audioHost->GetSampleRate(); RealtimePatchPropertyRequest *request = new RealtimePatchPropertyRequest( onRequestComplete, - clientId, instanceId, urid, onSuccess, onError,sampleTimeout); + clientId, instanceId, urid, onSuccess, onError, sampleTimeout); outstandingParameterRequests.push_back(request); this->audioHost->sendRealtimeParameterRequest(request); @@ -1969,7 +1987,9 @@ void PiPedalModel::UpdateDefaults(PedalboardItem *pedalboardItem, std::unordered { pedalboardItem->midiChannelBinding(MidiChannelBinding::DefaultForMissingValue()); } - } else { + } + else + { if (pedalboardItem->midiChannelBinding()) { pedalboardItem->midiChannelBinding(std::optional()); // clear it. @@ -2245,22 +2265,23 @@ void PiPedalModel::OnNotifyMidiListen(uint8_t cc0, uint8_t cc1, uint8_t cc2) { std::lock_guard lock(mutex); bool isNote = (cc0 & 0xF0) == 0x90; // Note On - if (isNote && cc2 == 0) { + if (isNote && cc2 == 0) + { return; // Note off. Oopsie. } - bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change - if (!isNote && !isControl) { + bool isControl = (cc0 & 0xF0) == 0xB0; // Control Change + if (!isNote && !isControl) + { return; // Not a note on or control change. } - - for (int i = 0; i < midiEventListeners.size(); ++i) + for (int i = 0; i < midiEventListeners.size(); ++i) { auto &listener = midiEventListeners[i]; auto subscriber = this->GetNotificationSubscriber(listener.clientId); if (subscriber) { - subscriber->OnNotifyMidiListener(listener.clientHandle, cc0,cc1,cc2); + subscriber->OnNotifyMidiListener(listener.clientHandle, cc0, cc1, cc2); } else { @@ -2345,6 +2366,7 @@ void PiPedalModel::CancelMonitorPatchProperty(int64_t clientId, int64_t clientHa audioHost->SetListenForMidiEvent(false); } } + std::vector PiPedalModel::GetAlsaDevices() { std::vector result = this->alsaDevices.GetAlsaDevices(); @@ -2402,9 +2424,9 @@ void PiPedalModel::SetSystemMidiBindings(std::vector &bindings) } } -PedalboardItem*PiPedalModel::GetPedalboardItemForFileProperty(const UiFileProperty& fileProperty) +PedalboardItem *PiPedalModel::GetPedalboardItemForFileProperty(const UiFileProperty &fileProperty) { - for (PedalboardItem*pedalboardItem: this->pedalboard.GetAllPlugins()) + for (PedalboardItem *pedalboardItem : this->pedalboard.GetAllPlugins()) { if (pedalboardItem->pathProperties_.contains(fileProperty.patchProperty())) { @@ -2418,25 +2440,24 @@ FileRequestResult PiPedalModel::GetFileList2(const std::string &relativePath_, c std::string relativePath = relativePath_; try { - if (!storage.IsInUploadsDirectory(relativePath)) + if (!storage.IsInUploadsDirectory(relativePath)) { - // if relativePath is in a resource directory of the plugin, then we have loaded a factory preset or are using a default property. - // map the resource path to the corresponding file in the uploads directory. + // map the resource path to the corresponding file in the uploads directory. // :-( PedalboardItem *pedalboardItem = GetPedalboardItemForFileProperty(fileProperty); if (pedalboardItem) { auto pluginInfo = GetPluginInfo(pedalboardItem->uri()); - if (pluginInfo) { - std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path()) - / fileProperty.resourceDirectory(); - if (IsSubdirectory(relativePath,resourcePath)) + if (pluginInfo) + { + std::filesystem::path resourcePath = fs::path(pluginInfo->bundle_path()) / fileProperty.resourceDirectory(); + if (IsSubdirectory(relativePath, resourcePath)) { - fs::path t = MakeRelativePath(relativePath,resourcePath); - t = fileProperty.directory() / t; - if (fs::exists(t)) + fs::path t = MakeRelativePath(relativePath, resourcePath); + t = fileProperty.directory() / t; + if (fs::exists(t)) { relativePath = t; } @@ -2469,10 +2490,9 @@ std::string PiPedalModel::CopyFilePropertyFile( bool overwrite) { std::lock_guard lock(mutex); - return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty,overwrite); + return storage.CopyFilePropertyFile(oldRelativePath, newRelativePath, uiFileProperty, overwrite); } - void PiPedalModel::DeleteSampleFile(const std::filesystem::path &fileName) { std::lock_guard lock(mutex); @@ -2484,28 +2504,29 @@ std::string PiPedalModel::CreateNewSampleDirectory(const std::string &relativePa std::lock_guard lock(mutex); return storage.CreateNewSampleDirectory(relativePath, uiFileProperty); } -FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty,const std::string&selectedPath) +FilePropertyDirectoryTree::ptr PiPedalModel::GetFilePropertydirectoryTree(const UiFileProperty &uiFileProperty, const std::string &selectedPath) { std::lock_guard lock(mutex); - return storage.GetFilePropertydirectoryTree(uiFileProperty,selectedPath); + return storage.GetFilePropertydirectoryTree(uiFileProperty, selectedPath); } -UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri) +UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId, const std::string &patchPropertyUri) { auto pedalboardItems = pedalboard.GetAllPlugins(); - for (const auto&pedalboardItem: pedalboardItems) { + for (const auto &pedalboardItem : pedalboardItems) + { if (pedalboardItem->instanceId() == instanceId) { Lv2PluginInfo::ptr pluginInfo = GetPluginInfo(pedalboardItem->uri()); if (pluginInfo && pluginInfo->piPedalUI()) { - for (const auto&fileProperty: pluginInfo->piPedalUI()->fileProperties()) - if (fileProperty->patchProperty() == patchPropertyUri) - { - return fileProperty; - } + for (const auto &fileProperty : pluginInfo->piPedalUI()->fileProperties()) + if (fileProperty->patchProperty() == patchPropertyUri) + { + return fileProperty; + } } } } @@ -2513,9 +2534,9 @@ UiFileProperty::ptr PiPedalModel::FindLoadedPatchProperty(int64_t instanceId,con throw std::runtime_error("Permission denied. Plugin not currently loaded."); } -std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId,const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength) +std::string PiPedalModel::UploadUserFile(const std::string &directory, int64_t instanceId, const std::string &patchProperty, const std::string &filename, std::istream &stream, size_t contentLength) { - UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId,patchProperty); + UiFileProperty::ptr fileProperty = FindLoadedPatchProperty(instanceId, patchProperty); if (!fileProperty) { Lv2Log::error(SS("Upload fle: Permission denied. No currently-loaded plugin provides that patch property: " << patchProperty)); @@ -2905,35 +2926,40 @@ bool PiPedalModel::GetHasWifi() return hasWifi; } -std::map PiPedalModel::GetWifiRegulatoryDomains() +std::map PiPedalModel::GetWifiRegulatoryDomains() { - std::map result; - try { - auto& regDb = RegDb::GetInstance(); + std::map result; + try + { + auto ®Db = RegDb::GetInstance(); result = regDb.getRegulatoryDomains(storage.GetConfigRoot() / "iso_codes.json"); - } catch (const std::exception&e) + } + catch (const std::exception &e) { Lv2Log::warning(SS("Unable to query Wifi Regulatory domains. " << e.what())); } return result; } -void PiPedalModel::CancelAudioRetry() { +void PiPedalModel::CancelAudioRetry() +{ std::lock_guard lock(mutex); - if (audioRetryPostHandle) + if (audioRetryPostHandle) { // don't think this can ever happen, but if it did, it would be bad. this->CancelPost(audioRetryPostHandle); audioRetryPostHandle = 0; } - } -void PiPedalModel::OnAlsaDriverTerminatedAbnormally() { - // notification from the realtime thread, via the audiohost that the + +void PiPedalModel::OnAlsaDriverTerminatedAbnormally() +{ + // notification from the realtime thread, via the audiohost that the // ALSA stream has broken. We want to restart. // get off the service thread as promptly as possible - this->Post([&]() { + this->Post([&]() + { std::lock_guard lock(mutex); if (closed) return; @@ -2981,8 +3007,7 @@ void PiPedalModel::OnAlsaDriverTerminatedAbnormally() { } else { Lv2Log::error(SS("Unable to reastart audio.")); - } - }); + } }); } bool PiPedalModel::IsInUploadsDirectory(const std::string &path) @@ -2992,26 +3017,24 @@ bool PiPedalModel::IsInUploadsDirectory(const std::string &path) void PiPedalModel::MoveAudioFile( const std::string &directory, - int32_t fromPosition, - int32_t toPosition -) { + int32_t fromPosition, + int32_t toPosition) +{ if (directory.empty()) { throw std::runtime_error("Directory is empty."); } AudioDirectoryInfo::Ptr dir = AudioDirectoryInfo::Create(directory); - dir->MoveAudioFile(directory, fromPosition, toPosition); + dir->MoveAudioFile(directory, fromPosition, toPosition); } void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string &title) { std::lock_guard lock(mutex); - if (!this->pedalboard.SetItemTitle(instanceId,title)) + if (!this->pedalboard.SetItemTitle(instanceId, title)) { return; } // no need to reload the pedalboard, but we do need to notify subscribers. - this->SetPresetChanged(-1, true); - this->FirePedalboardChanged(-1,false); - + this->SetPresetChanged(-1, true); + this->FirePedalboardChanged(-1, false); } - diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 8f82ad8..edf0ff3 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -240,6 +240,8 @@ namespace pipedal virtual void OnNotifyMidiRealtimeEvent(RealtimeMidiEventType eventType) override; virtual void OnNotifyMidiRealtimeSnapshotRequest(int32_t snapshotIndex,int64_t snapshotRequestId) override; virtual void OnAlsaDriverTerminatedAbnormally() override; + virtual void OnAlsaSequencerDeviceAdded(int client, const std::string &clientName) override; + virtual void OnAlsaSequencerDeviceRemoved(int client) override; void OnNotifyPathPatchPropertyReceived( int64_t instanceId, diff --git a/todo.txt b/todo.txt index 354eb88..5d5115a 100644 --- a/todo.txt +++ b/todo.txt @@ -1,8 +1,6 @@ -VU decay on a timer. +Test autohotspot detection. -Midi handling in the dummy audio connector. -PiPedal sequencer not visible. Is it missing duplex somewhere? check filetime conversion in gcc 12.2! (missing c++ 20 time conversion functions) AudioFiles.cpp fileTimeToInt64 diff --git a/vite/src/pipedal/SelectMidiChannelsDialog.tsx b/vite/src/pipedal/SelectMidiChannelsDialog.tsx index 2eec444..4ca546e 100644 --- a/vite/src/pipedal/SelectMidiChannelsDialog.tsx +++ b/vite/src/pipedal/SelectMidiChannelsDialog.tsx @@ -54,19 +54,23 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) { const [allPorts, setAllPorts] = useState(null); const [model] = useState(PiPedalModelFactory.getInstance()); const [changed, setChanged] = useState(false); + const [ readyToDisplay, setReadyToDisplay ] = useState(false); React.useEffect(() => { if (open) { + setReadyToDisplay(false); model.getAlsaSequencerPorts().then((ports) => { setAvailablePorts(ports); }).catch((error) => { model.showAlert(error); + setReadyToDisplay(true); setAvailablePorts(null); }); model.getAlsaSequencerConfiguration().then((config) => { setConfiguration(config); }).catch((error) => { model.showAlert(error); + setReadyToDisplay(true); setConfiguration(null); }); return () => { @@ -78,7 +82,7 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) { React.useEffect(() => { if (availablePorts !== null && configuration !== null) { let result: DialogItem[] = []; - + setReadyToDisplay(true); for (let port of availablePorts) { result.push({ id: port.id, @@ -159,7 +163,8 @@ function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) { return ( -