ALSA Sequencer Interim checkin.
This commit is contained in:
@@ -26,11 +26,83 @@
|
||||
#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>
|
||||
|
||||
// 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<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
|
||||
};
|
||||
|
||||
};
|
||||
using namespace impl;
|
||||
|
||||
AlsaSequencer::ptr AlsaSequencer::Create()
|
||||
{
|
||||
return std::make_shared<AlsaSequencerImpl>();
|
||||
}
|
||||
|
||||
std::vector<AlsaSequencerPort> AlsaSequencer::EnumeratePorts()
|
||||
{
|
||||
std::vector<AlsaSequencerPort> ports;
|
||||
@@ -149,7 +221,7 @@ namespace pipedal
|
||||
return ports;
|
||||
}
|
||||
|
||||
AlsaSequencer::AlsaSequencer()
|
||||
AlsaSequencerImpl::AlsaSequencerImpl()
|
||||
{
|
||||
seqHandle = nullptr;
|
||||
queueId = -1;
|
||||
@@ -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() {
|
||||
bool AlsaSequencerImpl::WaitForMessage(int timeoutMs)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
auto fdCount = snd_seq_poll_descriptors_count(seqHandle, POLLIN);
|
||||
if (fdCount == 0) return;
|
||||
if (fdCount == 0)
|
||||
return true;
|
||||
this->pollFds.resize(fdCount);
|
||||
|
||||
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.
|
||||
}
|
||||
bool AlsaSequencer::ReadMessage(AlsaMidiMessage &message, bool block)
|
||||
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) {
|
||||
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<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});
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pipedal
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,12 +25,23 @@
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <alsa/asoundlib.h>
|
||||
#include <alsa/seq.h>
|
||||
#include <stdexcept>
|
||||
#include <memory>
|
||||
|
||||
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,12 +72,162 @@ namespace pipedal
|
||||
|
||||
struct AlsaMidiMessage
|
||||
{
|
||||
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];
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -84,46 +245,34 @@ 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<self>;
|
||||
|
||||
static ptr Create();
|
||||
|
||||
static std::vector<AlsaSequencerPort> 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<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
|
||||
|
||||
// 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<AlsaSequencerPort> &seqDevices, const std::string &rawMidiId);
|
||||
|
||||
@@ -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 <functional>
|
||||
|
||||
namespace pipedal {
|
||||
|
||||
class Finally {
|
||||
public:
|
||||
Finally(std::function<void(void)> &&fn)
|
||||
:fn (std::move(fn))
|
||||
{
|
||||
|
||||
}
|
||||
~Finally() {
|
||||
fn();
|
||||
}
|
||||
private:
|
||||
std::function<void(void)> fn;
|
||||
};
|
||||
|
||||
}
|
||||
+40
-289
@@ -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()
|
||||
@@ -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<MidiEvent> midiEvents;
|
||||
size_t midiEventMemoryIndex = 0;
|
||||
std::vector<uint8_t> midiEventMemory;
|
||||
AlsaSequencer::ptr alsaSequencer;
|
||||
|
||||
public:
|
||||
class AlsaMidiDeviceImpl
|
||||
{
|
||||
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<uint8_t> inputSysexBuffer;
|
||||
|
||||
uint8_t readBuffer[1024];
|
||||
|
||||
void checkError(int result, const char *message)
|
||||
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
|
||||
{
|
||||
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<MidiEvent> *pInputEventBuffer = nullptr;
|
||||
|
||||
size_t ReadMidiEvents(
|
||||
std::vector<MidiEvent> &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<std::unique_ptr<AlsaMidiDeviceImpl>> 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<AlsaMidiDeviceImpl>();
|
||||
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(); }
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
|
||||
#include "JackConfiguration.hpp"
|
||||
#include <functional>
|
||||
|
||||
#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;
|
||||
|
||||
+7
-1
@@ -21,6 +21,7 @@
|
||||
#include "util.hpp"
|
||||
#include <lv2/atom/atom.h>
|
||||
#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<AudioDriver>(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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -122,6 +122,7 @@ namespace pipedal
|
||||
}
|
||||
|
||||
JackServerSettings jackServerSettings;
|
||||
AlsaSequencer::ptr alsaSequencer;
|
||||
|
||||
unsigned int periods = 0;
|
||||
|
||||
@@ -190,6 +191,10 @@ namespace pipedal
|
||||
throw;
|
||||
}
|
||||
}
|
||||
virtual void SetAlsaSequencer(AlsaSequencer::ptr alsaSequencer) override
|
||||
{
|
||||
this->alsaSequencer = alsaSequencer;
|
||||
}
|
||||
virtual std::string GetConfigurationDescription()
|
||||
{
|
||||
std::string result = SS(
|
||||
|
||||
@@ -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<AlsaMidiDeviceInfo> GetAlsaSequencers() {
|
||||
std::vector<AlsaMidiDeviceInfo> result;
|
||||
try {
|
||||
auto sequencer = AlsaSequencer::Create();
|
||||
if (sequencer)
|
||||
{
|
||||
std::vector<AlsaSequencerPort> 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())
|
||||
{
|
||||
|
||||
@@ -111,6 +111,10 @@ namespace pipedal
|
||||
{
|
||||
return inputMidiDevices_;
|
||||
}
|
||||
std::vector<AlsaMidiDeviceInfo>& GetInputMidiDevices()
|
||||
{
|
||||
return inputMidiDevices_;
|
||||
}
|
||||
JackChannelSelection RemoveInvalidChannels(const JackConfiguration&config) const;
|
||||
|
||||
static JackChannelSelection MakeDefault(const JackConfiguration&config);
|
||||
|
||||
+2
-2
@@ -433,11 +433,11 @@ static std::vector<AlsaMidiDeviceInfo> OldGetAlsaDevices(const char *devname, co
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<AlsaMidiDeviceInfo> pipedal::GetAlsaMidiInputDevices()
|
||||
std::vector<AlsaMidiDeviceInfo> pipedal::LegacyGetAlsaMidiInputDevices()
|
||||
{
|
||||
return GetAlsaDevices("rawmidi", "Input");
|
||||
}
|
||||
std::vector<AlsaMidiDeviceInfo> pipedal::GetAlsaMidiOutputDevices()
|
||||
std::vector<AlsaMidiDeviceInfo> pipedal::LegacyGetAlsaMidiOutputDevices()
|
||||
{
|
||||
return GetAlsaDevices("rawmidi", "Output");
|
||||
}
|
||||
|
||||
+5
-2
@@ -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<AlsaDeviceInfo> GetAlsaDevices();
|
||||
};
|
||||
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiInputDevices();
|
||||
std::vector<AlsaMidiDeviceInfo> GetAlsaMidiOutputDevices();
|
||||
// we use ALSA sequencers now instead of ALSA rawmidi devices.
|
||||
// Used by test suite to verify migration behaviour.
|
||||
std::vector<AlsaMidiDeviceInfo> LegacyGetAlsaMidiInputDevices();
|
||||
std::vector<AlsaMidiDeviceInfo> LegacyGetAlsaMidiOutputDevices();
|
||||
|
||||
}
|
||||
+10
-11
@@ -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<int>(message.cc0)
|
||||
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc1)
|
||||
<< " " << hex << setfill('0') << setw(2) << static_cast<int>(message.cc2)
|
||||
<< " " << 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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
+44
-1
@@ -22,6 +22,7 @@
|
||||
#include "Storage.hpp"
|
||||
#include "AudioConfig.hpp"
|
||||
#include "PiPedalException.hpp"
|
||||
#include "AlsaSequencer.hpp"
|
||||
#include <stdexcept>
|
||||
#include <sstream>
|
||||
#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<AlsaMidiDeviceInfo>& 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-11
@@ -48,6 +48,7 @@
|
||||
#include "AudioFiles.hpp"
|
||||
|
||||
#include <systemd/sd-daemon.h>
|
||||
#include "AlsaSequencer.hpp"
|
||||
|
||||
using namespace pipedal;
|
||||
|
||||
@@ -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
|
||||
@@ -98,20 +99,34 @@ void segvHandler(int sig) {
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
|
||||
static void EnableBacktrace()
|
||||
{
|
||||
signal(SIGSEGV, segvHandler);
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -223,8 +238,9 @@ int main(int argc, char *argv[])
|
||||
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,7 +253,8 @@ 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);
|
||||
}
|
||||
@@ -273,6 +290,7 @@ int main(int argc, char *argv[])
|
||||
return EXIT_SUCCESS; // indiate to systemd that we don't want a restart.
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user