From 862d5b6315e1c5f38c88525562537bb17aa14d16 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Sat, 28 Jun 2025 18:41:44 -0400 Subject: [PATCH] ALSA Sequencer Interim checkin. --- PiPedalCommon/src/AlsaSequencer.cpp | 436 +++++++++++++++++--- PiPedalCommon/src/CMakeLists.txt | 1 + PiPedalCommon/src/include/AlsaSequencer.hpp | 235 +++++++++-- PiPedalCommon/src/include/Finally.hpp | 39 ++ src/AlsaDriver.cpp | 339 ++------------- src/AudioDriver.hpp | 4 +- src/AudioHost.cpp | 8 +- src/CMakeLists.txt | 1 - src/DummyAudioDriver.cpp | 7 +- src/JackConfiguration.cpp | 20 +- src/JackConfiguration.hpp | 4 + src/PiPedalAlsa.cpp | 4 +- src/PiPedalAlsa.hpp | 7 +- src/PiPedalAlsaTest.cpp | 21 +- src/Storage.cpp | 45 +- src/main.cpp | 56 ++- todo.txt | 8 +- 17 files changed, 801 insertions(+), 434 deletions(-) create mode 100644 PiPedalCommon/src/include/Finally.hpp diff --git a/PiPedalCommon/src/AlsaSequencer.cpp b/PiPedalCommon/src/AlsaSequencer.cpp index 687e05b..a563402 100644 --- a/PiPedalCommon/src/AlsaSequencer.cpp +++ b/PiPedalCommon/src/AlsaSequencer.cpp @@ -26,17 +26,89 @@ #include #include #include "ss.hpp" +#include +#include +#include +#include +#include +#include "Finally.hpp" +#include "Lv2Log.hpp" +#include // enumerate alsa sequencer ports namespace pipedal { + + namespace impl + { + + enum class ConnectAction + { + Subscribe, + Unsubscribe + }; + + class AlsaSequencerImpl : public AlsaSequencer + { + public: + AlsaSequencerImpl(); + + public: + ~AlsaSequencerImpl(); + + virtual void ConnectPort(int clientId, int portId) override; + virtual void ConnectPort(const std::string &name) override; + + // 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) override; + + // Get current real-time from the queue (useful for calculating precise timing) + virtual bool GetQueueRealtime(uint64_t *sec, uint32_t *nsec) override; + + virtual void RemoveAllConnections() override; + + private: + void ModifyConnection(int clientId, int portId, ConnectAction action); + + // Get the current queue ID (returns -1 if no queue is active) + int GetQueueId() const { return queueId; } + + bool WaitForMessage(int timeoutMs); + // Create an ALSA input queue with real-time timestamps for the given client/port + int CreateRealtimeInputQueue(); + + struct Connection + { + int clientId; + int portId; + }; + + int myClientId = -1; + + std::mutex connectionsMutex; + std::vector connections; + std::vector pollFds; // For polling input events + snd_seq_t *seqHandle = nullptr; + int inPort = -1; + int queueId = -1; // Queue for real-time timestamps + }; + + }; + using namespace impl; + + AlsaSequencer::ptr AlsaSequencer::Create() + { + return std::make_shared(); + } + std::vector AlsaSequencer::EnumeratePorts() { std::vector ports; snd_seq_t *seq; - if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) + if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) { return ports; } @@ -135,7 +207,7 @@ namespace pipedal if (port.port > 0) { rawMidiDevice += SS("," << port.port); - } + } } port.rawMidiDevice = std::move(rawMidiDevice); } @@ -149,14 +221,14 @@ namespace pipedal return ports; } - AlsaSequencer::AlsaSequencer() + AlsaSequencerImpl::AlsaSequencerImpl() { seqHandle = nullptr; queueId = -1; // Open sequencer in input mode int rc; - rc = snd_seq_open(&seqHandle, "default", SND_SEQ_OPEN_DUPLEX, 0); + rc = snd_seq_open(&seqHandle, "default", SND_SEQ_OPEN_DUPLEX, 0); if (rc < 0) { // convert rc to message @@ -173,14 +245,29 @@ namespace pipedal // convert rc to message throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(inPort))); } + CreateRealtimeInputQueue(); + snd_seq_nonblock(seqHandle, 1); // Set sequencer to non-blocking mode + + // Get our client and port numbers for reference + myClientId = snd_seq_client_id(seqHandle); + if (myClientId < 0) + { + throw std::runtime_error(SS("Failed to get client ID: " << snd_strerror(myClientId))); + } } - AlsaSequencer::~AlsaSequencer() + void AlsaSequencerImpl::RemoveAllConnections() { for (const auto &connection : connections) { snd_seq_disconnect_from(seqHandle, inPort, connection.clientId, connection.portId); } + connections.clear(); + } + AlsaSequencerImpl::~AlsaSequencerImpl() + { + RemoveAllConnections(); + if (queueId >= 0) { snd_seq_free_queue(seqHandle, queueId); @@ -198,25 +285,17 @@ namespace pipedal } } - void AlsaSequencer::ConnectPort(int clientId, int portId) + void AlsaSequencerImpl::ConnectPort(int clientId, int portId) { - - CreateRealtimeInputQueue(); - // Connect this input port to the target client:port - int rc = snd_seq_connect_from(seqHandle, inPort, clientId, portId); - if (rc < 0) - { - throw std::runtime_error(SS("Failed to connect to ALSA port: " << snd_strerror(rc))); - } - this->connections.push_back({clientId, portId}); + ModifyConnection(clientId, portId, ConnectAction::Subscribe); } - void AlsaSequencer::ConnectPort(const std::string &name) + void AlsaSequencerImpl::ConnectPort(const std::string &id) { auto ports = EnumeratePorts(); for (const auto &port : ports) { - if (port.name == name) + if (port.id == id) { ConnectPort(port.client, port.port); return; @@ -225,33 +304,63 @@ namespace pipedal throw std::runtime_error("ALSA port not found"); } - void AlsaSequencer::WaitForMessage() { - auto fdCount = snd_seq_poll_descriptors_count(seqHandle, POLLIN); - if (fdCount == 0) return; - this->pollFds.resize(fdCount); + bool AlsaSequencerImpl::WaitForMessage(int timeoutMs) + { + while (true) + { + auto fdCount = snd_seq_poll_descriptors_count(seqHandle, POLLIN); + if (fdCount == 0) + return true; + this->pollFds.resize(fdCount); - snd_seq_poll_descriptors(seqHandle, pollFds.data(), fdCount, POLLIN); + snd_seq_poll_descriptors(seqHandle, pollFds.data(), fdCount, POLLIN); - poll(pollFds.data(), fdCount, -1); // Wait indefinitely for input + int rc = poll(pollFds.data(), fdCount, timeoutMs); + if (rc == 0) + { + return false; // timed out. + } + else if (rc < 0) + { + if (errno == EINTR) + { + continue; + } + else + { + throw std::runtime_error(SS("ALSA sequencer poll error: " << strerror(errno))); + } + } + else + { + return true; + } + } } - bool AlsaSequencer::ReadMessage(AlsaMidiMessage &message, bool block) + bool AlsaSequencerImpl::ReadMessage(AlsaMidiMessage &message, int timeoutMs) { // Event loop snd_seq_event_t *event = nullptr; - while (true) { + while (true) + { bool success = false; int rc = snd_seq_event_input(seqHandle, &event); - if (rc < 0) { - if (rc == -EAGAIN) { - if (!block) { + if (rc < 0) + { + if (rc == -EAGAIN) + { + if (!WaitForMessage(timeoutMs)) + { return false; } - WaitForMessage(); - } else { + } + else + { // Handle other errors throw std::runtime_error(SS("ALSA sequencer input error: " << snd_strerror(rc))); } - } else if (event) + } + else if (event) { success = true; // Extract timestamp information @@ -263,58 +372,198 @@ namespace pipedal switch (event->type) { case SND_SEQ_EVENT_NOTEON: - message.cc0 = 0x90 | event->data.note.channel; // channel - message.cc1 = event->data.note.note; // note - message.cc2 = event->data.note.velocity; // velocity + message.Set( + (uint8_t)(0x90 | event->data.note.channel), + (uint8_t)(event->data.note.note), // note + (uint8_t)(event->data.note.velocity)); // velocity break; case SND_SEQ_EVENT_NOTEOFF: // handle note-off - message.cc0 = 0x80 | event->data.note.channel; // channel - message.cc1 = event->data.note.note; // note - message.cc2 = event->data.note.off_velocity; // off velocity + message.Set( + uint8_t(0x80 | event->data.note.channel), + uint8_t(event->data.note.note), + uint8_t(event->data.note.off_velocity)); // off velocity break; case SND_SEQ_EVENT_KEYPRESS: - message.cc0 = 0xA0 | event->data.note.channel; // polyphonic key pressure - message.cc1 = event->data.note.note; // note - message.cc2 = event->data.note.velocity; // pressure + message.Set( + (uint8_t)(0xA0 | event->data.note.channel), // polyphonic key pressure + (uint8_t)(event->data.note.note), // note + (uint8_t)(event->data.note.velocity)); // pressure break; case SND_SEQ_EVENT_CONTROLLER: - message.cc0 = 0xB0 | event->data.control.channel; // control change - message.cc1 = event->data.control.param; // controller number - message.cc2 = event->data.control.value; // controller value + message.Set( + (uint8_t)(0xB0 | event->data.control.channel), // control change + (uint8_t)(event->data.control.param), // controller number + (uint8_t)(event->data.control.value)); // controller value break; case SND_SEQ_EVENT_PGMCHANGE: - message.cc0 = 0xC0 | event->data.control.channel; // program change - message.cc1 = event->data.control.value; // program number - message.cc2 = 0; // unused + message.Set( + (uint8_t)(0xC0 | event->data.control.channel), // program change + (uint8_t)(event->data.control.value)); break; + case SND_SEQ_EVENT_CHANPRESS: - message.cc0 = 0xD0 | event->data.control.channel; // channel pressure - message.cc1 = event->data.control.value; // pressure value - message.cc2 = 0; // unused + message.Set( + uint8_t(0xD0 | event->data.control.channel), + uint8_t(event->data.control.value)); break; case SND_SEQ_EVENT_PITCHBEND: - message.cc0 = 0xE0 | event->data.control.channel; // pitch bend - message.cc1 = (event->data.control.value >> 7) & 0x7F; // MSB - message.cc2 = event->data.control.value & 0x7F; // LSB + message.Set(uint8_t(0xE0 | event->data.control.channel), + uint8_t((event->data.control.value >> 7) & 0x7F), + uint8_t(event->data.control.value & 0x7F)); break; case SND_SEQ_EVENT_CONTROL14: + message.size = 6; + message.data = message.fixedBuffer; + message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Control Change 14-bit + message.fixedBuffer[1] = uint8_t(event->data.control.param); // MSB + message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB + message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[4] = uint8_t(event->data.control.value + 0x20); + message.fixedBuffer[5] = uint8_t(event->data.control.value) & 0x7F; // MSB value + break; + case SND_SEQ_EVENT_NONREGPARAM: + message.size = 12; + message.data = message.fixedBuffer; + message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Non-registered parameter + message.fixedBuffer[1] = 0x63; // MSB + message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB + message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[4] = 0x62; // LSB + message.fixedBuffer[5] = uint8_t(event->data.control.param) & 0x7F; // LSB + message.fixedBuffer[6] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[7] = uint8_t(0x06); // Non-registered parameter value MSB + message.fixedBuffer[8] = uint8_t(event->data.control.value >> 7) & 0x7F; // MSB value + message.fixedBuffer[9] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[10] = uint8_t(0x26); // Non-registered parameter value LSB + message.fixedBuffer[11] = uint8_t(event->data.control.value) & 0x7F; // LSb + break; case SND_SEQ_EVENT_REGPARAM: + message.size = 12; + message.data = message.fixedBuffer; + message.fixedBuffer[0] = uint8_t(0xB0 | event->data.control.channel); // Registered parameter + message.fixedBuffer[1] = uint8_t(0x65); // MSB + message.fixedBuffer[2] = uint8_t(event->data.control.param >> 7) & 0x7F; // MSB + message.fixedBuffer[3] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[4] = uint8_t(0x64); // LSB + message.fixedBuffer[5] = uint8_t(event->data.control.param) & 0x7F; // LSB + message.fixedBuffer[6] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[7] = uint8_t(0x6); // Registered parameter value MSB + message.fixedBuffer[8] = uint8_t(event->data.control.value >> 7) & 0x7F; // MSB value + message.fixedBuffer[9] = uint8_t(0xB0 | event->data.control.channel); + message.fixedBuffer[10] = uint8_t(0x26); // Registered parameter value LSB + message.fixedBuffer[11] = uint8_t(event->data.control.value) & 0x7F; // LSB value + break; case SND_SEQ_EVENT_SONGPOS: + message.Set( + 0xF2, + (uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB + (uint8_t)(event->data.control.value & 0x7F) // LSB + ); + break; + case SND_SEQ_EVENT_SONGSEL: + message.Set( + 0xF3, + (uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB + (uint8_t)(event->data.control.value & 0x7F) // LSB + ); + break; + case SND_SEQ_EVENT_QFRAME: + message.Set( + 0xF1, + (uint8_t)((event->data.control.value >> 7) & 0x7F), // MSB + (uint8_t)(event->data.control.value & 0x7F) // LSB + ); + break; + case SND_SEQ_EVENT_START: + message.Set(0xFA); // MIDI Real Time Start + break; + case SND_SEQ_EVENT_CONTINUE: + message.Set(0xFB); // MIDI Real Time Continue + break; + case SND_SEQ_EVENT_STOP: + message.Set(0xFC); // MIDI Real Time Stop + break; + case SND_SEQ_EVENT_TICK: + message.Set(0xF8); // MIDI Real Time Clock Tick + break; + case SND_SEQ_EVENT_SENSING: + message.Set(0xFE); // MIDI Real Time Active Sensing + break; + case SND_SEQ_EVENT_RESET: + message.Set(0xFF); // MIDI Real Time System Reset + break; + case SND_SEQ_EVENT_SYSEX: + // Handle SysEx messages + if (event->data.ext.len > 0 && event->data.ext.len + 2 <= sizeof(message.fixedBuffer)) + { + message.size = event->data.ext.len + 1; // +1 for SysEx + message.data = message.fixedBuffer; + message.fixedBuffer[0] = 0xF0; // Start of SysEx + memcpy(message.fixedBuffer + 1, event->data.ext.ptr, event->data.ext.len); + message.fixedBuffer[event->data.ext.len + 1] = 0xF7; // End of SysEx + } + else + { + message.size = event->data.ext.len; + message.data = (uint8_t *)event->data.ext.ptr; + if (message.data[0] != 0xF0) + { + throw std::logic_error("Invalid SysEx message: does not start with 0xF0"); + } + } + break; + case SND_SEQ_EVENT_SETPOS_TICK: + message.size = 3 + sizeof(event->data.queue.param.value); // Tempo events are usually 3 bytes + message.data = message.fixedBuffer; + message.fixedBuffer[0] = 0xFF; // Meta event type for tempo + message.fixedBuffer[1] = (uint8_t)MetaEventType::SetPositionTick; // Meta event subtype for tempo + message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue); // MSB + memcpy(message.fixedBuffer + 3, &event->data.queue.param.value, sizeof(event->data.queue.param.value)); + break; + case SND_SEQ_EVENT_SETPOS_TIME: + message.size = 3 + sizeof(event->data.queue.param.time); // Tempo events are usually 3 bytes + message.data = message.fixedBuffer; + message.fixedBuffer[0] = 0xFF; // Meta event type for tempo + message.fixedBuffer[1] = (uint8_t)MetaEventType::SetPositionTime; // Meta event subtype for tempo + message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue); + memcpy(message.fixedBuffer + 3, &event->data.queue.param.time, sizeof(event->data.queue.param.time)); + break; + case SND_SEQ_EVENT_TEMPO: + // Handle tempo events + message.size = sizeof(event->data.queue.param.value) + 3; // Tempo events are usually 3 bytes + if (message.size > sizeof(message.fixedBuffer)) + { + throw std::logic_error("Tempo event size exceeds fixed buffer size"); + } + message.data = message.fixedBuffer; + message.fixedBuffer[0] = 0xFF; // Meta event type for tempo + message.fixedBuffer[1] = (uint8_t)MetaEventType::Tempo; // Meta event subtype for tempo + message.fixedBuffer[2] = (uint8_t)(event->data.queue.queue); + memcpy(message.fixedBuffer + 3, &event->data.queue.param.value, sizeof(event->data.queue.param.value)); + break; + + case SND_SEQ_EVENT_CLOCK: + message.Set(0xF8); // MIDI Real Time Clock Tick + break; + case SND_SEQ_EVENT_KEYSIGN: + case SND_SEQ_EVENT_TIMESIGN: + // and a PASSEL of others! default: success = false; break; } snd_seq_free_event(event); } - if (success) { + if (success) + { return true; } } } - int AlsaSequencer::CreateRealtimeInputQueue() + int AlsaSequencerImpl::CreateRealtimeInputQueue() { if (!seqHandle) { @@ -381,7 +630,7 @@ namespace pipedal return queueId; } - bool AlsaSequencer::GetQueueRealtime(uint64_t *sec, uint32_t *nsec) + bool AlsaSequencerImpl::GetQueueRealtime(uint64_t *sec, uint32_t *nsec) { if (!seqHandle || queueId < 0) { @@ -418,4 +667,81 @@ namespace pipedal return {}; } + void AlsaSequencerImpl::ModifyConnection(int clientId, int portId, ConnectAction action) + { + std::lock_guard lock(connectionsMutex); + + // use our own seq handle so that we can do this free-threaded. + snd_seq_t *seq; + if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 0) < 0) + { + throw std::runtime_error("Failed to open ALSA sequencer for connection"); + } + Finally seq_finally([seq]() { + snd_seq_close(seq); + }); + + snd_seq_addr_t sender, dest; + dest.client = myClientId; + dest.port = 0; + sender.client = clientId; + sender.port = portId; + + snd_seq_port_subscribe_t *subs; + int queue = this->queueId; + + snd_seq_port_subscribe_alloca(&subs); + snd_seq_port_subscribe_set_sender(subs, &sender); + snd_seq_port_subscribe_set_dest(subs, &dest); + snd_seq_port_subscribe_set_queue(subs, queue); + snd_seq_port_subscribe_set_exclusive(subs, 0); + + if (action == ConnectAction::Unsubscribe) + { + if (snd_seq_get_port_subscription(seq, subs) < 0) + { + Lv2Log::warning( + "Failed to disconnect ALSA sequencer port %d:%d. Subscripton not found.", + (int)clientId, + (int)portId); + } else + { + int rc = snd_seq_unsubscribe_port(seq, subs); + 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) + { + 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); + 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)); + return; + } + this->connections.push_back({clientId, portId}); + } + } + } // namespace pipedal diff --git a/PiPedalCommon/src/CMakeLists.txt b/PiPedalCommon/src/CMakeLists.txt index 8cf14fc..99e0795 100644 --- a/PiPedalCommon/src/CMakeLists.txt +++ b/PiPedalCommon/src/CMakeLists.txt @@ -41,6 +41,7 @@ message(STATUS "NMPIPEDAL CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}") # Use the sdbus-c++ target in SDBusCpp namespace add_library(PiPedalCommon STATIC + include/Finally.hpp include/dbus/org.freedesktop.NetworkManager.Device.Statistics.hpp include/dbus/org.freedesktop.NetworkManager.Settings.Connection.hpp include/dbus/org.freedesktop.NetworkManager.IP6Config.hpp diff --git a/PiPedalCommon/src/include/AlsaSequencer.hpp b/PiPedalCommon/src/include/AlsaSequencer.hpp index abf296e..58ad6c6 100644 --- a/PiPedalCommon/src/include/AlsaSequencer.hpp +++ b/PiPedalCommon/src/include/AlsaSequencer.hpp @@ -25,12 +25,23 @@ #include #include -#include -#include -#include +#include namespace pipedal { + + // event messages starting with 0xFF are meta events. + // data not in midi format, is variable length, and private and the second byte is the MetaEventType. + enum class MetaEventType + { + None, + Tempo, + TimeSignature, + KeySignature, + SetPositionTick, + SetPositionTime + }; + struct AlsaSequencerPort { std::string id; @@ -61,22 +72,172 @@ namespace pipedal struct AlsaMidiMessage { - uint32_t timestamp; // in microseconds (tick time if using queue) + AlsaMidiMessage() + { + size = 0; + data = fixedBuffer; + } + AlsaMidiMessage(uint8_t cc0) + { + size = 1; + data = fixedBuffer; + fixedBuffer[0] = cc0; + } + AlsaMidiMessage(uint8_t cc0, uint8_t cc1) + { + size = 2; + data = fixedBuffer; + fixedBuffer[0] = cc0; + fixedBuffer[1] = cc1; + } + AlsaMidiMessage(uint8_t cc0, uint8_t cc1, uint8_t cc2) + { + size = 3; + data = fixedBuffer; + fixedBuffer[0] = cc0; + fixedBuffer[1] = cc1; + fixedBuffer[2] = cc2; + } + AlsaMidiMessage(uint8_t *data, size_t size) + { + this->size = size; + this->data = data; + } + AlsaMidiMessage(const AlsaMidiMessage &other) + { + this->timestamp = other.timestamp; + this->realtime_sec = other.realtime_sec; + this->realtime_nsec = other.realtime_nsec; + + if (other.size > 3) + { + this->size = other.size; + this->data = other.data; + } + else + { + this->size = other.size; + this->data = fixedBuffer; + for (size_t i = 0; i < other.size; ++i) + { + fixedBuffer[i] = other.fixedBuffer[i]; + } + } + } + AlsaMidiMessage &operator=(const AlsaMidiMessage &other) + { + this->timestamp = other.timestamp; + this->realtime_sec = other.realtime_sec; + this->realtime_nsec = other.realtime_nsec; + + if (other.size > 3) + { + this->size = other.size; + this->data = other.data; + } + else + { + this->size = other.size; + this->data = fixedBuffer; + for (size_t i = 0; i < other.size; ++i) + { + fixedBuffer[i] = other.fixedBuffer[i]; + } + } + return *this; + } + AlsaMidiMessage(AlsaMidiMessage &&other) + { + this->timestamp = other.timestamp; + this->realtime_sec = other.realtime_sec; + this->realtime_nsec = other.realtime_nsec; + + if (other.size > 3) + { + this->size = other.size; + this->data = other.data; + } + else + { + this->size = other.size; + this->data = fixedBuffer; + for (size_t i = 0; i < other.size; ++i) + { + fixedBuffer[i] = other.fixedBuffer[i]; + } + } + } + AlsaMidiMessage &operator=(AlsaMidiMessage &&other) + { + this->timestamp = other.timestamp; + this->realtime_sec = other.realtime_sec; + this->realtime_nsec = other.realtime_nsec; + + if (other.size > 3) + { + this->size = other.size; + this->data = other.data; + } + else + { + this->size = other.size; + this->data = fixedBuffer; + for (size_t i = 0; i < other.size; ++i) + { + fixedBuffer[i] = other.fixedBuffer[i]; + } + } + return *this; + } + + void Set(uint8_t cc0) + { + size = 1; + data = fixedBuffer; + fixedBuffer[0] = cc0; + } + void Set(uint8_t cc0, uint8_t cc1) + { + size = 2; + data = fixedBuffer; + fixedBuffer[0] = cc0; + fixedBuffer[1] = cc1; + } + void Set(uint8_t cc0, uint8_t cc1, uint8_t cc2) + { + size = 3; + data = fixedBuffer; + fixedBuffer[0] = cc0; + fixedBuffer[1] = cc1; + fixedBuffer[2] = cc2; + } + void Set(uint8_t *data, size_t size) + { + this->size = size; + this->data = data; + } + uint8_t cc0() const { return size > 0 ? data[0] : 0; } + uint8_t cc1() const { return size > 1 ? data[1] : 0; } + uint8_t cc2() const { return size > 2 ? data[2] : 0; } + + + uint32_t timestamp; // in microseconds (tick time if using queue) uint64_t realtime_sec; // real-time timestamp seconds uint32_t realtime_nsec; // real-time timestamp nanoseconds - uint8_t cc0; - uint8_t cc1; - uint8_t cc2; + size_t size = 0; + uint8_t *data = fixedBuffer; + + uint8_t fixedBuffer[12]; }; /* * AlsaSequencer - ALSA MIDI sequencer interface with real-time timestamp support - * + * * Example usage with real-time timestamps: - * + * * AlsaSequencer sequencer; * sequencer.connect(clientId, portId); - * + * * AlsaMidiMessage message; * while (sequencer.ReadMessage(&message)) { * // message.realtime_sec and message.realtime_nsec contain precise real-time timestamps @@ -84,48 +245,36 @@ namespace pipedal * printf("MIDI event at %lu.%09u seconds\n", message.realtime_sec, message.realtime_nsec); * } */ + class AlsaSequencer { + protected: + AlsaSequencer() {} + public: + virtual ~AlsaSequencer() = default; + + using self = AlsaSequencer; + using ptr = std::shared_ptr; + + static ptr Create(); + static std::vector EnumeratePorts(); - AlsaSequencer(); - ~AlsaSequencer(); + public: + virtual void ConnectPort(int clientId, int portId) = 0; + virtual void ConnectPort(const std::string &id) = 0; - void ConnectPort(int clientId, int portId); - void ConnectPort(const std::string&name); + // 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; - // Read a single MIDI message from the sequencer input port - bool ReadMessage(AlsaMidiMessage &message, bool block = true); + // currently non-functional + virtual bool GetQueueRealtime(uint64_t *sec, uint32_t *nsec) = 0; - - // Get current real-time from the queue (useful for calculating precise timing) - bool GetQueueRealtime(uint64_t* sec, uint32_t* nsec); - - // Get the current queue ID (returns -1 if no queue is active) - int GetQueueId() const { return queueId; } - - private: - void WaitForMessage(); - // Create an ALSA input queue with real-time timestamps for the given client/port - int CreateRealtimeInputQueue(); - - struct Connection - { - int clientId; - int portId; - }; - - std::vector connections; - std::vector pollFds; // For polling input events - snd_seq_t *seqHandle = nullptr; - int inPort = -1; - int queueId = -1; // Queue for real-time timestamps - - // Example: open an ALSA sequencer input port and read MIDI events continuously - void ReadMidiFromPort(int clientId, int portId); + virtual void RemoveAllConnections() = 0; }; - std::string RawMidiIdToSequencerId(const std::vector &seqDevices,const std::string &rawMidiId); + std::string RawMidiIdToSequencerId(const std::vector &seqDevices, const std::string &rawMidiId); } \ No newline at end of file diff --git a/PiPedalCommon/src/include/Finally.hpp b/PiPedalCommon/src/include/Finally.hpp new file mode 100644 index 0000000..cbbdc7e --- /dev/null +++ b/PiPedalCommon/src/include/Finally.hpp @@ -0,0 +1,39 @@ +// Copyright (c) 2024 Robin 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. + +#pragma once +#include + +namespace pipedal { + + class Finally { + public: + Finally(std::function &&fn) + :fn (std::move(fn)) + { + + } + ~Finally() { + fn(); + } + private: + std::function fn; + }; + +} \ No newline at end of file diff --git a/src/AlsaDriver.cpp b/src/AlsaDriver.cpp index e833410..feb108e 100644 --- a/src/AlsaDriver.cpp +++ b/src/AlsaDriver.cpp @@ -144,8 +144,6 @@ namespace pipedal return false; } - - struct AudioFormat { char name[40]; @@ -304,6 +302,7 @@ namespace pipedal AlsaDriverImpl(AudioDriverHost *driverHost) : driverHost(driverHost) { + midiEventMemoryIndex = 0; midiEventMemory.resize(MAX_MIDI_EVENT * MAX_MIDI_EVENT_SIZE); midiEvents.resize(MAX_MIDI_EVENT); for (size_t i = 0; i < midiEvents.size(); ++i) @@ -415,14 +414,7 @@ namespace pipedal snd_pcm_sw_params_free(playbackSwParams); playbackSwParams = nullptr; } - for (auto &midiState : this->midiDevices) - { - if (midiState) - { - midiState->Close(); - } - } - midiDevices.resize(0); + this->alsaSequencer = nullptr; } std::string discover_alsa_using_apps() @@ -913,7 +905,7 @@ namespace pipedal std::vector &buffers = this->playbackBuffers; int channels = this->playbackChannels; - constexpr double scale = 0x00FFFFFF; + constexpr double scale = 0x00FFFFFF; for (size_t frame = 0; frame < frames; ++frame) { for (int channel = 0; channel < channels; ++channel) @@ -1084,7 +1076,6 @@ namespace pipedal open = true; try { - OpenMidi(jackServerSettings, channelSelection); OpenAudio(jackServerSettings, channelSelection); std::atomic_thread_fence(std::memory_order::release); } @@ -1534,13 +1525,37 @@ namespace pipedal void ReadMidiData(uint32_t audioFrame) { - for (size_t i = 0; i < midiDevices.size(); ++i) + AlsaMidiMessage message; + + midiEventCount = 0; + while(alsaSequencer->ReadMessage(message,0)) { - size_t nRead = midiDevices[i]->ReadMidiEvents( - this->midiEvents, - midiEventCount, - audioFrame); - midiEventCount += nRead; + size_t messageSize = message.size; + if (messageSize == 0) + { + continue; + } + if (midiEventMemoryIndex + messageSize >= this->midiEventMemory.size()) { + continue; + } + if (midiEventCount >= this->midiEvents.size()) { + midiEvents.resize(midiEventCount*2); + } + // for now, prevent META event messages from propagating. + if (message.data[0] == 0xFF && message.size > 1) { + continue; + } + MidiEvent *pEvent = midiEvents.data() + midiEventCount++; + pEvent->time = audioFrame; + pEvent->size = messageSize; + pEvent->buffer = midiEventMemory.data() + midiEventMemoryIndex; + + memcpy( + midiEventMemory.data() + midiEventMemoryIndex, + message.data, + message.size); + midiEventMemoryIndex += messageSize; + } } @@ -1671,16 +1686,17 @@ namespace pipedal pBuffer[j] = 0; } } - try { + try + { while (!terminateAudio()) { std::this_thread::sleep_for(std::chrono::milliseconds(10)); // zero out input buffers. this->driverHost->OnProcess(this->bufferSize); } - } catch (const std::exception &e) + } + catch (const std::exception &e) { - } } this->driverHost->OnAudioTerminated(); @@ -1772,282 +1788,17 @@ namespace pipedal size_t midiEventCount = 0; std::vector midiEvents; + size_t midiEventMemoryIndex = 0; std::vector midiEventMemory; + AlsaSequencer::ptr alsaSequencer; public: - class AlsaMidiDeviceImpl + + + + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override { - private: - snd_rawmidi_t *hIn = nullptr; - snd_rawmidi_params_t *hInParams = nullptr; - std::string deviceName; - - // running status state. - uint8_t runningStatus = 0; - int dataLength = 0; - int dataIndex = 0; - size_t statusBytesRemaining = 0; - size_t data0 = 0; - size_t data1 = 0; - - bool inputProcessingSysex = false; - size_t inputSysexBufferCount = 0; - std::vector inputSysexBuffer; - - uint8_t readBuffer[1024]; - - void checkError(int result, const char *message) - { - if (result < 0) - { - throw PiPedalStateException(SS("Unexpected error: " << message << " (" << this->deviceName)); - } - } - - public: - AlsaMidiDeviceImpl() - { - inputSysexBuffer.resize(1024); - } - ~AlsaMidiDeviceImpl() { - Close(); - } - void Open(const AlsaMidiDeviceInfo &device) - { - runningStatus = 0; - inputProcessingSysex = false; - inputSysexBufferCount = 0; - - dataIndex = 0; - dataLength = 0; - - this->deviceName = device.description_; - - int err = snd_rawmidi_open(&hIn, nullptr, device.name_.c_str(), SND_RAWMIDI_NONBLOCK); - if (err < 0) - { - throw PiPedalStateException(SS("Can't open midi device " << deviceName << ". (" << snd_strerror(err))); - } - - err = snd_rawmidi_params_malloc(&hInParams); - checkError(err, "snd_rawmidi_params_malloc failed."); - - err = snd_rawmidi_params_set_buffer_size(hIn, hInParams, 2048); - checkError(err, "snd_rawmidi_params_set_buffer_size failed."); - - err = snd_rawmidi_params_set_no_active_sensing(hIn, hInParams, 1); - checkError(err, "snd_rawmidi_params_set_no_active_sensing failed."); - } - void Close() - { - if (hIn) - { - snd_rawmidi_close(hIn); - hIn = nullptr; - } - if (hInParams) - { - snd_rawmidi_params_free(hInParams); - hInParams = 0; - } - } - - int GetDataLength(uint8_t cc) - { - static int sDataLength[] = {0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 1, 1, 1, -1}; - return sDataLength[cc >> 4]; - } - - void MidiPut(uint8_t cc, uint8_t d0, uint8_t d1) - { - if (cc == 0) - return; - - // check for overrun. - if (inputEventBufferIndex >= pInputEventBuffer->size()) - { - return; - } - - auto &event = (*pInputEventBuffer)[inputEventBufferIndex]; - - event.time = inputSampleFrame; - event.size = dataLength + 1; - assert(dataLength + 1 <= MAX_MIDI_EVENT_SIZE); - event.buffer[0] = cc; - event.buffer[1] = d0; - event.buffer[2] = d1; - ++inputEventBufferIndex; - } - - void FillInputBuffer() - { - while (true) - { - ssize_t nRead = snd_rawmidi_read(hIn, readBuffer, sizeof(readBuffer)); - if (nRead == -EAGAIN) - return; - if (nRead < 0) - { - checkError(nRead, SS(this->deviceName << "MIDI event read failed. (" << snd_strerror(nRead)).c_str()); - } - ProcessInputBuffer(readBuffer, nRead); // expose write to test code. - } - } - - uint32_t inputSampleFrame = -1; - size_t inputEventBufferIndex; - std::vector *pInputEventBuffer = nullptr; - - size_t ReadMidiEvents( - std::vector &outputBuffer, - size_t startIndex, - uint32_t sampleFrame) - { - inputSampleFrame = sampleFrame; - inputEventBufferIndex = startIndex; - pInputEventBuffer = &outputBuffer; - FillInputBuffer(); - pInputEventBuffer = nullptr; - return inputEventBufferIndex - startIndex; - } - - void FlushSysex() - { - if (inputProcessingSysex) - { - // just discard it. :-/ - // if (this->eventCount != MAX_MIDI_EVENT) - // { - // auto *event = &(events[eventCount++]); - // event->size = this->bufferCount - sysexStartIndex; - // event->buffer = &(this->buffer[this->sysexStartIndex]); - // event->time = 0; - // } - // sysexStartIndex = -1; - } - inputProcessingSysex = false; - } - - int GetSystemCommonLength(uint8_t cc) - { - static int sizes[] = {-1, 1, 2, 1, -1, -1, 0, 0}; - return sizes[(cc >> 4) & 0x07]; - } - void ProcessInputBuffer(uint8_t *readBuffer, size_t nRead) - { - for (ssize_t i = 0; i < nRead; ++i) - { - uint8_t v = readBuffer[i]; - - if (v >= 0x80) - { - if (v >= 0xF0) - { - if (v == 0xF0) - { - inputProcessingSysex = true; - inputSysexBufferCount = 0; - - inputSysexBuffer[inputSysexBufferCount++] = 0xF0; - - runningStatus = 0; // discard subsequent data. - dataLength = -2; // indefinitely. - dataIndex = -1; - } - else if (v >= 0xF8) - { - // don't overwrite running status. - // don't break sysexes on a running status message. - // LV2 standard is ambiguous how realtime messages are handled, so just discard them. - continue; - } - else - { - FlushSysex(); - int length = GetSystemCommonLength(v); - if (length == -1) - break; // ignore illegal messages. - runningStatus = v; - dataLength = length; - dataIndex = 0; - } - } - else - { - FlushSysex(); - int dataLength = GetDataLength(v); - runningStatus = v; - if (dataLength == -1) - { - this->dataLength = dataLength; - dataIndex = -1; - } - else - { - this->dataLength = dataLength; - dataIndex = 0; - } - } - } - else - { - if (inputProcessingSysex) - { - if (inputSysexBufferCount != inputSysexBuffer.size()) - { - inputSysexBuffer[inputSysexBufferCount++] = v; - } - } - else - { - switch (dataIndex) - { - default: - // discard. - break; - case 0: - data0 = v; - dataIndex = 1; - break; - case 1: - data1 = v; - dataIndex = 2; - break; - } - } - } - if (dataIndex == dataLength && dataLength >= 0 && runningStatus != 0) - { - MidiPut(runningStatus, data0, data1); - dataIndex = 0; - } - } - } - }; - - std::vector> midiDevices; - - void OpenMidi(const JackServerSettings &jackServerSettings, const JackChannelSelection &channelSelection) - { - const auto &devices = channelSelection.GetInputMidiDevices(); - - midiDevices.reserve(devices.size()); - - for (size_t i = 0; i < devices.size(); ++i) - { - try - { - const auto &device = devices[i]; - auto midiDevice = std::make_unique(); - midiDevice->Open(device); - midiDevices.push_back(std::move(midiDevice)); - } - catch (const std::exception &e) - { - Lv2Log::error(e.what()); - } - } + this->alsaSequencer = alsaSequencer; } virtual size_t InputBufferCount() const { return activeCaptureBuffers.size(); } @@ -2286,7 +2037,7 @@ namespace pipedal } #ifdef JUNK -static void ExpectEvent(AlsaDriverImpl::AlsaMidiDeviceImpl &m, int event, const std::vector message) + static void ExpectEvent(AlsaDriverImpl::AlsaMidiDeviceImpl &m, int event, const std::vector message) { MidiEvent e; m.GetMidiInputEvent(&e, event); diff --git a/src/AudioDriver.hpp b/src/AudioDriver.hpp index 6660761..71b94c2 100644 --- a/src/AudioDriver.hpp +++ b/src/AudioDriver.hpp @@ -25,7 +25,7 @@ #include "JackConfiguration.hpp" #include - +#include "AlsaSequencer.hpp" @@ -74,7 +74,7 @@ namespace pipedal { virtual float*GetOutputBuffer(size_t channe) = 0; virtual void Open(const JackServerSettings & jackServerSettings,const JackChannelSelection &channelSelection) = 0; - + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) = 0; virtual void Activate() = 0; virtual void Deactivate() = 0; virtual void Close() = 0; diff --git a/src/AudioHost.cpp b/src/AudioHost.cpp index 047237b..38f82f3 100644 --- a/src/AudioHost.cpp +++ b/src/AudioHost.cpp @@ -21,6 +21,7 @@ #include "util.hpp" #include #include "SchedulerPriority.hpp" +#include "AlsaSequencer.hpp" #include "Lv2Log.hpp" @@ -388,6 +389,8 @@ bool SystemMidiBinding::IsMatch(const MidiEvent &event) class AudioHostImpl : public AudioHost, private AudioDriverHost, private IPatchWriterCallback { private: + AlsaSequencer::ptr alsaSequencer; + void OnWritePatchPropertyBuffer( PatchPropertyWriter::Buffer *); @@ -599,6 +602,7 @@ private: this->outputRingBuffer.reset(); audioDriver = nullptr; + alsaSequencer = nullptr; } void ZeroBuffer(float *buffer, size_t nframes) @@ -1248,6 +1252,7 @@ public: lv2_atom_forge_init(&inputWriterForge, pHost->GetMapFeature().GetMap()); cpuTemperatureMonitor = CpuTemperatureMonitor::Get(); + this->alsaSequencer = AlsaSequencer::Create(); } virtual ~AudioHostImpl() { @@ -1637,6 +1642,7 @@ public: { this->isDummyAudioDriver = true; this->audioDriver = std::unique_ptr(CreateDummyAudioDriver(this, jackServerSettings.GetAlsaInputDevice())); + this->audioDriver->SetAlsaSequencer(this->alsaSequencer); } else { @@ -1666,7 +1672,7 @@ public: try { audioDriver->Open(jackServerSettings, this->channelSelection); - + audioDriver->SetAlsaSequencer(this->alsaSequencer); this->sampleRate = audioDriver->GetSampleRate(); this->overrunGracePeriodSamples = (uint64_t)(((uint64_t)this->sampleRate) * OVERRUN_GRACE_PERIOD_S); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 07108d8..b0d36a6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -217,7 +217,6 @@ set (PIPEDAL_SOURCES Lv2PluginChangeMonitor.cpp Lv2PluginChangeMonitor.hpp WebServerConfig.cpp WebServerConfig.hpp Locale.hpp Locale.cpp - Finally.hpp ZipFile.cpp ZipFile.hpp TemporaryFile.cpp TemporaryFile.hpp FilePropertyDirectoryTree.cpp FilePropertyDirectoryTree.hpp diff --git a/src/DummyAudioDriver.cpp b/src/DummyAudioDriver.cpp index a4b58c5..88848a5 100644 --- a/src/DummyAudioDriver.cpp +++ b/src/DummyAudioDriver.cpp @@ -122,6 +122,7 @@ namespace pipedal } JackServerSettings jackServerSettings; + AlsaSequencer::ptr alsaSequencer; unsigned int periods = 0; @@ -190,7 +191,11 @@ namespace pipedal throw; } } - virtual std::string GetConfigurationDescription() + virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override + { + this->alsaSequencer = alsaSequencer; + } + virtual std::string GetConfigurationDescription() { std::string result = SS( "DUMMY, " diff --git a/src/JackConfiguration.cpp b/src/JackConfiguration.cpp index 6ea3949..da7e256 100644 --- a/src/JackConfiguration.cpp +++ b/src/JackConfiguration.cpp @@ -23,6 +23,7 @@ #include "AlsaDriver.hpp" #include "Lv2Log.hpp" #include "PiPedalException.hpp" +#include "AlsaSequencer.hpp" #if JACK_HOST @@ -106,6 +107,23 @@ namespace pipedal { #endif } +static std::vector GetAlsaSequencers() { + std::vector result; + try { + auto sequencer = AlsaSequencer::Create(); + if (sequencer) + { + std::vector ports = sequencer->EnumeratePorts(); + for (const auto &port : ports) + { + result.push_back(AlsaMidiDeviceInfo(port.id.c_str(), port.name.c_str())); + } + } + } catch (const std::exception& e) { + Lv2Log::error("Failed to enumerate ALSA sequencers: %s", e.what()); + } + return result; +} void JackConfiguration::AlsaInitialize( const JackServerSettings &jackServerSettings) { @@ -116,7 +134,7 @@ void JackConfiguration::AlsaInitialize( { this->inputMidiDevices_.clear(); } else { - this->inputMidiDevices_ = GetAlsaMidiInputDevices(); + this->inputMidiDevices_ = GetAlsaSequencers(); // NB: Sequencers, not rawmidi devices, anymore. } if (jackServerSettings.IsValid()) { diff --git a/src/JackConfiguration.hpp b/src/JackConfiguration.hpp index 59a2ec8..71a2629 100644 --- a/src/JackConfiguration.hpp +++ b/src/JackConfiguration.hpp @@ -111,6 +111,10 @@ namespace pipedal { return inputMidiDevices_; } + std::vector& GetInputMidiDevices() + { + return inputMidiDevices_; + } JackChannelSelection RemoveInvalidChannels(const JackConfiguration&config) const; static JackChannelSelection MakeDefault(const JackConfiguration&config); diff --git a/src/PiPedalAlsa.cpp b/src/PiPedalAlsa.cpp index 48b0fa5..af1be7e 100644 --- a/src/PiPedalAlsa.cpp +++ b/src/PiPedalAlsa.cpp @@ -433,11 +433,11 @@ static std::vector OldGetAlsaDevices(const char *devname, co return result; } -std::vector pipedal::GetAlsaMidiInputDevices() +std::vector pipedal::LegacyGetAlsaMidiInputDevices() { return GetAlsaDevices("rawmidi", "Input"); } -std::vector pipedal::GetAlsaMidiOutputDevices() +std::vector pipedal::LegacyGetAlsaMidiOutputDevices() { return GetAlsaDevices("rawmidi", "Output"); } diff --git a/src/PiPedalAlsa.hpp b/src/PiPedalAlsa.hpp index 906e4a2..cf602b2 100644 --- a/src/PiPedalAlsa.hpp +++ b/src/PiPedalAlsa.hpp @@ -53,6 +53,7 @@ namespace pipedal { std::string name_; std::string description_; + // non-serialized. int card_ = -1; int device_ = -1; int subdevice_ = -1; @@ -74,7 +75,9 @@ namespace pipedal { std::vector GetAlsaDevices(); }; - std::vector GetAlsaMidiInputDevices(); - std::vector GetAlsaMidiOutputDevices(); + // we use ALSA sequencers now instead of ALSA rawmidi devices. + // Used by test suite to verify migration behaviour. + std::vector LegacyGetAlsaMidiInputDevices(); + std::vector LegacyGetAlsaMidiOutputDevices(); } \ No newline at end of file diff --git a/src/PiPedalAlsaTest.cpp b/src/PiPedalAlsaTest.cpp index 1e153bd..aa36d68 100644 --- a/src/PiPedalAlsaTest.cpp +++ b/src/PiPedalAlsaTest.cpp @@ -38,10 +38,8 @@ static void DiscoveryTest() auto result = devices.GetAlsaDevices(); std::cout << result.size() << " ALSA devices found." << std::endl; - auto midiInputDevices = GetAlsaMidiInputDevices(); - std::cout << midiInputDevices.size() << " ALSA MIDI input devices found." << std::endl; - auto midiOutputDevices = GetAlsaMidiOutputDevices(); - std::cout << midiOutputDevices.size() << " ALSA MIDI output devices found." << std::endl; + auto midiInputDevices = AlsaSequencer::EnumeratePorts(); + std::cout << midiInputDevices.size() << " ALSA MIDI input sequencers found." << std::endl; } void EnumerateSequencers() @@ -86,18 +84,19 @@ void EnumerateSequencers() void ReadFromsequencerTest() { cout << "--- Reading from ALSA Sequencer" << endl; - AlsaSequencer sequencer; - sequencer.ConnectPort("V25 V25 In"); + AlsaSequencer::ptr sequencer = AlsaSequencer::Create(); + + sequencer->ConnectPort("V25 V25 In"); AlsaMidiMessage message; while (true) { - if (sequencer.ReadMessage(message,true)) + if (sequencer->ReadMessage(message,true)) { cout << "Received MIDI message: " - << " " << hex << setfill('0') << setw(2) << static_cast(message.cc0) - << " " << hex << setfill('0') << setw(2) << static_cast(message.cc1) - << " " << hex << setfill('0') << setw(2) << static_cast(message.cc2) + << " " << hex << setfill('0') << setw(2) << static_cast(message.cc0()) + << " " << hex << setfill('0') << setw(2) << static_cast(message.cc1()) + << " " << hex << setfill('0') << setw(2) << static_cast(message.cc2()) << " Timestamp: " << setw(8) << dec << message.timestamp << std::endl; } @@ -109,7 +108,7 @@ void TestConfigMigration() cout << "--- Testing ALSA Config Migration" << endl; // older versions of PiPedal uses ALSA rawmidi. // this code test coversion of rawmidi device IDs to ALSA Sequencer IDs. - auto rawDevices = GetAlsaMidiInputDevices(); + auto rawDevices = LegacyGetAlsaMidiInputDevices(); auto seqDevices = AlsaSequencer::EnumeratePorts(); for (const auto&seqDevice: seqDevices) { diff --git a/src/Storage.cpp b/src/Storage.cpp index 5d058ca..c4fba2e 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -22,6 +22,7 @@ #include "Storage.hpp" #include "AudioConfig.hpp" #include "PiPedalException.hpp" +#include "AlsaSequencer.hpp" #include #include #include "json.hpp" @@ -734,6 +735,48 @@ JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration &j return jackChannelSelection.RemoveInvalidChannels(jackConfiguration); } + +static void MigrateRawMidiToAlsaSequencer(JackChannelSelection &channelSelection) +{ + try { + auto sequencerPorts = AlsaSequencer::EnumeratePorts(); + + // Migrate raw MIDI devices to ALSA sequencer ports. + std::vector& selectedDevices = channelSelection.GetInputMidiDevices(); + + 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; + 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(); + + } +} + void Storage::LoadChannelSelection() { auto fileName = this->GetChannelSelectionFileName(); @@ -745,11 +788,11 @@ void Storage::LoadChannelSelection() json_reader reader(s); reader.read(&this->jackChannelSelection); this->isJackChannelSelectionValid = true; + MigrateRawMidiToAlsaSequencer(this->jackChannelSelection); } catch (const std::exception &e) { Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); - throw PiPedalStateException("Unexpected error reading Jack settings file."); } } } diff --git a/src/main.cpp b/src/main.cpp index 68e02c3..12ead14 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -48,8 +48,9 @@ #include "AudioFiles.hpp" #include +#include "AlsaSequencer.hpp" -using namespace pipedal; +using namespace pipedal; #ifdef __ARM_ARCH_ISA_A64 #define AARCH64 @@ -81,9 +82,9 @@ static bool isJackServiceRunning() return std::filesystem::exists(path); } - #if ENABLE_BACKTRACE -void segvHandler(int sig) { +void segvHandler(int sig) +{ void *array[10]; // Get void*'s for all entries on the stack @@ -92,26 +93,40 @@ void segvHandler(int sig) { // Print out all the frames to stderr const char *message = "Error: SEGV signal received.\n"; - auto _ = write(STDERR_FILENO,message,strlen(message)); + auto _ = write(STDERR_FILENO, message, strlen(message)); - backtrace_symbols_fd(array+2, size-2, STDERR_FILENO); + backtrace_symbols_fd(array + 2, size - 2, STDERR_FILENO); _exit(EXIT_FAILURE); } - static void EnableBacktrace() { - signal(SIGSEGV, segvHandler); - + signal(SIGSEGV, segvHandler); } #endif -static bool TryGetLogLevel(const std::string&strLogLevel,LogLevel*result) +static bool TryGetLogLevel(const std::string &strLogLevel, LogLevel *result) { - if (strLogLevel == "debug") {*result= LogLevel::Debug; return true;} - if (strLogLevel == "info") {*result= LogLevel::Info; return true;} - if (strLogLevel == "warning") {*result= LogLevel::Warning; return true;} - if (strLogLevel == "error") { *result= LogLevel::Error; return true;} + if (strLogLevel == "debug") + { + *result = LogLevel::Debug; + return true; + } + if (strLogLevel == "info") + { + *result = LogLevel::Info; + return true; + } + if (strLogLevel == "warning") + { + *result = LogLevel::Warning; + return true; + } + if (strLogLevel == "error") + { + *result = LogLevel::Error; + return true; + } *result = LogLevel::Info; return false; } @@ -140,7 +155,7 @@ int main(int argc, char *argv[]) parser.AddOption("-h", &help); parser.AddOption("--help", &help); parser.AddOption("-systemd", &systemd); - parser.AddOption("-log-level",&logLevel); + parser.AddOption("-log-level", &logLevel); parser.AddOption("-port", &portOption); parser.AddOption("-test-extra-device", &testExtraDevice); // advertise two different devices (for testing multi-device connect) @@ -220,11 +235,12 @@ int main(int argc, char *argv[]) if (logLevel.length() != 0) { LogLevel lv2LogLevel; - if (TryGetLogLevel(logLevel,&lv2LogLevel)) + if (TryGetLogLevel(logLevel, &lv2LogLevel)) { Lv2Log::log_level(lv2LogLevel); - - } else { + } + else + { Lv2Log::error(SS("Invalid log level: " << logLevel)); return EXIT_SUCCESS; // indicate to systemd that we don't want a restart. } @@ -237,9 +253,10 @@ int main(int argc, char *argv[]) // clean up orphaned temporary files. const std::filesystem::path webTempDirectory = "/var/pipedal/web_temp"; - if (!webTempDirectory.empty()) { + if (!webTempDirectory.empty()) + { std::filesystem::remove_all(webTempDirectory); //// user must belong to the pipedald grop when debugging. - std::filesystem::create_directories(webTempDirectory); + std::filesystem::create_directories(webTempDirectory); } // configure AudiDirectoryInfo to use the correct directories. @@ -273,6 +290,7 @@ int main(int argc, char *argv[]) return EXIT_SUCCESS; // indiate to systemd that we don't want a restart. } + try { { diff --git a/todo.txt b/todo.txt index 4a7eda0..f3ed0a6 100644 --- a/todo.txt +++ b/todo.txt @@ -1,4 +1,10 @@ -check filetime conversion in gcc 12.2. +Route the midi connections. + +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