Alsa Sequencer tests.

This commit is contained in:
Robin E. R. Davies
2025-06-27 04:49:52 -04:00
parent 07af383211
commit 96af9287ba
5 changed files with 547 additions and 9 deletions
+1 -1
View File
@@ -142,7 +142,7 @@
//"[pipedal_alsa_test]"
// "[wifi_channels_test]"
// "[locale]"
"[modFileTypes]"
"[pipedal_alsa_seq_test]"
],
"stopAtEntry": false,
+348
View File
@@ -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 <vector>
#include <string>
#include "ss.hpp"
// enumerate alsa sequencer ports
namespace pipedal
{
std::vector<AlsaSequencerPort> AlsaSequencer::EnumeratePorts()
{
std::vector<AlsaSequencerPort> 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;
}
}
+1
View File
@@ -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
+126
View File
@@ -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 <vector>
#include <string>
#include <alsa/asoundlib.h>
#include <alsa/seq.h>
#include <stdexcept>
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<AlsaSequencerPort> 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<Connection> 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);
};
}
+71 -8
View File
@@ -23,6 +23,8 @@
#include <cstdint>
#include <string>
#include <iostream>
#include "AlsaSequencer.hpp"
#include <iomanip>
#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<int>(message.cc0)
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc1)
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(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();
}