From a81056a657dcf857db686a3cc4a1852b62f3ad7c Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sun, 29 Jun 2025 07:44:39 -0400 Subject: [PATCH] ALSA Sequencer connections. --- PiPedalCommon/src/AlsaSequencer.cpp | 84 +++++-- PiPedalCommon/src/include/AlsaSequencer.hpp | 56 +++++ src/AudioHost.cpp | 15 +- src/AudioHost.hpp | 2 + src/JackConfiguration.hpp | 6 +- src/PiPedalModel.cpp | 49 ++++ src/PiPedalModel.hpp | 7 + src/PiPedalSocket.cpp | 21 ++ src/Storage.cpp | 97 ++++++-- src/Storage.hpp | 15 ++ vite/src/pipedal/AlsaSequencer.tsx | 53 +++++ vite/src/pipedal/App.tsx | 2 +- vite/src/pipedal/LoopDialog.tsx | 8 +- vite/src/pipedal/PiPedalModel.tsx | 27 +++ vite/src/pipedal/SelectMidiChannelsDialog.tsx | 216 ++++++++++++------ vite/src/pipedal/SettingsDialog.tsx | 34 +-- vite/src/pipedal/ToobPlayerControl.tsx | 2 +- vite/src/pipedal/ToolTipEx.tsx | 25 +- vite/src/pipedal/UseWindowSize.tsx | 55 +++-- 19 files changed, 613 insertions(+), 161 deletions(-) create mode 100644 vite/src/pipedal/AlsaSequencer.tsx diff --git a/PiPedalCommon/src/AlsaSequencer.cpp b/PiPedalCommon/src/AlsaSequencer.cpp index a563402..d3d5afd 100644 --- a/PiPedalCommon/src/AlsaSequencer.cpp +++ b/PiPedalCommon/src/AlsaSequencer.cpp @@ -59,6 +59,7 @@ namespace pipedal virtual void ConnectPort(int clientId, int portId) override; virtual void ConnectPort(const std::string &name) override; + virtual void SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override; // Read a single MIDI message from the sequencer input port. A timeout of -1 blocks indefinitely. // A timeout of 0 returns immediately. @@ -213,6 +214,15 @@ namespace pipedal } port.id = SS("seq:" << port.clientName << "/" << port.name); + port.displaySortOrder = port.client * 256 + port.port; + if (port.isVirtual) + { + port.displaySortOrder += 1 * 256 * 256; // MIDI virtual ports after real ports. + } + else if (port.clientName == "Midi Through") + { + port.displaySortOrder += 2 * 256 * 256; // MIDI Through at the very end because it's weird. + } ports.push_back(std::move(port)); } } @@ -258,11 +268,11 @@ namespace pipedal } void AlsaSequencerImpl::RemoveAllConnections() { - for (const auto &connection : connections) + while (connections.size() != 0) { - snd_seq_disconnect_from(seqHandle, inPort, connection.clientId, connection.portId); + auto connection = connections.back(); + ModifyConnection(connection.clientId, connection.portId, ConnectAction::Unsubscribe); } - connections.clear(); } AlsaSequencerImpl::~AlsaSequencerImpl() { @@ -303,6 +313,25 @@ namespace pipedal } throw std::runtime_error("ALSA port not found"); } + void AlsaSequencerImpl::SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) + { + this->RemoveAllConnections(); // Currently no configuration options to set + + auto ports = EnumeratePorts(); + for (const auto &connection : alsaSequencerConfiguration.connections()) + { + // Connect to each port specified in the configuration + std::string id = connection.id(); + for (const auto &port : ports) + { + if (port.id == id) + { + ConnectPort(port.client, port.port); + break; + } + } + } + } bool AlsaSequencerImpl::WaitForMessage(int timeoutMs) { @@ -547,6 +576,23 @@ namespace pipedal case SND_SEQ_EVENT_CLOCK: 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; +#else +#define MSG_DEBUG_LOG(x) +#endif + MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_START) + MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_EXIT) + MSG_DEBUG_LOG(SND_SEQ_EVENT_CLIENT_CHANGE) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_START) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_EXIT) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_CHANGE) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_SUBSCRIBED) + MSG_DEBUG_LOG(SND_SEQ_EVENT_PORT_UNSUBSCRIBED) + case SND_SEQ_EVENT_KEYSIGN: case SND_SEQ_EVENT_TIMESIGN: // and a PASSEL of others! @@ -677,9 +723,8 @@ namespace pipedal { throw std::runtime_error("Failed to open ALSA sequencer for connection"); } - Finally seq_finally([seq]() { - snd_seq_close(seq); - }); + Finally seq_finally([seq]() + { snd_seq_close(seq); }); snd_seq_addr_t sender, dest; dest.client = myClientId; @@ -704,44 +749,55 @@ namespace pipedal "Failed to disconnect ALSA sequencer port %d:%d. Subscripton not found.", (int)clientId, (int)portId); - } else + } + else { int rc = snd_seq_unsubscribe_port(seq, subs); - if (rc < 0) { + if (rc < 0) + { Lv2Log::warning( "Failed to disconnect ALSA sequencer port %d:%d. (%s)", (int)clientId, (int)portId, snd_strerror(rc)); } - } - for (auto it = this->connections.begin(); it != this->connections.end();++it) + for (auto it = this->connections.begin(); it != this->connections.end(); ++it) { if (it->clientId == clientId && it->portId == portId) { it = this->connections.erase(it); break; } - } + } } else { if (snd_seq_get_port_subscription(seq, subs) == 0) { - Lv2Log::warning("ALSA sequencer port %d:%d is already subscribed.",(int)clientId,(int)portId); + Lv2Log::warning("ALSA sequencer port %d:%d is already subscribed.", (int)clientId, (int)portId); return; } int rc = snd_seq_subscribe_port(seq, subs); if (rc < 0) { Lv2Log::error("Failed to connect ALSA sequencer port %d:%d. (%s)", - (int)clientId,(int)portId, - snd_strerror(rc)); + (int)clientId, (int)portId, + snd_strerror(rc)); return; } this->connections.push_back({clientId, portId}); } } + JSON_MAP_BEGIN(AlsaSequencerPortSelection) + JSON_MAP_REFERENCE(AlsaSequencerPortSelection, id) + JSON_MAP_REFERENCE(AlsaSequencerPortSelection, name) + JSON_MAP_REFERENCE(AlsaSequencerPortSelection, sortOrder) + JSON_MAP_END() + + JSON_MAP_BEGIN(AlsaSequencerConfiguration) + JSON_MAP_REFERENCE(AlsaSequencerConfiguration, connections) + JSON_MAP_END() + } // namespace pipedal diff --git a/PiPedalCommon/src/include/AlsaSequencer.hpp b/PiPedalCommon/src/include/AlsaSequencer.hpp index 58ad6c6..c8cbbca 100644 --- a/PiPedalCommon/src/include/AlsaSequencer.hpp +++ b/PiPedalCommon/src/include/AlsaSequencer.hpp @@ -26,6 +26,7 @@ #include #include #include +#include "json.hpp" namespace pipedal { @@ -42,6 +43,58 @@ namespace pipedal SetPositionTime }; + class AlsaSequencerPortSelection { + private: + std::string id_; + std::string name_; + int32_t sortOrder_ = 0; + + public: + AlsaSequencerPortSelection() = default; + AlsaSequencerPortSelection(const std::string &id, const std::string &name, int32_t displaySortOrder) + : id_(id), name_(name), sortOrder_(displaySortOrder) {} + AlsaSequencerPortSelection(const AlsaSequencerPortSelection &other) = default; + AlsaSequencerPortSelection(AlsaSequencerPortSelection &&other) = default; + AlsaSequencerPortSelection &operator=(const AlsaSequencerPortSelection &other) = default; + AlsaSequencerPortSelection &operator=(AlsaSequencerPortSelection &&other) = default; + + + const std::string& id() const { return id_; } + void id(const std::string &value) { id_ = value; } + const std::string &name() const { return name_; } + void name(const std::string &value) { name_ = value; } + + DECLARE_JSON_MAP(AlsaSequencerPortSelection); + }; + class AlsaSequencerConfiguration { + private: + std::vector connections_; + public: + const std::vector& connections() const { return connections_; } + std::vector& connections() { return connections_; } + void connections(const std::vector& value) { connections_ = value; } + + + bool operator==(const AlsaSequencerConfiguration &other) const + { + if (connections_.size() != other.connections_.size()) + return false; + for (size_t i = 0; i < connections_.size(); ++i) + { + if (connections_[i].id() != other.connections_[i].id() || + connections_[i].name() != other.connections_[i].name()) + { + return false; + } + } + return true; + } + + DECLARE_JSON_MAP(AlsaSequencerConfiguration); + + }; + + struct AlsaSequencerPort { std::string id; @@ -67,6 +120,7 @@ namespace pipedal bool isPort = false; bool isVirtual = false; int cardNumber = -1; + int32_t displaySortOrder = 0; std::string rawMidiDevice; // e.g. "hw:0,0,0" for kernel devices }; @@ -265,6 +319,8 @@ namespace pipedal virtual void ConnectPort(int clientId, int portId) = 0; virtual void ConnectPort(const std::string &id) = 0; + virtual void SetConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; + // Read a single MIDI message from the sequencer input port. A timeout of -1 blocks indefinitely. // A timeout of 0 returns immediately. virtual bool ReadMessage(AlsaMidiMessage &message, int timeoutMs = -1) = 0; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 38f82f3..507ec8d 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -532,7 +532,7 @@ private: std::string GetAtomObjectType(uint8_t *pData) { - LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; + LV2_Atom_Object *pAtom = (LV2_Atom_Object *)pData; if (pAtom->atom.type != uris.atom_Object) { throw std::invalid_argument("Not an Lv2 Object"); @@ -602,7 +602,6 @@ private: this->outputRingBuffer.reset(); audioDriver = nullptr; - alsaSequencer = nullptr; } void ZeroBuffer(float *buffer, size_t nframes) @@ -1259,6 +1258,7 @@ public: Close(); CleanRestartThreads(true); audioDriver = nullptr; + this->alsaSequencer = nullptr; } virtual JackConfiguration GetServerConfiguration() @@ -1642,7 +1642,6 @@ public: { this->isDummyAudioDriver = true; this->audioDriver = std::unique_ptr(CreateDummyAudioDriver(this, jackServerSettings.GetAlsaInputDevice())); - this->audioDriver->SetAlsaSequencer(this->alsaSequencer); } else { @@ -1820,6 +1819,9 @@ public: } } + virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override; + + void OnNotifyPathPatchPropertyReceived( int64_t instanceId, const std::string &pathPatchPropertyUri, @@ -2267,6 +2269,13 @@ void AudioHostImpl::OnWritePatchPropertyBuffer( this->realtimeWriter.SendPathPropertyBuffer(buffer); } +void AudioHostImpl::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) +{ + this->alsaSequencer->SetConfiguration(alsaSequencerConfiguration); +} + + + JSON_MAP_BEGIN(JackHostStatus) JSON_MAP_REFERENCE(JackHostStatus, active) JSON_MAP_REFERENCE(JackHostStatus, errorMessage) diff --git a/src/AudioHost.hpp b/src/AudioHost.hpp index 4fbf8a7..a7edf89 100644 --- a/src/AudioHost.hpp +++ b/src/AudioHost.hpp @@ -39,6 +39,7 @@ namespace pipedal struct RealtimeNextMidiProgramRequest; class PluginHost; class Pedalboard; + class AlsaSequencerConfiguration; using PortMonitorCallback = std::function; @@ -225,6 +226,7 @@ namespace pipedal virtual void Open(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) = 0; virtual void Close() = 0; + virtual void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; virtual uint32_t GetSampleRate() = 0; virtual JackConfiguration GetServerConfiguration() = 0; diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index 71a2629..b76abb0 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -107,11 +107,13 @@ namespace pipedal return outputAudioPorts_; } - const std::vector& GetInputMidiDevices() const + // replaced with AlsaSequencerConfiguration + const std::vector& LegacyGetInputMidiDevices() const { return inputMidiDevices_; } - std::vector& GetInputMidiDevices() + // replaced with AlsaSequencerConfiguration + std::vector& LegacyGetInputMidiDevices() { return inputMidiDevices_; } diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index 51f8448..4ae775c 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -292,10 +292,16 @@ 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 @@ -1400,6 +1406,49 @@ void PiPedalModel::RestartAudio(bool useDummyAudioDriver) } } +void PiPedalModel::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) +{ + std::lock_guard lock(mutex); + + // reset midi connections even if the configuration hasn't changed. + this->audioHost->SetAlsaSequencerConfiguration(alsaSequencerConfiguration); + + auto current = storage.GetAlsaSequencerConfiguration(); + if (alsaSequencerConfiguration != current) + { + this->storage.SetAlsaSequencerConfiguration(alsaSequencerConfiguration); + // notify subscribers. + std::vector t{subscribers.begin(), subscribers.end()}; + for (auto &subscriber : t) + { + subscriber->OnAlsaSequencerConfigurationChanged(alsaSequencerConfiguration); + } + } + +} +AlsaSequencerConfiguration PiPedalModel::GetAlsaSequencerConfiguration() +{ + std::lock_guard lock(mutex); + return this->storage.GetAlsaSequencerConfiguration(); +} + +std::vector PiPedalModel::GetAlsaSequencerPorts() +{ + auto ports = AlsaSequencer::EnumeratePorts(); + std::vector result; + for (auto &port : ports) + { + result.push_back(AlsaSequencerPortSelection{ + port.id, + port.name, + port.displaySortOrder + }); + } + return result; +} + + + void PiPedalModel::SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection) { { diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index 5fd9997..8f82ad8 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -94,6 +94,7 @@ namespace pipedal virtual void OnNetworkChanging(bool hotspotConnected) = 0; virtual void OnHasWifiChanged(bool hasWifi) = 0; + virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; virtual void Close() = 0; }; @@ -168,6 +169,7 @@ namespace pipedal std::vector atomOutputListeners; JackServerSettings jackServerSettings; + PluginHost pluginHost; AtomConverter atomConverter; // must be AFTER pluginHost! @@ -387,6 +389,11 @@ namespace pipedal void SetJackChannelSelection(int64_t clientId, const JackChannelSelection &channelSelection); JackChannelSelection GetJackChannelSelection(); + void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); + AlsaSequencerConfiguration GetAlsaSequencerConfiguration(); + + std::vector GetAlsaSequencerPorts(); + void SetShowStatusMonitor(bool show); bool GetShowStatusMonitor(); diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index c24ab81..4a4132b 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1730,6 +1730,22 @@ public: { auto regulatoryDomains = this->model.GetWifiRegulatoryDomains(); this->Reply(replyTo, "getWifiRegulatoryDomains", regulatoryDomains); + } else if (message == "setAlsaSequencerConfiguration") + { + AlsaSequencerConfiguration config; + pReader->read(&config); + this->model.SetAlsaSequencerConfiguration(config); + this->Reply(replyTo, "setAlsaSequencerConfiguration"); + } + else if (message == "getAlsaSequencerConfiguration") + { + AlsaSequencerConfiguration config = this->model.GetAlsaSequencerConfiguration(); + this->Reply(replyTo, "getAlsaSequencerConfiguration", config); + } + else if (message == "getAlsaSequencerPorts") + { + std::vector result = model.GetAlsaSequencerPorts(); + this->Reply(replyTo,"getAlsaSequencerPorts", result); } else { @@ -1831,6 +1847,11 @@ private: Flush(); } + virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) override + { + Send("onAlsaSequencerConfigurationChanged", alsaSequencerConfiguration); + } + virtual void OnNetworkChanging(bool hotspotConnected) override { try diff --git a/src/Storage.cpp b/src/Storage.cpp index c4fba2e..af60144 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -261,6 +261,8 @@ void Storage::Initialize() catch (const std::exception &) { } + LoadAlsaSequencerConfiguration(); + LoadWifiConfigSettings(); LoadWifiDirectConfigSettings(); LoadUserSettings(); @@ -313,6 +315,10 @@ std::filesystem::path Storage::GetChannelSelectionFileName() { return this->dataRoot / "JackChannelSelection.json"; } +std::filesystem::path Storage::GetAlsaSequencerConfigurationFileName() +{ + return this->dataRoot / "MidiDevices.json"; +} std::filesystem::path Storage::GetIndexFileName() const { return this->GetPresetsDirectory() / BANKS_FILENAME; @@ -736,46 +742,100 @@ JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration &j } -static void MigrateRawMidiToAlsaSequencer(JackChannelSelection &channelSelection) +static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector &selectedDevices) { + AlsaSequencerConfiguration result; try { auto sequencerPorts = AlsaSequencer::EnumeratePorts(); - // Migrate raw MIDI devices to ALSA sequencer ports. - std::vector& selectedDevices = channelSelection.GetInputMidiDevices(); + // Prepare Migrate raw MIDI devices to ALSA sequencer ports. for (auto i = selectedDevices.begin(); i != selectedDevices.end(); ++i) { if (i->name_.starts_with("hw:")) { - bool migrated = false; for (const auto &port: sequencerPorts) { if (i->name_ == port.rawMidiDevice) { - // Found a matching sequencer port. - i->name_ = port.rawMidiDevice; - i->description_ = port.name; - migrated = true; + result.connections().push_back( + AlsaSequencerPortSelection(port.id, port.name,port.displaySortOrder) + ); break; } } - if (!migrated) { - i = selectedDevices.erase(i); - if (i == selectedDevices.end()) - { - break; - } - --i; - } } } } catch (const std::exception&e) { Lv2Log::error(SS("Failed to migrate MIDI settings. " << e.what())); // ick. - channelSelection.GetInputMidiDevices().clear(); } + return result; } +void Storage::LoadAlsaSequencerConfiguration() +{ + auto fileName = this->GetAlsaSequencerConfigurationFileName(); + + if (std::filesystem::exists(fileName)) + { + try + { + std::ifstream s(fileName); + json_reader reader(s); + AlsaSequencerConfiguration result; + reader.read(&result); + this->alsaSequencerConfiguration = result; + } + catch (const std::exception &e) + { + Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); + } + } else { + + // migrate legacy settings from JackConfiguration? + if (this->isJackChannelSelectionValid) + { + this->alsaSequencerConfiguration = + MigrateRawMidiToAlsaSequencer( + this->jackChannelSelection.LegacyGetInputMidiDevices()); + this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); // let them be clear, the next it gets updated. + + } else { + // no legacy settings, so just create a default configuration. + this->alsaSequencerConfiguration = AlsaSequencerConfiguration(); + } + SaveAlsaSequencerConfiguration(); + } + +} +void Storage::SaveAlsaSequencerConfiguration() +{ + auto fileName = this->GetAlsaSequencerConfigurationFileName(); + + try + { + pipedal::ofstream_synced s(fileName); + json_writer writer(s); + writer.write(this->alsaSequencerConfiguration); + } + catch (const std::exception &e) + { + Lv2Log::error("I/O error writing %s: %s", fileName.c_str(), e.what()); + } + +} + +void Storage::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) +{ + this->alsaSequencerConfiguration = alsaSequencerConfiguration; + SaveAlsaSequencerConfiguration(); +} + +AlsaSequencerConfiguration Storage::GetAlsaSequencerConfiguration() const +{ + return this->alsaSequencerConfiguration; +} + void Storage::LoadChannelSelection() { @@ -788,7 +848,6 @@ void Storage::LoadChannelSelection() json_reader reader(s); reader.read(&this->jackChannelSelection); this->isJackChannelSelectionValid = true; - MigrateRawMidiToAlsaSequencer(this->jackChannelSelection); } catch (const std::exception &e) { @@ -801,6 +860,8 @@ void Storage::SaveChannelSelection() auto fileName = this->GetChannelSelectionFileName(); try { + // replaced with AlsaSequencerConfiguration. Delete legacy data. + this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); pipedal::ofstream_synced s(fileName); json_writer writer(s, false); writer.write(this->jackChannelSelection); diff --git a/src/Storage.hpp b/src/Storage.hpp index 440ca17..4ef9346 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -31,6 +31,7 @@ #include "FileEntry.hpp" #include #include "FilePropertyDirectoryTree.hpp" +#include "AlsaSequencer.hpp" namespace pipedal { @@ -84,6 +85,7 @@ private: std::filesystem::path GetIndexFileName() const; std::filesystem::path GetBankFileName(const std::string & name) const; std::filesystem::path GetChannelSelectionFileName(); + std::filesystem::path GetAlsaSequencerConfigurationFileName(); std::filesystem::path GetCurrentPresetPath() const; void LoadBankIndex(); @@ -94,11 +96,19 @@ private: void LoadChannelSelection(); void SaveChannelSelection(); + + void LoadAlsaSequencerConfiguration(); + void SaveAlsaSequencerConfiguration(); + + void SaveBankFile(const std::string& name,const BankFile&bankFile); void LoadBankFile(const std::string &name,BankFile *pBank); std::string GetPresetCopyName(const std::string &name); bool isJackChannelSelectionValid = false; JackChannelSelection jackChannelSelection; + + AlsaSequencerConfiguration alsaSequencerConfiguration;; + WifiConfigSettings wifiConfigSettings; WifiDirectConfigSettings wifiDirectConfigSettings; @@ -126,6 +136,7 @@ public: void LoadWifiDirectConfigSettings(); void LoadUserSettings(); void SaveUserSettings(); + void LoadBank(int64_t instanceId); int64_t GetBankByMidiBankNumber(uint8_t bankNumber); const Pedalboard& GetCurrentPreset(); @@ -181,6 +192,10 @@ public: void SaveCurrentPreset(const CurrentPreset ¤tPreset); bool RestoreCurrentPreset(CurrentPreset*pResult); + void SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration); + AlsaSequencerConfiguration GetAlsaSequencerConfiguration() const; + + //std::string MapPropertyFileName(Lv2PluginInfo*pluginInfo, const std::string&path); private: diff --git a/vite/src/pipedal/AlsaSequencer.tsx b/vite/src/pipedal/AlsaSequencer.tsx new file mode 100644 index 0000000..a49aff0 --- /dev/null +++ b/vite/src/pipedal/AlsaSequencer.tsx @@ -0,0 +1,53 @@ +// Copyright (c) 2025 Robin E. R. Davies +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// 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. + +export class AlsaSequencerPortSelection { + deserialize(json: any) { + this.id = json.id; + this.name = json.name; + this.sortOrder = json.sortOrder; + return this; + }; + static deserialize_array(input: any): AlsaSequencerPortSelection[] { + let result: AlsaSequencerPortSelection[] = []; + for (let i = 0; i < input.length; ++i) { + result[i] = new AlsaSequencerPortSelection().deserialize(input[i]); + } + return result; + } + id: string = ""; + name: string = ""; + sortOrder: number = 0; +}; + +export class AlsaSequencerConfiguration { + deserialize(input: any) { + this.connections = AlsaSequencerPortSelection.deserialize_array(input.connections); + return this; + } + deserialize_array(input: any): AlsaSequencerConfiguration[] { + let result: AlsaSequencerConfiguration[] = []; + for (let i = 0; i < input.length; ++i) { + result[i] = new AlsaSequencerConfiguration().deserialize(input[i]); + } + return result; + } + + connections: AlsaSequencerPortSelection[] = []; +}; diff --git a/vite/src/pipedal/App.tsx b/vite/src/pipedal/App.tsx index d1ab4dd..68e4672 100644 --- a/vite/src/pipedal/App.tsx +++ b/vite/src/pipedal/App.tsx @@ -19,7 +19,7 @@ import React from 'react'; import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles'; -import { CssBaseline } from '@mui/material'; +import CssBaseline from '@mui/material/CssBaseline'; import VirtualKeyboardHandler from './VirtualKeyboardHandler'; import AppThemed from "./AppThemed"; diff --git a/vite/src/pipedal/LoopDialog.tsx b/vite/src/pipedal/LoopDialog.tsx index 698a404..c32e5ba 100644 --- a/vite/src/pipedal/LoopDialog.tsx +++ b/vite/src/pipedal/LoopDialog.tsx @@ -36,7 +36,7 @@ import Slider, { SliderProps } from "@mui/material/Slider"; import Checkbox from "@mui/material/Checkbox"; import TimebaseSelectorDialog from "./TimebaseselectorDialog"; import Timebase, { TimebaseUnits } from "./Timebase"; -import { useWindowHeight } from "@react-hook/window-size"; +import useWindowSize from "./UseWindowSize"; import Button from "@mui/material/Button"; import PlayArrow from "@mui/icons-material/PlayArrow"; import StopIcon from '@mui/icons-material/Stop'; @@ -405,8 +405,8 @@ export default function LoopDialog(props: LoopDialogProps) { const [loopEnd, setLoopEnd] = React.useState(props.value.loopEnd); - const height = useWindowHeight(); - const fullScreen = height < 500; + const [windowSize] = useWindowSize(); + const fullScreen = windowSize.height < 500; function cancelPlaying() { props.onCancelPlaying(); @@ -444,7 +444,7 @@ export default function LoopDialog(props: LoopDialogProps) { open={props.isOpen} onEnterKey={() => { }} - fullScreen={height < 500} + fullScreen={fullScreen} sx={{ "& .MuiDialog-container": { "& .MuiPaper-root": { diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index c00728b..d7545be 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -43,6 +43,7 @@ import { ColorTheme, getColorScheme, setColorScheme } from './DarkMode'; import FilePropertyDirectoryTree from './FilePropertyDirectoryTree'; import AudioFileMetadata from './AudioFileMetadata'; import { pathFileName } from './FileUtils'; +import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer'; export enum State { @@ -452,6 +453,7 @@ export class PiPedalModel //implements PiPedalModel favorites: ObservableProperty = new ObservableProperty({}); + alsaSequencerConfiguration : ObservableProperty = new ObservableProperty(new AlsaSequencerConfiguration()); presets: ObservableProperty = new ObservableProperty ( @@ -668,6 +670,9 @@ export class PiPedalModel //implements PiPedalModel this.handlePluginPresetsChanged(pluginUri); } else if (message === "onJackConfigurationChanged") { this.jackConfiguration.set(new JackConfiguration().deserialize(body)); + } else if (message === "onAlsaSequencerConfigurationChanged") { + this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize(body)); + } else if (message === "onLoadPluginPreset") { let instanceId = body.instanceId as number; let controlValues = ControlValue.deserializeArray(body.controlValues); @@ -1134,6 +1139,9 @@ export class PiPedalModel //implements PiPedalModel this.jackSettings.set(new JackChannelSelection().deserialize( await this.getWebSocket().request("getJackSettings") )); + this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize( + await this.getWebSocket().request("getAlsaSequencerConfiguration") + )); this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request("getBankIndex"))); this.favorites.set(await this.getWebSocket().request("getFavorites")); @@ -3193,6 +3201,25 @@ export class PiPedalModel //implements PiPedalModel // notify the server. this.webSocket?.send("setPedalboardItemTitle", { instanceId: instanceId, title: title }); } + setAlsaSequencerConfiguration(alsaSequencerConfiguration: AlsaSequencerConfiguration): void { + this.webSocket?.send("setAlsaSequencerConfiguration", alsaSequencerConfiguration); + } + async getAlsaSequencerConfiguration(): Promise { + if (this.webSocket) + { + let result = await this.webSocket.request("getAlsaSequencerConfiguration"); + return new AlsaSequencerConfiguration().deserialize(result); + } + throw new Error("No connection."); + } + async getAlsaSequencerPorts(): Promise { + if (this.webSocket) { + let result = await this.webSocket.request("getAlsaSequencerPorts"); + return AlsaSequencerPortSelection.deserialize_array(result); + } + throw new Error("No connection."); + } + }; let instance: PiPedalModel | undefined = undefined; diff --git a/vite/src/pipedal/SelectMidiChannelsDialog.tsx b/vite/src/pipedal/SelectMidiChannelsDialog.tsx index db6647d..2eec444 100644 --- a/vite/src/pipedal/SelectMidiChannelsDialog.tsx +++ b/vite/src/pipedal/SelectMidiChannelsDialog.tsx @@ -17,105 +17,191 @@ // 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. +import React from 'react'; import { useState } from 'react'; import Button from '@mui/material/Button'; import List from '@mui/material/List'; import ListItemButton from '@mui/material/ListItemButton'; import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; import DialogActions from '@mui/material/DialogActions'; import FormControlLabel from '@mui/material/FormControlLabel'; import Typography from '@mui/material/Typography'; import Checkbox from '@mui/material/Checkbox'; -import {AlsaMidiDeviceInfo} from './AlsaMidiDeviceInfo'; +import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSequencer'; import DialogEx from './DialogEx'; - +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; export interface SelectMidiChannelsDialogProps { open: boolean; - selectedChannels: AlsaMidiDeviceInfo[]; - availableChannels: AlsaMidiDeviceInfo[]; - onClose: (selectedChannels: AlsaMidiDeviceInfo[] | null) => void; + onClose: () => void; } -function isChecked(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo): boolean { - for (let i = 0; i < selectedChannels.length; ++i) { - if (selectedChannels[i].equals(channel)) return true; - } - return false; -} -function addPort(availableChannels: AlsaMidiDeviceInfo[], selectedChannels: AlsaMidiDeviceInfo[], newChannel: AlsaMidiDeviceInfo) -:AlsaMidiDeviceInfo[] - { - let result: AlsaMidiDeviceInfo[] = []; - for (let i = 0; i < availableChannels.length; ++i) { - let channel = availableChannels[i]; - if (isChecked(selectedChannels, channel) || channel.equals(newChannel)) { - result.push(channel); - } - } - return result; -} - -function removePort(selectedChannels: AlsaMidiDeviceInfo[], channel: AlsaMidiDeviceInfo) -:AlsaMidiDeviceInfo[] -{ - let result: AlsaMidiDeviceInfo[] = []; - for (let i = 0; i < selectedChannels.length; ++i) { - if (!selectedChannels[i].equals(channel)) { - result.push(selectedChannels[i]); - } - } - return result; -} +interface DialogItem { + id: string; + name: string; + sortOrder: number + offline: boolean; +}; function SelectMidiChannelsDialog(props: SelectMidiChannelsDialogProps) { //const classes = useStyles(); - const { onClose, selectedChannels, availableChannels, open } = props; - const [currentSelection, setCurrentSelection] = useState(selectedChannels); + const { open, onClose } = props; + const [availablePorts, setAvailablePorts] = useState(null); + const [configuration, setConfiguration] = useState(null); + const [allPorts, setAllPorts] = useState(null); + const [model] = useState(PiPedalModelFactory.getInstance()); + const [changed, setChanged] = useState(false); - - - let toggleSelect = (value: AlsaMidiDeviceInfo) => { - if (!isChecked(currentSelection, value)) { - setCurrentSelection(addPort(availableChannels, currentSelection, value)); + React.useEffect(() => { + if (open) { + model.getAlsaSequencerPorts().then((ports) => { + setAvailablePorts(ports); + }).catch((error) => { + model.showAlert(error); + setAvailablePorts(null); + }); + model.getAlsaSequencerConfiguration().then((config) => { + setConfiguration(config); + }).catch((error) => { + model.showAlert(error); + setConfiguration(null); + }); + return () => { + } } else { - setCurrentSelection(removePort(currentSelection, value)); + return () => { }; } + }, [open]); + React.useEffect(() => { + if (availablePorts !== null && configuration !== null) { + let result: DialogItem[] = []; + + for (let port of availablePorts) { + result.push({ + id: port.id, + name: port.name, + sortOrder: port.sortOrder, + offline: false + }); + } + + // include ports that have been previously selected but are not in the current list of available ports + for (let port of configuration.connections) { + if (!availablePorts.some((p) => p.id === port.id)) { + result.push( + { + id: port.id, + name: port.name, + sortOrder: port.sortOrder, + offline: true + } + ); + } + } + result.sort((a, b) => { + return a.sortOrder - b.sortOrder; + }); + setAllPorts(result); + } else { + setAllPorts(null); + } + + }, [availablePorts, configuration]); + + const isChecked = (value: DialogItem) => { + if (availablePorts === null || configuration === null) { + return false; + } + return configuration.connections.some((port) => port.id === value.id); + }; + const setChecked = (value_: DialogItem, checked: boolean) => { + if (availablePorts === null || configuration === null) { + return; + } + let value = new AlsaSequencerPortSelection(); + value.id = value_.id; + value.name = value_.name; + value.sortOrder = value_.sortOrder; + let newConnections = configuration.connections.slice(); + if (checked) { + newConnections.push(value); + } else { + newConnections = newConnections.filter((port) => port.id !== value.id); + } + let newConfiguration = new AlsaSequencerConfiguration(); + newConfiguration.connections = newConnections; + setConfiguration(newConfiguration); + }; + let toggleSelect = (value: DialogItem) => { + if (availablePorts === null || configuration === null) { + return; + } + if (!isChecked(value)) { + setChecked(value, true); + } else { + setChecked(value, false); + } + setChanged(true); }; const handleClose = (): void => { - onClose(null); + onClose(); }; const handleOk = (): void => { - onClose(currentSelection); + if (changed && configuration !== null) { + model.setAlsaSequencerConfiguration(configuration); + } + onClose(); }; return ( - - Select MIDI Device - - {availableChannels.map((channel) => ( - - toggleSelect(channel)} key={channel.name} /> - } - label={channel.description} - /> - - ) + Select MIDI Inputs + + + {allPorts !== null && allPorts.length === 0 && ( + + No MIDI devices found. + )} + {allPorts != null && allPorts.map((port) => ( + + toggleSelect(port)} /> + } + label={ + ( +
+ + {port.name} + + {port.offline && ( + + (offline) + + )} +
+ )} + /> +
+ ) - )} - {availableChannels.length === 0 && ( - - No MIDI devices found. - )} -
+ )} +
+