Files
op-pedal/PiPedalCommon/src/AlsaSequencer.cpp
T
2026-01-10 10:41:20 -05:00

1042 lines
41 KiB
C++

/*
* 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 <regex>
#include "ss.hpp"
#include <alsa/asoundlib.h>
#include <alsa/seq.h>
#include <stdexcept>
#include <poll.h>
#include <cstring>
#include "Finally.hpp"
#include "Lv2Log.hpp"
#include <mutex>
#include <thread>
// 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;
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.
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<Connection> connections;
std::vector<struct pollfd> pollFds; // For polling input events
snd_seq_t *seqHandle = nullptr;
int inPort = -1;
int queueId = -1; // Queue for real-time timestamps
};
class AlsaSequencerDeviceMonitorImpl : public AlsaSequencerDeviceMonitor
{
public:
virtual ~AlsaSequencerDeviceMonitorImpl() override;
virtual void StartMonitoring(
Callback &&onChangeCallback) override;
virtual void StopMonitoring() override;
private:
int CreateInputQueue(snd_seq_t *seqHandle, int inPort);
bool started = false;
void ServiceProc();
std::unique_ptr<std::jthread> serviceThread;
std::atomic<bool> terminateThread{false};
Callback callback;
};
};
using namespace impl;
AlsaSequencer::ptr AlsaSequencer::Create()
{
return std::make_shared<AlsaSequencerImpl>();
}
std::vector<AlsaSequencerPort> AlsaSequencer::EnumeratePorts()
{
std::vector<AlsaSequencerPort> ports;
snd_seq_t *seq;
if (snd_seq_open(&seq, "default", SND_SEQ_OPEN_DUPLEX, 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);
#ifdef SND_SEQ_PORT_TYPE_MIDI_UMP
port.isUmp = (typeBits & SND_SEQ_PORT_TYPE_MIDI_UMP) != 0;
#else
port.isUmp = false; // UMP support is not available in all versions of ALSA
#endif
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.isPort = (typeBits & SND_SEQ_PORT_TYPE_PORT) != 0;
port.isSoftware = (typeBits & SND_SEQ_PORT_TYPE_SOFTWARE) != 0;
port.isVirtual = port.name.starts_with("VirMIDI");
port.cardNumber = snd_seq_client_info_get_card(client_info);
port.port = snd_seq_port_info_get_port(port_info);
if (port.isKernelDevice && port.isPort && port.cardNumber >= 0 && port.port >= 0)
{
// For kernel devices, we can construct a raw MIDI device string
std::string rawMidiDevice;
if (port.isVirtual)
{
// "VirMidI 2-1"
std::regex virtualDeviceRegex{"^(VirMIDI) (\\d+)-(\\d+)$"};
{
// Extract the card and device numbers from the match
std::smatch match;
std::regex_search(port.name, match, virtualDeviceRegex);
if (match.size() == 4)
{
try
{
std::string devName = match[1];
int card = std::stoi(match[2]);
int device = std::stoi(match[3]);
rawMidiDevice = SS("hw:CARD=" << devName << ",DEV=" << device);
}
catch (const std::exception &ignored)
{
}
}
}
}
else
{
rawMidiDevice = SS("hw:CARD=" << port.clientName << ",DEV=" << 0);
if (port.port > 0)
{
rawMidiDevice += SS("," << port.port);
}
}
port.rawMidiDevice = std::move(rawMidiDevice);
}
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));
}
}
snd_seq_close(seq);
return ports;
}
AlsaSequencerImpl::AlsaSequencerImpl()
{
seqHandle = nullptr;
queueId = -1;
// Open sequencer in input mode
int rc;
Lv2Log::debug("Opening ALSA Sequencer");
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)));
}
size_t inputBufferSize = snd_seq_get_input_buffer_size(seqHandle);
(void)inputBufferSize;
rc = snd_seq_set_input_buffer_size(seqHandle, 128 * 1024);
if (rc < 0)
{
Lv2Log::warning("Failed resize the ALSA sequencer input buffer: %s", snd_strerror(rc));
}
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)));
}
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)));
}
}
void AlsaSequencerImpl::RemoveAllConnections()
{
while (connections.size() != 0)
{
auto connection = connections.back();
ModifyConnection(connection.clientId, connection.portId, ConnectAction::Unsubscribe);
}
}
AlsaSequencerImpl::~AlsaSequencerImpl()
{
RemoveAllConnections();
if (queueId >= 0)
{
snd_seq_free_queue(seqHandle, queueId);
queueId = -1;
}
if (inPort >= 0)
{
snd_seq_delete_port(seqHandle, inPort);
inPort = -1;
}
if (seqHandle)
{
Lv2Log::debug("Closing ALSA Sequencer");
snd_seq_close(seqHandle);
seqHandle = nullptr;
}
}
void AlsaSequencerImpl::ConnectPort(int clientId, int portId)
{
ModifyConnection(clientId, portId, ConnectAction::Subscribe);
}
void AlsaSequencerImpl::ConnectPort(const std::string &id)
{
auto ports = EnumeratePorts();
for (const auto &port : ports)
{
if (port.id == id)
{
ConnectPort(port.client, port.port);
return;
}
}
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)
{
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);
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 AlsaSequencerImpl::ReadMessage(AlsaMidiMessage &message, int timeoutMs)
{
// Event loop
snd_seq_event_t *event = nullptr;
while (true)
{
bool success = false;
int rc = snd_seq_event_input(seqHandle, &event);
if (rc < 0)
{
if (rc == -EAGAIN)
{
if (!WaitForMessage(timeoutMs))
{
return false;
}
}
else
{
// Handle other errors
throw std::runtime_error(SS("ALSA sequencer input error: " << snd_strerror(rc)));
}
}
else if (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.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.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.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.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.Set(
(uint8_t)(0xC0 | event->data.control.channel), // program change
(uint8_t)(event->data.control.value));
break;
case SND_SEQ_EVENT_CHANPRESS:
message.Set(
uint8_t(0xD0 | event->data.control.channel),
uint8_t(event->data.control.value));
break;
case SND_SEQ_EVENT_PITCHBEND:
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;
#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!
default:
success = false;
break;
}
snd_seq_free_event(event);
}
if (success)
{
return true;
}
}
}
int AlsaSequencerImpl::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)));
}
// Flush the output buffer to start the queue and apply port settings
snd_seq_drain_output(seqHandle);
}
return queueId;
}
bool AlsaSequencerImpl::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;
}
std::string RawMidiIdToSequencerId(const std::vector<AlsaSequencerPort> &seqDevices, const std::string &rawMidiId)
{
for (const auto &device : seqDevices)
{
if (device.rawMidiDevice == rawMidiId)
{
return device.id;
}
}
return {};
}
void AlsaSequencerImpl::ModifyConnection(int clientId, int portId, ConnectAction action)
{
std::lock_guard<std::mutex> 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});
}
}
AlsaSequencerDeviceMonitor::ptr AlsaSequencerDeviceMonitor::Create()
{
return std::make_shared<AlsaSequencerDeviceMonitorImpl>();
}
void AlsaSequencerDeviceMonitorImpl::StartMonitoring(
Callback &&onChangeCallback)
{
started = true;
this->callback = std::move(onChangeCallback);
this->serviceThread = std::make_unique<std::jthread>(
[this]()
{
ServiceProc();
});
}
void AlsaSequencerDeviceMonitorImpl::ServiceProc()
{
int err;
snd_seq_t *seqHandle;
err = snd_seq_open(&seqHandle, "default", SND_SEQ_OPEN_DUPLEX, 0);
Finally seq_finally(
[seqHandle]()
{
snd_seq_close(seqHandle);
});
if (err < 0)
{
Lv2Log::error("Error opening ALSA Device Monitor sequencer: %s", snd_strerror(err));
return;
}
snd_seq_set_client_name(seqHandle, "Device Monitor");
int inPort = snd_seq_create_simple_port(
seqHandle, "PiPedal:portMonitor",
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
SND_SEQ_PORT_TYPE_APPLICATION);
if (inPort < 0)
{
Lv2Log::error("Error creating ALSA Device Monitor port: %s", snd_strerror(inPort));
return;
}
Finally inPort_finaly {
[seqHandle, inPort]()
{
snd_seq_delete_port(seqHandle, inPort);
}
};
// Set client name
int queueId = CreateInputQueue(seqHandle,inPort);
if (queueId < 0)
{
Lv2Log::error("Error creating ALSA Device Monitor queue: %s", snd_strerror(queueId));
return;
}
Finally queue_finally(
[seqHandle, queueId]()
{
snd_seq_free_queue(seqHandle, queueId);
});
// Subscribe to system announcements
snd_seq_port_subscribe_t *subscription;
snd_seq_port_subscribe_alloca(&subscription);
snd_seq_addr_t sender, dest;
sender.client = SND_SEQ_CLIENT_SYSTEM;
sender.port = SND_SEQ_PORT_SYSTEM_ANNOUNCE;
dest.client = snd_seq_client_id(seqHandle);
dest.port = inPort;
snd_seq_port_subscribe_set_sender(subscription, &sender);
snd_seq_port_subscribe_set_dest(subscription, &dest);
err = snd_seq_subscribe_port(seqHandle, subscription);
if (err < 0)
{
Lv2Log::error("Failed to subscribe to ALSA sequencer announcements: %s", snd_strerror(err));
return;
}
// Create poll descriptors
std::vector<struct pollfd> pollFds;
snd_seq_nonblock(seqHandle, 1); // Set sequencer to non-blocking mode
while (!terminateThread)
{
int nPollFds = snd_seq_poll_descriptors_count(seqHandle, POLLIN);
pollFds.resize(nPollFds);
snd_seq_poll_descriptors(seqHandle, pollFds.data(), nPollFds, POLLIN);
// Poll for events
if (poll(pollFds.data(), nPollFds, 100) > 0)
{
snd_seq_event_t *event;
while (snd_seq_event_input(seqHandle, &event) > 0)
{
if (event->type == SND_SEQ_EVENT_CLIENT_START)
{
// Get the client name for logging/debugging
snd_seq_client_info_t *client_info;
snd_seq_client_info_alloca(&client_info);
if (snd_seq_get_any_client_info(seqHandle, event->data.addr.client, client_info) >= 0) {
std::string clientName = snd_seq_client_info_get_name(client_info);
callback(MonitorAction::DeviceAdded, event->data.addr.client, clientName);
}
}
else if (event->type == SND_SEQ_EVENT_CLIENT_EXIT)
{
callback(MonitorAction::DeviceRemoved, event->data.addr.client,"");
}
snd_seq_free_event(event);
}
}
}
return;
}
void AlsaSequencerDeviceMonitorImpl::StopMonitoring()
{
if (started)
{
started = false;
terminateThread = true;
serviceThread = nullptr; // (joins)
}
}
AlsaSequencerDeviceMonitorImpl::~AlsaSequencerDeviceMonitorImpl()
{
StopMonitoring();
}
int AlsaSequencerDeviceMonitorImpl::CreateInputQueue(snd_seq_t *seqHandle, int inPort)
{
if (!seqHandle)
{
throw std::runtime_error("ALSA sequencer not initialized");
}
// Create a new queue if we don't have one yet
int queueId = -1;
{
queueId = snd_seq_alloc_named_queue(seqHandle, "PiPedal Device Monitor Queue");
if (queueId < 0)
{
throw std::runtime_error(SS("Failed to create ALSA queue: " << snd_strerror(queueId)));
}
// Set queue timing to real-time mode
snd_seq_queue_tempo_t *tempo;
snd_seq_queue_tempo_alloca(&tempo);
snd_seq_queue_tempo_set_tempo(tempo, 120); // 120 BPM default
snd_seq_queue_tempo_set_ppq(tempo, 96); // 96 ticks per quarter note
int rc = snd_seq_set_queue_tempo(seqHandle, queueId, tempo);
if (rc < 0)
{
snd_seq_free_queue(seqHandle, queueId);
throw std::runtime_error(SS("Failed to set queue tempo: " << snd_strerror(rc)));
}
// Start the queue
rc = snd_seq_start_queue(seqHandle, queueId, nullptr);
if (rc < 0)
{
snd_seq_free_queue(seqHandle, queueId);
throw std::runtime_error(SS("Failed to start queue: " << snd_strerror(rc)));
}
// Set the queue for input timestamping
snd_seq_port_info_t *port_info;
snd_seq_port_info_alloca(&port_info);
rc = snd_seq_get_port_info(seqHandle, inPort, port_info);
if (rc < 0)
{
snd_seq_free_queue(seqHandle, queueId);
throw std::runtime_error(SS("Failed to get port info: " << snd_strerror(rc)));
}
// Enable timestamping on the input port
snd_seq_port_info_set_timestamping(port_info, 1);
snd_seq_port_info_set_timestamp_real(port_info, 1);
snd_seq_port_info_set_timestamp_queue(port_info, queueId);
rc = snd_seq_set_port_info(seqHandle, inPort, port_info);
if (rc < 0)
{
snd_seq_free_queue(seqHandle, queueId);
throw std::runtime_error(SS("Failed to set port timestamping: " << snd_strerror(rc)));
}
return queueId;
}
return queueId;
}
JSON_MAP_BEGIN(AlsaSequencerPortSelection)
JSON_MAP_REFERENCE(AlsaSequencerPortSelection, id)
JSON_MAP_REFERENCE(AlsaSequencerPortSelection, name)
JSON_MAP_REFERENCE(AlsaSequencerPortSelection, sortOrder)
JSON_MAP_END()
JSON_MAP_BEGIN(AlsaSequencerConfiguration)
JSON_MAP_REFERENCE(AlsaSequencerConfiguration, connections)
JSON_MAP_END()
} // namespace pipedal