From 96af9287ba240976fef6885c8d17c6e30f312e69 Mon Sep 17 00:00:00 2001 From: "Robin E. R. Davies" Date: Fri, 27 Jun 2025 04:49:52 -0400 Subject: [PATCH] Alsa Sequencer tests. --- .vscode/launch.json | 2 +- PiPedalCommon/src/AlsaSequencer.cpp | 348 ++++++++++++++++++++ PiPedalCommon/src/CMakeLists.txt | 1 + PiPedalCommon/src/include/AlsaSequencer.hpp | 126 +++++++ src/PiPedalAlsaTest.cpp | 79 ++++- 5 files changed, 547 insertions(+), 9 deletions(-) create mode 100644 PiPedalCommon/src/AlsaSequencer.cpp create mode 100644 PiPedalCommon/src/include/AlsaSequencer.hpp diff --git a/.vscode/launch.json b/.vscode/launch.json index 8f588ff..bb49bf9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -142,7 +142,7 @@ //"[pipedal_alsa_test]" // "[wifi_channels_test]" // "[locale]" - "[modFileTypes]" + "[pipedal_alsa_seq_test]" ], "stopAtEntry": false, diff --git a/PiPedalCommon/src/AlsaSequencer.cpp b/PiPedalCommon/src/AlsaSequencer.cpp new file mode 100644 index 0000000..28be46f --- /dev/null +++ b/PiPedalCommon/src/AlsaSequencer.cpp @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * 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. + */ + +#include "AlsaSequencer.hpp" +#include +#include +#include "ss.hpp" + +// enumerate alsa sequencer ports + +namespace pipedal +{ + std::vector AlsaSequencer::EnumeratePorts() + { + std::vector ports; + + snd_seq_t *seq; + if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_INPUT, 0) < 0) + { + return ports; + } + + snd_seq_client_info_t *client_info; + snd_seq_port_info_t *port_info; + + snd_seq_client_info_alloca(&client_info); + snd_seq_port_info_alloca(&port_info); + + snd_seq_client_info_set_client(client_info, -1); + + while (snd_seq_query_next_client(seq, client_info) >= 0) + { + int client = snd_seq_client_info_get_client(client_info); + + snd_seq_port_info_set_client(port_info, client); + snd_seq_port_info_set_port(port_info, -1); + + while (snd_seq_query_next_port(seq, port_info) >= 0) + { + unsigned int capability = snd_seq_port_info_get_capability(port_info); + + // Skip system ports and ports without MIDI capability + if (client == SND_SEQ_CLIENT_SYSTEM || + !(capability & (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_WRITE))) + { + continue; + } + if ((capability & (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)) != (SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ)) + { + continue; // Skip ports that are not readable AND subscribable + } + + AlsaSequencerPort port; + port.client = client; + port.isKernelDevice = snd_seq_client_info_get_type(client_info) == SND_SEQ_KERNEL_CLIENT; + port.port = snd_seq_port_info_get_port(port_info); + port.name = snd_seq_port_info_get_name(port_info); + port.clientName = snd_seq_client_info_get_name(client_info); + port.canRead = capability & SND_SEQ_PORT_CAP_READ; + port.canWrite = capability & SND_SEQ_PORT_CAP_WRITE; + port.canReadSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_READ; + port.canWriteSubscribe = capability & SND_SEQ_PORT_CAP_SUBS_WRITE; + auto typeBits = snd_seq_port_info_get_type(port_info); + + port.isUmp = (typeBits & SND_SEQ_PORT_TYPE_MIDI_UMP) != 0; + port.isSystemAnnounce = (typeBits & SND_SEQ_PORT_SYSTEM_ANNOUNCE) != 0; + port.isMidiSynth = (typeBits & SND_SEQ_PORT_TYPE_MIDI_GENERIC) != 0 || + (typeBits & SND_SEQ_PORT_TYPE_MIDI_GM) != 0 || + (typeBits & SND_SEQ_PORT_TYPE_MIDI_GS) != 0 || + (typeBits & SND_SEQ_PORT_TYPE_MIDI_XG) != 0; + port.isApplication = (typeBits & SND_SEQ_PORT_TYPE_APPLICATION) != 0; + port.isSpecific = (typeBits & SND_SEQ_PORT_TYPE_SPECIFIC) != 0; + port.isSynth = (typeBits & SND_SEQ_PORT_TYPE_SYNTH) != 0; + port.isHardware = (typeBits & SND_SEQ_PORT_TYPE_HARDWARE) != 0; + port.isSoftware = (typeBits & SND_SEQ_PORT_TYPE_SOFTWARE) != 0; + + port.cardNumber = snd_seq_client_info_get_card(client_info); + port.port = snd_seq_port_info_get_port(port_info); + + if (port.isKernelDevice & port.cardNumber >= 0 && port.port >= 0) + { + // For kernel devices, we can construct a raw MIDI device string + port.rawMidiDevice = SS("hw:" << port.cardNumber << ",0," << port.port); + } + port.id = SS("seq:" << port.clientName << "/" << port.name); + + ports.push_back(port); + } + } + + snd_seq_close(seq); + return ports; + } + + AlsaSequencer::AlsaSequencer() + { + seqHandle = nullptr; + queueId = -1; + // Open sequencer in input mode + int rc; + + rc = snd_seq_open(&seqHandle, "default", SND_SEQ_OPEN_DUPLEX, 0); + if (rc < 0) + { + // convert rc to message + throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(rc))); + } + snd_seq_set_client_name(seqHandle, "PiPedal"); + + inPort = snd_seq_create_simple_port(seqHandle, "PiPedal:in", + SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, + SND_SEQ_PORT_TYPE_MIDI_GENERIC | + SND_SEQ_PORT_TYPE_MIDI_GM | SND_SEQ_PORT_TYPE_APPLICATION); + if (inPort < 0) + { + // convert rc to message + throw std::runtime_error(SS("Failed to open ALSA sequencer:" << snd_strerror(inPort))); + } + } + AlsaSequencer::~AlsaSequencer() + { + for (const auto &connection : connections) + { + snd_seq_disconnect_from(seqHandle, inPort, connection.clientId, connection.portId); + } + if (queueId >= 0) + { + snd_seq_free_queue(seqHandle, queueId); + queueId = -1; + } + if (inPort >= 0) + { + snd_seq_delete_port(seqHandle, inPort); + inPort = -1; + } + if (seqHandle) + { + snd_seq_close(seqHandle); + seqHandle = nullptr; + } + } + + void AlsaSequencer::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}); + } + + void AlsaSequencer::ConnectPort(const std::string &name) + { + auto ports = EnumeratePorts(); + for (const auto &port : ports) + { + if (port.name == name) + { + ConnectPort(port.client, port.port); + return; + } + } + throw std::runtime_error("ALSA port not found"); + } + + bool AlsaSequencer::ReadMessage(AlsaMidiMessage &message) + { + // Event loop + snd_seq_event_t *event = nullptr; + bool success = false; + if (snd_seq_event_input(seqHandle, &event) >= 0 && event) + { + success = true; + // Extract timestamp information + message.timestamp = event->time.tick; + message.realtime_sec = event->time.time.tv_sec; + message.realtime_nsec = event->time.time.tv_nsec; + + // Process MIDI event here, e.g. NOTEON, NOTEOFF, etc. + 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 + 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 + 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 + 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 + 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 + 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 + 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 + break; + case SND_SEQ_EVENT_CONTROL14: + case SND_SEQ_EVENT_NONREGPARAM: + case SND_SEQ_EVENT_REGPARAM: + case SND_SEQ_EVENT_SONGPOS: + default: + success = false; + break; + } + snd_seq_free_event(event); + } + return success; + } + + int AlsaSequencer::CreateRealtimeInputQueue() + { + if (!seqHandle) + { + throw std::runtime_error("ALSA sequencer not initialized"); + } + + // Create a new queue if we don't have one yet + if (queueId < 0) + { + queueId = snd_seq_alloc_named_queue(seqHandle, "PiPedal Realtime 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); + queueId = -1; + 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); + queueId = -1; + 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); + queueId = -1; + 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); + queueId = -1; + throw std::runtime_error(SS("Failed to set port timestamping: " << snd_strerror(rc))); + } + } + + return queueId; + } + bool AlsaSequencer::GetQueueRealtime(uint64_t *sec, uint32_t *nsec) + { + if (!seqHandle || queueId < 0) + { + return false; + } + + snd_seq_queue_status_t *status; + snd_seq_queue_status_alloca(&status); + + int rc = snd_seq_get_queue_status(seqHandle, queueId, status); + if (rc < 0) + { + return false; + } + + const snd_seq_real_time_t *realtime = snd_seq_queue_status_get_real_time(status); + if (sec) + *sec = realtime->tv_sec; + if (nsec) + *nsec = realtime->tv_nsec; + + return true; + } +} diff --git a/PiPedalCommon/src/CMakeLists.txt b/PiPedalCommon/src/CMakeLists.txt index 6d5776f..8cf14fc 100644 --- a/PiPedalCommon/src/CMakeLists.txt +++ b/PiPedalCommon/src/CMakeLists.txt @@ -57,6 +57,7 @@ add_library(PiPedalCommon STATIC include/dbus/org.freedesktop.NetworkManager.Device.Wireless.hpp include/dbus/org.freedesktop.NetworkManager.DHCP4Config.hpp + AlsaSequencer.cpp include/AlsaSequencer.hpp Utf8Utils.cpp include/Utf8Utils.hpp NetworkManagerInterfaces.cpp include/NetworkManagerInterfaces.hpp diff --git a/PiPedalCommon/src/include/AlsaSequencer.hpp b/PiPedalCommon/src/include/AlsaSequencer.hpp new file mode 100644 index 0000000..d70db85 --- /dev/null +++ b/PiPedalCommon/src/include/AlsaSequencer.hpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025 Robin E. R. Davies + * All rights reserved. + + * 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 +#include +#include +#include +#include + +namespace pipedal +{ + struct AlsaSequencerPort + { + std::string id; + int client; + int port; + std::string name; + std::string clientName; + bool canRead = false; + bool canWrite = false; + bool canReadSubscribe = false; + bool canWriteSubscribe = false; + + bool isKernelDevice = false; + bool isSystemAnnounce = false; + bool isUmp = false; + bool isMidi = false; + bool isApplication = false; + bool isMidiSynth = false; + bool isSynth = false; + bool isSpecific; + bool isHardware = false; + bool isSoftware = false; + int cardNumber = -1; + std::string rawMidiDevice; // e.g. "hw:0,0,0" for kernel devices + }; + + struct AlsaMidiMessage + { + 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; + }; + + /* + * 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 + * // message.timestamp contains the queue tick time + * printf("MIDI event at %lu.%09u seconds\n", message.realtime_sec, message.realtime_nsec); + * } + */ + class AlsaSequencer + { + public: + static std::vector EnumeratePorts(); + + AlsaSequencer(); + ~AlsaSequencer(); + + void ConnectPort(int clientId, int portId); + void ConnectPort(const std::string&name); + + // Read a single MIDI message from the sequencer input port + bool ReadMessage(AlsaMidiMessage &message); + + + // 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: + // 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; + + 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); + }; + +} \ No newline at end of file diff --git a/src/PiPedalAlsaTest.cpp b/src/PiPedalAlsaTest.cpp index 4b6e3c0..c0c4da5 100644 --- a/src/PiPedalAlsaTest.cpp +++ b/src/PiPedalAlsaTest.cpp @@ -23,6 +23,8 @@ #include #include #include +#include "AlsaSequencer.hpp" +#include #include "PiPedalAlsa.hpp" @@ -40,16 +42,77 @@ static void DiscoveryTest() 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; - - } +void EnumerateSequencers() +{ + cout << "--- Enumerating ALSA Sequencers" << endl; + auto sequencers = AlsaSequencer::EnumeratePorts(); + for (const auto &sequencer : sequencers) + { + cout << sequencer.client << ": "<< sequencer.port << " - " << sequencer.clientName << "/" << sequencer.name; + + cout << " [" << (sequencer.isKernelDevice ? "Kernel" : "User"); + if (!sequencer.rawMidiDevice.empty()) + { + cout << " " << sequencer.rawMidiDevice; + } + cout << "]" << endl; + + cout << " Read: " << (sequencer.canRead ? "Yes" : "No") + << ", Write: " << (sequencer.canWrite ? "Yes" : "No") + << ", Read Subscribe: " << (sequencer.canReadSubscribe ? "Yes" : "No") + << ", Write Subscribe: " << (sequencer.canWriteSubscribe ? "Yes" : "No") << std::endl; + + cout + << " System Announce : " << (sequencer.isSystemAnnounce ? " Yes " : " No ") + << "UMP: " + << (sequencer.isUmp ? "Yes" : "No") + << "MIDI: " + << (sequencer.isMidi ? "Yes" : "No") + << "Application: " + << (sequencer.isApplication ? "Yes" : "No") + << " MIDI Synth: " + << (sequencer.isMidiSynth ? "Yes" : "No") + << "Synth: " + << (sequencer.isSynth ? "Yes" : "No") + << std::endl; + cout << " Specific: " << (sequencer.isSpecific ? "Yes" : "No") + << "Hardware: " << (sequencer.isHardware ? "Yes" : "No") + << "Software: " << (sequencer.isSoftware ? "Yes" : "No") << std::endl; + } +} + +void ReadFromsequencerTest() +{ + cout << "--- Reading from ALSA Sequencer" << endl; + AlsaSequencer sequencer; + sequencer.ConnectPort("V25 V25 In"); + + AlsaMidiMessage message; + while (true) + { + if (sequencer.ReadMessage(message)) + { + 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) + << " Timestamp: " << setw(8) << dec << message.timestamp + << std::endl; + } + } +} + +TEST_CASE("ALSA Seq Test", "[pipedal_alsa_seq_test][Build][Dev]") +{ + EnumerateSequencers(); + + ReadFromsequencerTest(); +} + +TEST_CASE("ALSA Test", "[pipedal_alsa_test][Build][Dev]") +{ -TEST_CASE( "ALSA Test", "[pipedal_alsa_test][Build][Dev]" ) { DiscoveryTest(); - - } - - -