very, very simple WP with midi CC out
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include <libremidi/backends/alsa_raw/config.hpp>
|
||||
|
||||
#if __has_include(<alsa/asoundlib.h>)
|
||||
#include <alsa/asoundlib.h>
|
||||
#else
|
||||
extern "C" {
|
||||
typedef struct _snd_seq snd_seq_t;
|
||||
typedef struct snd_seq_event snd_seq_event_t;
|
||||
typedef struct snd_seq_addr
|
||||
{
|
||||
unsigned char client;
|
||||
unsigned char port;
|
||||
} snd_seq_addr_t;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace libremidi::alsa_seq
|
||||
{
|
||||
|
||||
// Possible parameters:
|
||||
// - Direct / non-direct output
|
||||
// - Timer source used (high resolution, etc)
|
||||
// - Timestamping
|
||||
// - Tempo, ppq?
|
||||
|
||||
struct poll_parameters
|
||||
{
|
||||
snd_seq_addr_t addr{};
|
||||
std::function<int(const snd_seq_event_t&)> callback;
|
||||
};
|
||||
|
||||
struct input_configuration
|
||||
{
|
||||
using poll_parameters_type = poll_parameters;
|
||||
|
||||
std::string client_name = "libremidi client";
|
||||
snd_seq_t* context{};
|
||||
std::function<bool(const poll_parameters&)> manual_poll;
|
||||
std::function<bool(snd_seq_addr_t)> stop_poll;
|
||||
|
||||
static constexpr int midi_version = 1;
|
||||
};
|
||||
|
||||
struct output_configuration
|
||||
{
|
||||
std::string client_name = "libremidi client";
|
||||
snd_seq_t* context{};
|
||||
|
||||
static constexpr int midi_version = 1;
|
||||
};
|
||||
|
||||
struct observer_configuration
|
||||
{
|
||||
std::string client_name = "libremidi client";
|
||||
snd_seq_t* context{};
|
||||
std::function<bool(const poll_parameters&)> manual_poll;
|
||||
std::function<bool(snd_seq_addr_t)> stop_poll;
|
||||
|
||||
static constexpr int midi_version = 1;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
#pragma once
|
||||
#include <libremidi/backends/linux/alsa.hpp>
|
||||
#include <libremidi/config.hpp>
|
||||
#include <libremidi/detail/observer.hpp>
|
||||
|
||||
#include <sys/time.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace libremidi::alsa_seq
|
||||
{
|
||||
struct event_handle
|
||||
{
|
||||
const libasound& snd;
|
||||
snd_seq_event_t* ev{};
|
||||
|
||||
explicit event_handle(const libasound& snd) noexcept
|
||||
: snd{snd}
|
||||
{
|
||||
}
|
||||
|
||||
explicit event_handle(const libasound& snd, snd_seq_event_t* ev) noexcept
|
||||
: snd{snd}
|
||||
, ev{ev}
|
||||
{
|
||||
}
|
||||
|
||||
event_handle(const event_handle&) noexcept = delete;
|
||||
event_handle& operator=(const event_handle&) noexcept = delete;
|
||||
event_handle(event_handle&&) noexcept = delete;
|
||||
event_handle& operator=(event_handle&&) noexcept = delete;
|
||||
|
||||
void reset(snd_seq_event_t* new_ev) noexcept
|
||||
{
|
||||
if (ev)
|
||||
snd.seq.free_event(ev);
|
||||
ev = new_ev;
|
||||
}
|
||||
|
||||
~event_handle() { snd.seq.free_event(ev); }
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
inline constexpr port_handle seq_to_port_handle(uint64_t client, uint64_t port) noexcept
|
||||
{
|
||||
return (client << 32) + port;
|
||||
}
|
||||
|
||||
inline constexpr std::pair<int, int> seq_from_port_handle(port_handle p) noexcept
|
||||
{
|
||||
int client = p >> 32;
|
||||
int port = p & 0xFFFFFFFF;
|
||||
return {client, port};
|
||||
}
|
||||
|
||||
inline void for_all_ports(
|
||||
const libasound& snd, snd_seq_t* seq,
|
||||
std::function<void(snd_seq_client_info_t&, snd_seq_port_info_t&)> func)
|
||||
{
|
||||
snd_seq_client_info_t* cinfo{};
|
||||
snd_seq_client_info_alloca(&cinfo);
|
||||
snd_seq_port_info_t* pinfo{};
|
||||
snd_seq_port_info_alloca(&pinfo);
|
||||
|
||||
snd.seq.client_info_set_client(cinfo, -1);
|
||||
while (snd.seq.query_next_client(seq, cinfo) >= 0)
|
||||
{
|
||||
int client = snd.seq.client_info_get_client(cinfo);
|
||||
if (client == 0)
|
||||
continue;
|
||||
|
||||
// Reset query info
|
||||
snd.seq.port_info_set_client(pinfo, client);
|
||||
snd.seq.port_info_set_port(pinfo, -1);
|
||||
while (snd.seq.query_next_port(seq, pinfo) >= 0)
|
||||
{
|
||||
func(*cinfo, *pinfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is used to count or get the pinfo structure for a given port
|
||||
// number.
|
||||
inline unsigned int iterate_port_info(
|
||||
const libasound& snd, snd_seq_t* seq, snd_seq_port_info_t* pinfo, unsigned int type,
|
||||
int portNumber)
|
||||
{
|
||||
snd_seq_client_info_t* cinfo{};
|
||||
int count = 0;
|
||||
snd_seq_client_info_alloca(&cinfo);
|
||||
|
||||
snd.seq.client_info_set_client(cinfo, -1);
|
||||
while (snd.seq.query_next_client(seq, cinfo) >= 0)
|
||||
{
|
||||
const int client = snd.seq.client_info_get_client(cinfo);
|
||||
if (client == 0)
|
||||
continue;
|
||||
|
||||
// Reset query info
|
||||
snd.seq.port_info_set_client(pinfo, client);
|
||||
snd.seq.port_info_set_port(pinfo, -1);
|
||||
while (snd.seq.query_next_port(seq, pinfo) >= 0)
|
||||
{
|
||||
const unsigned int atyp = snd.seq.port_info_get_type(pinfo);
|
||||
if (((atyp & SND_SEQ_PORT_TYPE_MIDI_GENERIC) == 0) && ((atyp & SND_SEQ_PORT_TYPE_SYNTH) == 0)
|
||||
&& ((atyp & SND_SEQ_PORT_TYPE_APPLICATION) == 0))
|
||||
continue;
|
||||
|
||||
const unsigned int caps = snd.seq.port_info_get_capability(pinfo);
|
||||
if ((caps & type) != type)
|
||||
continue;
|
||||
if ((caps & SND_SEQ_PORT_CAP_NO_EXPORT) != 0)
|
||||
continue;
|
||||
if (count == portNumber)
|
||||
return 1;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
// If a negative portNumber was used, return the port count.
|
||||
if (portNumber < 0)
|
||||
return count;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// A structure to hold variables related to the ALSA API
|
||||
// implementation.
|
||||
struct alsa_data
|
||||
{
|
||||
const libasound& snd = libasound::instance();
|
||||
snd_seq_t* seq{};
|
||||
int vport{-1};
|
||||
snd_seq_addr_t vaddr{};
|
||||
|
||||
snd_seq_port_subscribe_t* subscription{};
|
||||
snd_midi_event_t* coder{};
|
||||
|
||||
[[nodiscard]] int init_client(auto& configuration)
|
||||
{
|
||||
// Initialize or use the snd_seq client
|
||||
if (configuration.context)
|
||||
{
|
||||
seq = configuration.context;
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set up the ALSA sequencer client.
|
||||
int ret = snd.seq.open(&seq, "default", SND_SEQ_OPEN_DUPLEX, SND_SEQ_NONBLOCK);
|
||||
if (ret < 0)
|
||||
return ret;
|
||||
|
||||
// Set client name.
|
||||
if (!configuration.client_name.empty())
|
||||
snd.seq.set_client_name(seq, configuration.client_name.data());
|
||||
|
||||
#if __has_include(<alsa/ump.h>)
|
||||
if (snd.seq.ump.set_client_midi_version)
|
||||
{
|
||||
switch (configuration.midi_version)
|
||||
{
|
||||
case 1:
|
||||
snd.seq.ump.set_client_midi_version(seq, SND_SEQ_CLIENT_LEGACY_MIDI);
|
||||
break;
|
||||
case 2:
|
||||
snd.seq.ump.set_client_midi_version(seq, SND_SEQ_CLIENT_UMP_MIDI_2_0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
void set_client_name(std::string_view clientName)
|
||||
{
|
||||
snd.seq.set_client_name(seq, clientName.data());
|
||||
}
|
||||
|
||||
void set_port_name(std::string_view portName)
|
||||
{
|
||||
snd_seq_port_info_t* pinfo;
|
||||
snd_seq_port_info_alloca(&pinfo);
|
||||
snd.seq.get_port_info(seq, vport, pinfo);
|
||||
snd.seq.port_info_set_name(pinfo, portName.data());
|
||||
snd.seq.set_port_info(seq, vport, pinfo);
|
||||
}
|
||||
|
||||
unsigned int get_port_count(int caps) const
|
||||
{
|
||||
snd_seq_port_info_t* pinfo;
|
||||
snd_seq_port_info_alloca(&pinfo);
|
||||
|
||||
return alsa_seq::iterate_port_info(snd, seq, pinfo, caps, -1);
|
||||
}
|
||||
|
||||
std::optional<snd_seq_addr_t> get_port_info(const port_information& portNumber)
|
||||
{
|
||||
auto [client, port] = alsa_seq::seq_from_port_handle(portNumber.port);
|
||||
// FIXME check that the {client, port} pair actually exists
|
||||
// snd_seq_port_info_t* src_pinfo{};
|
||||
// snd_seq_port_info_alloca(&src_pinfo);
|
||||
// snd.seq.port_info_set_client(src_pinfo, client);
|
||||
// snd.seq.port_info_set_port(src_pinfo, port);
|
||||
|
||||
// {
|
||||
// self.template error<invalid_parameter_error>(
|
||||
// self.configuration,
|
||||
// "alsa::get_port_info: invalid 'portNumber' argument: " + std::to_string(portNumber));
|
||||
// return {};
|
||||
// }
|
||||
snd_seq_addr_t addr;
|
||||
addr.client = client;
|
||||
addr.port = port;
|
||||
return addr;
|
||||
}
|
||||
|
||||
[[nodiscard]] int create_port(
|
||||
auto& /*self*/, std::string_view portName, unsigned int caps, unsigned int type,
|
||||
std::optional<int> queue)
|
||||
{
|
||||
if (this->vport < 0)
|
||||
{
|
||||
snd_seq_port_info_t* pinfo{};
|
||||
snd_seq_port_info_alloca(&pinfo);
|
||||
|
||||
snd.seq.port_info_set_name(pinfo, portName.data());
|
||||
snd.seq.port_info_set_client(pinfo, 0);
|
||||
snd.seq.port_info_set_port(pinfo, 0);
|
||||
snd.seq.port_info_set_capability(pinfo, caps);
|
||||
snd.seq.port_info_set_type(pinfo, type);
|
||||
|
||||
if (type & SND_SEQ_PORT_TYPE_MIDI_GENERIC)
|
||||
{
|
||||
snd.seq.port_info_set_midi_channels(pinfo, 16);
|
||||
}
|
||||
|
||||
if (queue)
|
||||
{
|
||||
snd.seq.port_info_set_timestamping(pinfo, 1);
|
||||
snd.seq.port_info_set_timestamp_real(pinfo, 1);
|
||||
snd.seq.port_info_set_timestamp_queue(pinfo, *queue);
|
||||
}
|
||||
|
||||
if (int err = snd.seq.create_port(this->seq, pinfo); err < 0)
|
||||
return err;
|
||||
|
||||
this->vport = snd.seq.port_info_get_port(pinfo);
|
||||
if (int err = snd.seq.get_port_info(this->seq, this->vport, pinfo); err < 0)
|
||||
return err;
|
||||
|
||||
if (auto addr = snd.seq.port_info_get_addr(pinfo))
|
||||
this->vaddr = *addr;
|
||||
else
|
||||
return -1;
|
||||
|
||||
return this->vport;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int create_connection(auto& self, snd_seq_addr_t sender, snd_seq_addr_t receiver, bool realtime)
|
||||
{
|
||||
// Create the connection between ports
|
||||
// Make subscription
|
||||
if (int err = snd.seq.port_subscribe_malloc(&this->subscription); err < 0)
|
||||
{
|
||||
self.template error<driver_error>(
|
||||
self.configuration, "create_connection: ALSA error allocation port subscription.");
|
||||
return err;
|
||||
}
|
||||
|
||||
snd.seq.port_subscribe_set_sender(this->subscription, &sender);
|
||||
snd.seq.port_subscribe_set_dest(this->subscription, &receiver);
|
||||
|
||||
if (realtime)
|
||||
{
|
||||
snd.seq.port_subscribe_set_time_update(this->subscription, 1);
|
||||
snd.seq.port_subscribe_set_time_real(this->subscription, 1);
|
||||
}
|
||||
|
||||
if (int err = snd.seq.subscribe_port(this->seq, this->subscription); err != 0)
|
||||
{
|
||||
snd.seq.port_subscribe_free(this->subscription);
|
||||
this->subscription = nullptr;
|
||||
return err;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void unsubscribe()
|
||||
{
|
||||
if (this->subscription)
|
||||
{
|
||||
snd.seq.unsubscribe_port(this->seq, this->subscription);
|
||||
snd.seq.port_subscribe_free(this->subscription);
|
||||
this->subscription = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
#pragma once
|
||||
#include <libremidi/backends/alsa_seq/config.hpp>
|
||||
#include <libremidi/backends/alsa_seq/helpers.hpp>
|
||||
#include <libremidi/backends/linux/helpers.hpp>
|
||||
#include <libremidi/detail/midi_in.hpp>
|
||||
#include <libremidi/detail/midi_stream_decoder.hpp>
|
||||
|
||||
namespace libremidi::alsa_seq
|
||||
{
|
||||
struct dummy_processing
|
||||
{
|
||||
explicit dummy_processing(auto&&...) { }
|
||||
};
|
||||
|
||||
template <typename ConfigurationImpl>
|
||||
using midi_in_base
|
||||
= std::conditional_t<ConfigurationImpl::midi_version == 1, midi1::in_api, midi2::in_api>;
|
||||
template <typename ConfigurationImpl>
|
||||
using midi_in_processing = std::conditional_t<
|
||||
ConfigurationImpl::midi_version == 1, midi1::input_state_machine, dummy_processing>;
|
||||
|
||||
template <typename ConfigurationBase, typename ConfigurationImpl>
|
||||
class midi_in_impl
|
||||
: public midi_in_base<ConfigurationImpl>
|
||||
, protected alsa_data
|
||||
, public error_handler
|
||||
{
|
||||
public:
|
||||
struct
|
||||
: ConfigurationBase
|
||||
, ConfigurationImpl
|
||||
{
|
||||
} configuration;
|
||||
midi_in_processing<ConfigurationImpl> m_processing{this->configuration};
|
||||
|
||||
bool require_timestamps() const noexcept
|
||||
{
|
||||
switch (configuration.timestamps)
|
||||
{
|
||||
case timestamp_mode::NoTimestamp:
|
||||
case timestamp_mode::SystemMonotonic:
|
||||
case timestamp_mode::AudioFrame:
|
||||
return false;
|
||||
case timestamp_mode::Absolute:
|
||||
case timestamp_mode::Relative:
|
||||
case timestamp_mode::Custom:
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
explicit midi_in_impl(ConfigurationBase&& conf, ConfigurationImpl&& apiconf)
|
||||
: midi_in_base<ConfigurationImpl>{}
|
||||
, configuration{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
if (init_client(configuration) < 0)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_in_alsa::initialize: error creating ALSA sequencer client "
|
||||
"object.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the input queue
|
||||
if (require_timestamps())
|
||||
{
|
||||
this->queue_id = snd.seq.alloc_queue(seq);
|
||||
// Set arbitrary tempo (mm=100) and resolution (240)
|
||||
snd_seq_queue_tempo_t* qtempo{};
|
||||
snd_seq_queue_tempo_alloca(&qtempo);
|
||||
snd.seq.queue_tempo_set_tempo(qtempo, 600000);
|
||||
snd.seq.queue_tempo_set_ppq(qtempo, 240);
|
||||
snd.seq.set_queue_tempo(this->seq, this->queue_id, qtempo);
|
||||
snd.seq.drain_output(this->seq);
|
||||
}
|
||||
|
||||
// Create the event -> midi encoder
|
||||
{
|
||||
int result = snd.midi.event_new(0, &coder);
|
||||
if (result < 0)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration, "midi_in_alsa::initialize: error during snd_midi_event_new.");
|
||||
return;
|
||||
}
|
||||
snd.midi.event_init(coder);
|
||||
snd.midi.event_no_status(coder, 1);
|
||||
}
|
||||
}
|
||||
|
||||
~midi_in_impl() override
|
||||
{
|
||||
// Cleanup.
|
||||
if (this->vport >= 0)
|
||||
snd.seq.delete_port(this->seq, this->vport);
|
||||
|
||||
if (require_timestamps())
|
||||
snd.seq.free_queue(this->seq, this->queue_id);
|
||||
|
||||
snd.midi.event_free(coder);
|
||||
|
||||
// Close if we do not have an user-provided client object
|
||||
if (!configuration.context)
|
||||
snd.seq.close(this->seq);
|
||||
}
|
||||
|
||||
libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_SEQ; }
|
||||
|
||||
[[nodiscard]] int create_port(std::string_view portName)
|
||||
{
|
||||
return alsa_data::create_port(
|
||||
*this, portName, SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
|
||||
SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION,
|
||||
require_timestamps() ? std::optional<int>{this->queue_id} : std::nullopt);
|
||||
}
|
||||
|
||||
void start_queue()
|
||||
{
|
||||
if (require_timestamps())
|
||||
{
|
||||
snd.seq.control_queue(this->seq, this->queue_id, SND_SEQ_EVENT_START, 0, nullptr);
|
||||
this->queue_creation_time = std::chrono::steady_clock::now();
|
||||
snd.seq.drain_output(this->seq);
|
||||
}
|
||||
}
|
||||
|
||||
void stop_queue()
|
||||
{
|
||||
if (require_timestamps())
|
||||
{
|
||||
snd.seq.control_queue(this->seq, this->queue_id, SND_SEQ_EVENT_STOP, 0, nullptr);
|
||||
snd.seq.drain_output(this->seq);
|
||||
}
|
||||
}
|
||||
|
||||
int connect_port(snd_seq_addr_t sender)
|
||||
{
|
||||
snd_seq_addr_t receiver{};
|
||||
receiver.client = snd.seq.client_id(this->seq);
|
||||
receiver.port = this->vport;
|
||||
|
||||
return create_connection(*this, sender, receiver, false);
|
||||
}
|
||||
|
||||
std::optional<snd_seq_addr_t> to_address(const port_information& p)
|
||||
{
|
||||
return alsa_data::get_port_info(p);
|
||||
}
|
||||
|
||||
int init_port(std::optional<snd_seq_addr_t> source, std::string_view portName)
|
||||
{
|
||||
this->close_port();
|
||||
|
||||
if (!source)
|
||||
return -1;
|
||||
|
||||
if (int ret = create_port(portName); ret < 0)
|
||||
{
|
||||
error<driver_error>(configuration, "midi_in_alsa::create_port: ALSA error creating port.");
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (int ret = connect_port(*source); ret < 0)
|
||||
{
|
||||
error<driver_error>(
|
||||
configuration, "midi_in_alsa::create_port: ALSA error making port connection.");
|
||||
return ret;
|
||||
}
|
||||
|
||||
start_queue();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int init_virtual_port(std::string_view portName)
|
||||
{
|
||||
this->close_port();
|
||||
|
||||
if (int ret = create_port(portName); ret < 0)
|
||||
return ret;
|
||||
|
||||
start_queue();
|
||||
return 0;
|
||||
}
|
||||
|
||||
void close_port() override
|
||||
{
|
||||
unsubscribe();
|
||||
stop_queue();
|
||||
}
|
||||
|
||||
void set_client_name(std::string_view clientName) override
|
||||
{
|
||||
alsa_data::set_client_name(clientName);
|
||||
}
|
||||
|
||||
void set_port_name(std::string_view portName) override { alsa_data::set_port_name(portName); }
|
||||
|
||||
protected:
|
||||
void set_timestamp(const auto& ev, auto& msg) noexcept
|
||||
{
|
||||
static constexpr int64_t nanos = 1e9;
|
||||
switch (configuration.timestamps)
|
||||
{
|
||||
case timestamp_mode::NoTimestamp:
|
||||
msg.timestamp = 0;
|
||||
return;
|
||||
case timestamp_mode::Relative: {
|
||||
const auto t1 = static_cast<int64_t>(ev.time.time.tv_sec) * nanos
|
||||
+ static_cast<int64_t>(ev.time.time.tv_nsec);
|
||||
const auto t0 = static_cast<int64_t>(last_time.tv_sec) * nanos
|
||||
+ static_cast<int64_t>(last_time.tv_nsec);
|
||||
const auto time = t1 - t0;
|
||||
|
||||
last_time = ev.time.time;
|
||||
|
||||
if (this->firstMessage == true)
|
||||
{
|
||||
this->firstMessage = false;
|
||||
msg.timestamp = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.timestamp = time;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case timestamp_mode::Absolute: {
|
||||
msg.timestamp = ev.time.time.tv_sec * nanos + ev.time.time.tv_nsec;
|
||||
break;
|
||||
}
|
||||
case timestamp_mode::Custom: {
|
||||
msg.timestamp
|
||||
= configuration.get_timestamp(ev.time.time.tv_sec * nanos + ev.time.time.tv_nsec);
|
||||
break;
|
||||
}
|
||||
case timestamp_mode::SystemMonotonic: {
|
||||
namespace clk = std::chrono;
|
||||
msg.timestamp = system_ns();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
timestamp absolute_timestamp() const noexcept override
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||
std::chrono::steady_clock::now() - this->queue_creation_time)
|
||||
.count();
|
||||
}
|
||||
|
||||
int process_event(const snd_seq_event_t& ev)
|
||||
{
|
||||
if constexpr (ConfigurationImpl::midi_version == 1)
|
||||
{
|
||||
switch (ev.type)
|
||||
{
|
||||
case SND_SEQ_EVENT_PORT_SUBSCRIBED:
|
||||
case SND_SEQ_EVENT_PORT_UNSUBSCRIBED:
|
||||
return 0;
|
||||
case SND_SEQ_EVENT_SYSEX: {
|
||||
if (configuration.ignore_sysex)
|
||||
return 0;
|
||||
else if (ev.data.ext.len > decoding_buffer.size())
|
||||
decoding_buffer.resize(ev.data.ext.len);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr timestamp_backend_info timestamp_info{
|
||||
.has_absolute_timestamps = true,
|
||||
.absolute_is_monotonic = false,
|
||||
.has_samples = false,
|
||||
};
|
||||
|
||||
const auto to_ns = [ts = ev.time.time] {
|
||||
return static_cast<int64_t>(ts.tv_sec) * 1'000'000'000 + static_cast<int64_t>(ts.tv_nsec);
|
||||
};
|
||||
auto buf = decoding_buffer.data();
|
||||
auto buf_space = decoding_buffer.size();
|
||||
|
||||
// FIXME according to the doc snd_midi_event_decode can apparently return multiple events????
|
||||
const auto avail = snd.midi.event_decode(coder, buf, buf_space, &ev);
|
||||
if (avail > 0)
|
||||
{
|
||||
m_processing.on_bytes(
|
||||
{buf, buf + avail}, m_processing.template timestamp<timestamp_info>(to_ns, 0));
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return avail;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int process_events()
|
||||
{
|
||||
if constexpr (ConfigurationImpl::midi_version == 1)
|
||||
{
|
||||
snd_seq_event_t* ev{};
|
||||
event_handle handle{snd};
|
||||
int result = 0;
|
||||
while ((result = snd.seq.event_input(seq, &ev)) > 0)
|
||||
{
|
||||
handle.reset(ev);
|
||||
if (int err = process_event(*ev); err < 0)
|
||||
return err;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
#if __has_include(<alsa/ump.h>)
|
||||
int process_ump_event(const snd_seq_ump_event_t& ev)
|
||||
{
|
||||
// Filter the message types before any decoding
|
||||
switch (ev.type)
|
||||
{
|
||||
case SND_SEQ_EVENT_PORT_SUBSCRIBED:
|
||||
case SND_SEQ_EVENT_PORT_UNSUBSCRIBED:
|
||||
return 0;
|
||||
|
||||
case SND_SEQ_EVENT_QFRAME: // MIDI time code
|
||||
case SND_SEQ_EVENT_TICK: // 0xF9 ... MIDI timing tick
|
||||
case SND_SEQ_EVENT_CLOCK: // 0xF8 ... MIDI timing (clock) tick
|
||||
if (configuration.ignore_timing)
|
||||
return 0;
|
||||
break;
|
||||
|
||||
case SND_SEQ_EVENT_SENSING: // Active sensing
|
||||
if (configuration.ignore_sensing)
|
||||
return 0;
|
||||
break;
|
||||
|
||||
case SND_SEQ_EVENT_SYSEX: {
|
||||
if (configuration.ignore_sysex)
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// MIDI 2 : no decoder, we can just send the UMP data directly, yay
|
||||
libremidi::ump ump;
|
||||
std::memcpy(ump.data, ev.ump, sizeof(ev.ump));
|
||||
set_timestamp(ev, ump);
|
||||
configuration.on_message(std::move(ump));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int process_ump_events()
|
||||
{
|
||||
snd_seq_ump_event_t* ev{};
|
||||
event_handle handle{snd};
|
||||
int result = 0;
|
||||
while ((result = snd.seq.ump.event_input(seq, &ev)) > 0)
|
||||
{
|
||||
handle.reset((snd_seq_event_t*)ev);
|
||||
if (int err = process_ump_event(*ev); err < 0)
|
||||
return err;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
int queue_id{}; // an input queue is needed to get timestamped events
|
||||
snd_seq_real_time_t last_time{};
|
||||
|
||||
// Only needed for midi 1
|
||||
std::vector<unsigned char> decoding_buffer = std::vector<unsigned char>(4096);
|
||||
std::chrono::steady_clock::time_point queue_creation_time;
|
||||
};
|
||||
|
||||
template <typename ConfigurationBase, typename ConfigurationImpl>
|
||||
class midi_in_alsa_threaded : public midi_in_impl<ConfigurationBase, ConfigurationImpl>
|
||||
{
|
||||
public:
|
||||
midi_in_alsa_threaded(ConfigurationBase&& conf, ConfigurationImpl&& apiconf)
|
||||
: midi_in_impl<ConfigurationBase, ConfigurationImpl>{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
if (this->termination_event < 0)
|
||||
{
|
||||
this->template error<driver_error>(
|
||||
this->configuration, "midi_in_alsa::initialize: error creating eventfd.");
|
||||
}
|
||||
}
|
||||
|
||||
~midi_in_alsa_threaded() { this->close_port(); }
|
||||
|
||||
private:
|
||||
bool open_port(const input_port& pt, std::string_view local_port_name) override
|
||||
{
|
||||
if (int err = this->init_port(this->to_address(pt), local_port_name); err < 0)
|
||||
return false;
|
||||
|
||||
if (!start_thread())
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool open_virtual_port(std::string_view portName) override
|
||||
{
|
||||
if (int err = this->init_virtual_port(portName); err < 0)
|
||||
return false;
|
||||
|
||||
if (!this->start_thread())
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void close_port() override
|
||||
{
|
||||
midi_in_impl<ConfigurationBase, ConfigurationImpl>::close_port();
|
||||
|
||||
stop_thread();
|
||||
}
|
||||
|
||||
[[nodiscard]] int start_thread()
|
||||
{
|
||||
try
|
||||
{
|
||||
this->thread = std::thread([this] { thread_handler(); });
|
||||
}
|
||||
catch (const std::system_error& e)
|
||||
{
|
||||
using namespace std::literals;
|
||||
this->unsubscribe();
|
||||
|
||||
this->template error<thread_error>(
|
||||
this->configuration,
|
||||
"midi_in_alsa::start_thread: error starting MIDI input thread: "s + e.what());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop_thread()
|
||||
{
|
||||
termination_event.notify();
|
||||
|
||||
if (this->thread.joinable())
|
||||
this->thread.join();
|
||||
|
||||
termination_event.consume();
|
||||
}
|
||||
|
||||
void thread_handler()
|
||||
{
|
||||
int poll_fd_count = alsa_data::snd.seq.poll_descriptors_count(this->seq, POLLIN) + 1;
|
||||
auto poll_fds = (struct pollfd*)alloca(poll_fd_count * sizeof(struct pollfd));
|
||||
poll_fds[0] = this->termination_event;
|
||||
alsa_data::snd.seq.poll_descriptors(this->seq, poll_fds + 1, poll_fd_count - 1, POLLIN);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
if (alsa_data::snd.seq.event_input_pending(this->seq, 1) == 0)
|
||||
{
|
||||
// No data pending
|
||||
if (poll(poll_fds, poll_fd_count, -1) >= 0)
|
||||
{
|
||||
// We got our stop-thread signal
|
||||
if (termination_event.ready(poll_fds[0]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
int res{};
|
||||
if constexpr (ConfigurationImpl::midi_version == 1)
|
||||
{
|
||||
res = this->process_events();
|
||||
}
|
||||
#if __has_include(<alsa/ump.h>)
|
||||
else if constexpr (ConfigurationImpl::midi_version == 2)
|
||||
{
|
||||
res = this->process_ump_events();
|
||||
}
|
||||
#endif
|
||||
|
||||
(void)res;
|
||||
#if defined(__LIBREMIDI_DEBUG__)
|
||||
if (res < 0)
|
||||
std::cerr << "midi_in_alsa::thread_handler: MIDI input error: " << snd.strerror(res)
|
||||
<< "\n";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
std::thread thread{};
|
||||
eventfd_notifier termination_event{};
|
||||
};
|
||||
|
||||
template <typename ConfigurationBase, typename ConfigurationImpl>
|
||||
class midi_in_alsa_manual : public midi_in_impl<ConfigurationBase, ConfigurationImpl>
|
||||
{
|
||||
public:
|
||||
midi_in_alsa_manual(ConfigurationBase&& conf, ConfigurationImpl&& apiconf)
|
||||
: midi_in_impl<ConfigurationBase, ConfigurationImpl>{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
assert(this->configuration.manual_poll);
|
||||
assert(this->configuration.stop_poll);
|
||||
}
|
||||
|
||||
[[nodiscard]] int init_callback()
|
||||
{
|
||||
using poll_params = typename ConfigurationImpl::poll_parameters_type;
|
||||
this->configuration.manual_poll(
|
||||
poll_params{.addr = this->vaddr, .callback = [this](const auto& ev) {
|
||||
if constexpr (ConfigurationImpl::midi_version == 1)
|
||||
return this->process_event(ev);
|
||||
#if __has_include(<alsa/ump.h>)
|
||||
else
|
||||
return this->process_ump_event(ev);
|
||||
#endif
|
||||
}});
|
||||
return 0;
|
||||
}
|
||||
|
||||
~midi_in_alsa_manual() { this->close_port(); }
|
||||
|
||||
bool open_port(const input_port& pt, std::string_view local_port_name) override
|
||||
{
|
||||
if (int err = this->init_port(this->to_address(pt), local_port_name); err < 0)
|
||||
return false;
|
||||
|
||||
if (int err = init_callback(); err < 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool open_virtual_port(std::string_view name) override
|
||||
{
|
||||
if (int err = this->init_virtual_port(name); err < 0)
|
||||
return false;
|
||||
|
||||
if (int err = init_callback(); err < 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void close_port() override
|
||||
{
|
||||
this->configuration.stop_poll(this->vaddr);
|
||||
|
||||
midi_in_impl<ConfigurationBase, ConfigurationImpl>::close_port();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
template <>
|
||||
inline std::unique_ptr<midi_in_api>
|
||||
make<alsa_seq::midi_in_impl<libremidi::input_configuration, alsa_seq::input_configuration>>(
|
||||
libremidi::input_configuration&& conf, libremidi::alsa_seq::input_configuration&& api)
|
||||
{
|
||||
if (api.manual_poll)
|
||||
return std::make_unique<alsa_seq::midi_in_alsa_manual<
|
||||
libremidi::input_configuration, alsa_seq::input_configuration>>(
|
||||
std::move(conf), std::move(api));
|
||||
else
|
||||
return std::make_unique<alsa_seq::midi_in_alsa_threaded<
|
||||
libremidi::input_configuration, alsa_seq::input_configuration>>(
|
||||
std::move(conf), std::move(api));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
#pragma once
|
||||
#include <libremidi/backends/alsa_seq/config.hpp>
|
||||
#include <libremidi/backends/alsa_seq/helpers.hpp>
|
||||
#include <libremidi/detail/midi_out.hpp>
|
||||
|
||||
namespace libremidi::alsa_seq
|
||||
{
|
||||
|
||||
class midi_out_impl final
|
||||
: public midi1::out_api
|
||||
, private alsa_data
|
||||
, public error_handler
|
||||
{
|
||||
public:
|
||||
struct
|
||||
: libremidi::output_configuration
|
||||
, alsa_seq::output_configuration
|
||||
{
|
||||
} configuration;
|
||||
|
||||
midi_out_impl(libremidi::output_configuration&& conf, alsa_seq::output_configuration&& apiconf)
|
||||
: configuration{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
if (init_client(configuration) < 0)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_in_alsa::initialize: error creating ALSA sequencer client "
|
||||
"object.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (snd.midi.event_new(this->bufferSize, &this->coder) < 0)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_out_alsa::initialize: error initializing MIDI event "
|
||||
"parser.");
|
||||
return;
|
||||
}
|
||||
snd.midi.event_init(this->coder);
|
||||
}
|
||||
|
||||
~midi_out_impl() override
|
||||
{
|
||||
// Close a connection if it exists.
|
||||
midi_out_impl::close_port();
|
||||
|
||||
// Cleanup.
|
||||
if (this->vport >= 0)
|
||||
snd.seq.delete_port(this->seq, this->vport);
|
||||
if (this->coder)
|
||||
snd.midi.event_free(this->coder);
|
||||
|
||||
if (!configuration.context)
|
||||
snd.seq.close(this->seq);
|
||||
}
|
||||
|
||||
libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_SEQ; }
|
||||
|
||||
[[nodiscard]] int create_port(std::string_view portName)
|
||||
{
|
||||
return alsa_data::create_port(
|
||||
*this, portName, SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ,
|
||||
SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION, std::nullopt);
|
||||
}
|
||||
|
||||
bool open_port(const output_port& p, std::string_view portName) override
|
||||
{
|
||||
unsigned int nSrc = this->get_port_count(SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE);
|
||||
if (nSrc < 1)
|
||||
{
|
||||
error<no_devices_found_error>(
|
||||
this->configuration, "midi_out_alsa::open_port: no MIDI output sources found!");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto sink = get_port_info(p);
|
||||
if (!sink)
|
||||
return false;
|
||||
|
||||
if (int err = create_port(portName); err < 0)
|
||||
{
|
||||
error<driver_error>(configuration, "midi_out_alsa::create_port: ALSA error creating port.");
|
||||
return false;
|
||||
}
|
||||
|
||||
snd_seq_addr_t source{
|
||||
.client = (unsigned char)snd.seq.client_id(this->seq), .port = (unsigned char)this->vport};
|
||||
if (int err = create_connection(*this, source, *sink, true); err < 0)
|
||||
{
|
||||
error<driver_error>(
|
||||
configuration, "midi_out_alsa::create_port: ALSA error making port connection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool open_virtual_port(std::string_view portName) override
|
||||
{
|
||||
if (int err = create_port(portName); err < 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void close_port() override { unsubscribe(); }
|
||||
|
||||
void set_client_name(std::string_view clientName) override
|
||||
{
|
||||
alsa_data::set_client_name(clientName);
|
||||
}
|
||||
|
||||
void set_port_name(std::string_view portName) override { alsa_data::set_port_name(portName); }
|
||||
|
||||
void send_message(const unsigned char* message, std::size_t size) override
|
||||
{
|
||||
int64_t result{};
|
||||
if (size > this->bufferSize)
|
||||
{
|
||||
this->bufferSize = size;
|
||||
result = snd.midi.event_resize_buffer(this->coder, size);
|
||||
if (result != 0)
|
||||
{
|
||||
error<driver_error>(
|
||||
this->configuration,
|
||||
"midi_out_alsa::send_message: ALSA error resizing MIDI event "
|
||||
"buffer.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t offset = 0;
|
||||
while (offset < size)
|
||||
{
|
||||
snd_seq_event_t ev;
|
||||
snd_seq_ev_clear(&ev);
|
||||
snd_seq_ev_set_source(&ev, this->vport);
|
||||
snd_seq_ev_set_subs(&ev);
|
||||
// FIXME direct is set but snd_seq_event_output_direct is not used...
|
||||
snd_seq_ev_set_direct(&ev);
|
||||
|
||||
const int64_t nBytes = size; // signed to avoir potential overflow with size - offset below
|
||||
result = snd.midi.event_encode(this->coder, message + offset, (long)(nBytes - offset), &ev);
|
||||
if (result < 0)
|
||||
{
|
||||
warning(this->configuration, "midi_out_alsa::send_message: event parsing error!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.type == SND_SEQ_EVENT_NONE)
|
||||
{
|
||||
warning(this->configuration, "midi_out_alsa::send_message: incomplete message!");
|
||||
return;
|
||||
}
|
||||
|
||||
offset += result;
|
||||
|
||||
result = snd.seq.event_output(this->seq, &ev);
|
||||
if (result < 0)
|
||||
{
|
||||
warning(
|
||||
this->configuration,
|
||||
"midi_out_alsa::send_message: error sending MIDI message to port.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
snd.seq.drain_output(this->seq);
|
||||
}
|
||||
|
||||
private:
|
||||
uint64_t bufferSize{32};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
#pragma once
|
||||
#include <libremidi/backends/alsa_seq/config.hpp>
|
||||
#include <libremidi/backends/alsa_seq/helpers.hpp>
|
||||
#include <libremidi/backends/linux/helpers.hpp>
|
||||
#include <libremidi/detail/observer.hpp>
|
||||
|
||||
#include <alsa/asoundlib.h>
|
||||
|
||||
#include <bitset>
|
||||
#include <map>
|
||||
|
||||
namespace libremidi::alsa_seq
|
||||
{
|
||||
|
||||
struct port_info
|
||||
{
|
||||
std::string client_name;
|
||||
std::string port_name;
|
||||
int client{};
|
||||
int port{};
|
||||
bool isInput{};
|
||||
bool isOutput{};
|
||||
};
|
||||
|
||||
template <typename ConfigurationImpl>
|
||||
class observer_impl
|
||||
: public observer_api
|
||||
, public alsa_data
|
||||
{
|
||||
public:
|
||||
struct
|
||||
: libremidi::observer_configuration
|
||||
, ConfigurationImpl
|
||||
{
|
||||
} configuration;
|
||||
|
||||
explicit observer_impl(libremidi::observer_configuration&& conf, ConfigurationImpl&& apiconf)
|
||||
: configuration{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
using namespace std::literals;
|
||||
if (int err = init_client(configuration); err < 0)
|
||||
{
|
||||
throw driver_error("observer_alsa: snd_seq_open failed");
|
||||
}
|
||||
|
||||
if (!configuration.has_callbacks())
|
||||
return;
|
||||
|
||||
// Init with the existing ports
|
||||
if (configuration.notify_in_constructor)
|
||||
init_all_ports();
|
||||
|
||||
// Create the port to listen on the server events
|
||||
{
|
||||
#if __has_include(<alsa/ump.h>)
|
||||
constexpr int midi2_cap
|
||||
= ConfigurationImpl::midi_version == 2 ? SND_SEQ_PORT_CAP_UMP_ENDPOINT : 0;
|
||||
#else
|
||||
constexpr int midi2_cap = 0;
|
||||
#endif
|
||||
|
||||
constexpr int caps = SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_WRITE
|
||||
| SND_SEQ_PORT_CAP_SUBS_READ | SND_SEQ_PORT_CAP_SUBS_WRITE | midi2_cap;
|
||||
int err = alsa_data::create_port(
|
||||
*this, "libremidi-observe", caps, SND_SEQ_PORT_TYPE_APPLICATION, false);
|
||||
if (err < 0)
|
||||
{
|
||||
throw driver_error("observer: ALSA error creating port.");
|
||||
}
|
||||
}
|
||||
|
||||
// Connect the ALSA server events to our port
|
||||
{
|
||||
int err
|
||||
= snd.seq.connect_from(seq, vport, SND_SEQ_CLIENT_SYSTEM, SND_SEQ_PORT_SYSTEM_ANNOUNCE);
|
||||
if (err < 0)
|
||||
{
|
||||
throw driver_error("observer_alsa: snd_seq_connect_from failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<port_info> get_info(int client, int port) const noexcept
|
||||
{
|
||||
port_info p;
|
||||
p.client = client;
|
||||
p.port = port;
|
||||
|
||||
snd_seq_client_info_t* cinfo;
|
||||
snd_seq_client_info_alloca(&cinfo);
|
||||
if (int err = snd.seq.get_any_client_info(seq, client, cinfo); err < 0)
|
||||
return std::nullopt;
|
||||
|
||||
snd_seq_port_info_t* pinfo;
|
||||
snd_seq_port_info_alloca(&pinfo);
|
||||
if (int err = snd.seq.get_any_port_info(seq, client, port, pinfo); err < 0)
|
||||
return std::nullopt;
|
||||
|
||||
const auto tp = snd.seq.port_info_get_type(pinfo);
|
||||
bool ok = this->configuration.track_any;
|
||||
if ((tp & SND_SEQ_PORT_TYPE_HARDWARE) && this->configuration.track_hardware)
|
||||
ok = true;
|
||||
else if ((tp & SND_SEQ_PORT_TYPE_SOFTWARE) && this->configuration.track_virtual)
|
||||
ok = true;
|
||||
if (!ok)
|
||||
return {};
|
||||
|
||||
if (auto name = snd.seq.client_info_get_name(cinfo))
|
||||
p.client_name = name;
|
||||
|
||||
if (auto name = snd.seq.port_info_get_name(pinfo))
|
||||
p.port_name = name;
|
||||
|
||||
auto cap = snd.seq.port_info_get_capability(pinfo);
|
||||
p.isInput = (cap & SND_SEQ_PORT_CAP_DUPLEX) | (cap & SND_SEQ_PORT_CAP_READ);
|
||||
p.isOutput = (cap & SND_SEQ_PORT_CAP_DUPLEX) | (cap & SND_SEQ_PORT_CAP_WRITE);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
template <bool Input>
|
||||
auto to_port_info(port_info p) const noexcept
|
||||
-> std::conditional_t<Input, input_port, output_port>
|
||||
{
|
||||
static_assert(sizeof(this->seq) <= sizeof(libremidi::client_handle));
|
||||
static_assert(sizeof(std::uintptr_t) <= sizeof(libremidi::client_handle));
|
||||
return {
|
||||
{.client = std::uintptr_t(this->seq),
|
||||
.port = alsa_seq::seq_to_port_handle(p.client, p.port),
|
||||
.manufacturer = "",
|
||||
.device_name = p.client_name,
|
||||
.port_name = p.port_name,
|
||||
.display_name = p.port_name}};
|
||||
}
|
||||
|
||||
void init_all_ports()
|
||||
{
|
||||
alsa_seq::for_all_ports(
|
||||
snd, this->seq, [this](snd_seq_client_info_t& client, snd_seq_port_info_t& port) {
|
||||
int clt = snd.seq.client_info_get_client(&client);
|
||||
int pt = snd.seq.port_info_get_port(&port);
|
||||
register_port(clt, pt);
|
||||
});
|
||||
}
|
||||
|
||||
libremidi::API get_current_api() const noexcept override
|
||||
{
|
||||
if constexpr (ConfigurationImpl::midi_version == 1)
|
||||
return libremidi::API::ALSA_SEQ;
|
||||
else
|
||||
return libremidi::API::ALSA_SEQ_UMP;
|
||||
}
|
||||
|
||||
std::vector<libremidi::input_port> get_input_ports() const noexcept override
|
||||
{
|
||||
std::vector<libremidi::input_port> ret;
|
||||
alsa_seq::for_all_ports(
|
||||
snd, this->seq, [this, &ret](snd_seq_client_info_t& client, snd_seq_port_info_t& port) {
|
||||
int clt = snd.seq.client_info_get_client(&client);
|
||||
int pt = snd.seq.port_info_get_port(&port);
|
||||
if (auto p = get_info(clt, pt))
|
||||
if (p->isInput)
|
||||
ret.push_back(to_port_info<true>(*p));
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<libremidi::output_port> get_output_ports() const noexcept override
|
||||
{
|
||||
std::vector<libremidi::output_port> ret;
|
||||
alsa_seq::for_all_ports(
|
||||
snd, this->seq, [this, &ret](snd_seq_client_info_t& client, snd_seq_port_info_t& port) {
|
||||
int clt = snd.seq.client_info_get_client(&client);
|
||||
int pt = snd.seq.port_info_get_port(&port);
|
||||
if (auto p = get_info(clt, pt))
|
||||
if (p->isOutput)
|
||||
ret.push_back(to_port_info<false>(*p));
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
void register_port(int client, int port)
|
||||
{
|
||||
auto pp = get_info(client, port);
|
||||
if (!pp)
|
||||
return;
|
||||
auto& p = *pp;
|
||||
if (p.client == snd.seq.client_id(seq))
|
||||
return;
|
||||
|
||||
knownClients_[{p.client, p.port}] = p;
|
||||
if (p.isInput && configuration.input_added)
|
||||
{
|
||||
configuration.input_added(to_port_info<true>(p));
|
||||
}
|
||||
|
||||
if (p.isOutput && configuration.output_added)
|
||||
{
|
||||
configuration.output_added(to_port_info<false>(p));
|
||||
}
|
||||
}
|
||||
|
||||
void unregister_port(int client, int port)
|
||||
{
|
||||
auto it = knownClients_.find({client, port});
|
||||
if (it != knownClients_.end())
|
||||
{
|
||||
auto p = it->second;
|
||||
knownClients_.erase(it);
|
||||
|
||||
if (p.isInput && configuration.input_removed)
|
||||
{
|
||||
configuration.input_removed(to_port_info<true>(p));
|
||||
}
|
||||
|
||||
if (p.isOutput && configuration.output_removed)
|
||||
{
|
||||
configuration.output_removed(to_port_info<false>(p));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void handle_event(const snd_seq_event_t& ev)
|
||||
{
|
||||
switch (ev.type)
|
||||
{
|
||||
case SND_SEQ_EVENT_PORT_START: {
|
||||
register_port(ev.data.addr.client, ev.data.addr.port);
|
||||
break;
|
||||
}
|
||||
case SND_SEQ_EVENT_PORT_EXIT: {
|
||||
unregister_port(ev.data.addr.client, ev.data.addr.port);
|
||||
break;
|
||||
}
|
||||
case SND_SEQ_EVENT_PORT_CHANGE:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
~observer_impl()
|
||||
{
|
||||
if (seq)
|
||||
{
|
||||
if (vport)
|
||||
snd.seq.delete_port(seq, vport);
|
||||
|
||||
if (!configuration.context)
|
||||
snd.seq.close(seq);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::pair<int, int>, port_info> knownClients_;
|
||||
};
|
||||
|
||||
template <typename ConfigurationImpl>
|
||||
class observer_threaded : public observer_impl<ConfigurationImpl>
|
||||
{
|
||||
public:
|
||||
observer_threaded(libremidi::observer_configuration&& conf, ConfigurationImpl&& apiconf)
|
||||
: observer_impl<ConfigurationImpl>{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
// Create relevant descriptors
|
||||
auto& snd = alsa_data::snd;
|
||||
const auto N = snd.seq.poll_descriptors_count(this->seq, POLLIN);
|
||||
descriptors_.resize(N + 1);
|
||||
snd.seq.poll_descriptors(this->seq, descriptors_.data(), N, POLLIN);
|
||||
descriptors_.back() = this->termination_event;
|
||||
|
||||
// Start the listening thread
|
||||
thread = std::thread{[this] {
|
||||
auto& snd = alsa_data::snd;
|
||||
for (;;)
|
||||
{
|
||||
int err = poll(descriptors_.data(), descriptors_.size(), -1);
|
||||
if (err >= 0)
|
||||
{
|
||||
// We got our stop-thread signal
|
||||
if (descriptors_.back().revents & POLLIN)
|
||||
break;
|
||||
|
||||
snd_seq_event_t* ev{};
|
||||
event_handle handle{snd};
|
||||
while (snd.seq.event_input(this->seq, &ev) >= 0)
|
||||
{
|
||||
handle.reset(ev);
|
||||
this->handle_event(*ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
~observer_threaded()
|
||||
{
|
||||
termination_event.notify();
|
||||
|
||||
if (thread.joinable())
|
||||
thread.join();
|
||||
}
|
||||
|
||||
eventfd_notifier termination_event{};
|
||||
std::thread thread;
|
||||
std::vector<pollfd> descriptors_;
|
||||
};
|
||||
|
||||
template <typename ConfigurationImpl>
|
||||
class observer_manual : public observer_impl<ConfigurationImpl>
|
||||
{
|
||||
public:
|
||||
observer_manual(libremidi::observer_configuration&& conf, ConfigurationImpl&& apiconf)
|
||||
: observer_impl<ConfigurationImpl>{std::move(conf), std::move(apiconf)}
|
||||
{
|
||||
this->configuration.manual_poll(
|
||||
poll_parameters{.addr = this->vaddr, .callback = [this](const auto& v) {
|
||||
this->handle_event(v);
|
||||
return 0;
|
||||
}});
|
||||
}
|
||||
|
||||
~observer_manual() { this->configuration.stop_poll(this->vaddr); }
|
||||
};
|
||||
}
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
template <>
|
||||
inline std::unique_ptr<observer_api>
|
||||
make<alsa_seq::observer_impl<alsa_seq::observer_configuration>>(
|
||||
libremidi::observer_configuration&& conf, libremidi::alsa_seq::observer_configuration&& api)
|
||||
{
|
||||
if (api.manual_poll)
|
||||
return std::make_unique<alsa_seq::observer_manual<alsa_seq::observer_configuration>>(
|
||||
std::move(conf), std::move(api));
|
||||
else
|
||||
return std::make_unique<alsa_seq::observer_threaded<alsa_seq::observer_configuration>>(
|
||||
std::move(conf), std::move(api));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
#pragma once
|
||||
#if __has_include(<boost/lockfree/spsc_queue.hpp>)
|
||||
#include <libremidi/backends/alsa_seq/config.hpp>
|
||||
#include <libremidi/backends/alsa_seq/helpers.hpp>
|
||||
#include <libremidi/backends/linux/helpers.hpp>
|
||||
#include <libremidi/shared_context.hpp>
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace libremidi::alsa_seq
|
||||
{
|
||||
|
||||
struct shared_handler : public libremidi::shared_context
|
||||
{
|
||||
const libasound& snd = libasound::instance();
|
||||
struct equals_addr
|
||||
{
|
||||
constexpr bool operator()(const snd_seq_addr_t& lhs, const snd_seq_addr_t& rhs) noexcept
|
||||
{
|
||||
return lhs.client == rhs.client && lhs.port == rhs.port;
|
||||
}
|
||||
};
|
||||
|
||||
explicit shared_handler(std::string_view v)
|
||||
{
|
||||
if (int err = snd.seq.open(&client, "default", SND_SEQ_OPEN_DUPLEX, SND_SEQ_NONBLOCK); err < 0)
|
||||
{
|
||||
client = nullptr;
|
||||
// fixme throw?
|
||||
return;
|
||||
}
|
||||
|
||||
if (!v.empty())
|
||||
snd.seq.set_client_name(client, v.data());
|
||||
|
||||
// Last descriptor is the eventfd one
|
||||
int fds_size = snd.seq.poll_descriptors_count(client, POLLIN);
|
||||
fds.reserve(fds_size + 2);
|
||||
fds.resize(fds_size);
|
||||
snd.seq.poll_descriptors(client, fds.data(), fds_size, POLLIN);
|
||||
fds.push_back(termination_event);
|
||||
fds.push_back(queue_event);
|
||||
}
|
||||
|
||||
void start_processing() override
|
||||
{
|
||||
thread = std::thread{[this] { process(); }};
|
||||
}
|
||||
|
||||
void stop_processing() override
|
||||
{
|
||||
termination_event.notify();
|
||||
if (thread.joinable())
|
||||
thread.join();
|
||||
termination_event.consume();
|
||||
}
|
||||
|
||||
static shared_configurations make(std::string_view client_name)
|
||||
{
|
||||
auto clt = std::make_shared<shared_handler>(client_name);
|
||||
|
||||
auto cb = [client = std::weak_ptr{clt}](const libremidi::alsa_seq::poll_parameters& params) {
|
||||
if (auto clt = client.lock())
|
||||
{
|
||||
clt->events.push(
|
||||
{.type = shared_handler::event_type::callback_added, .payload = std::move(params)});
|
||||
clt->queue_event.notify();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
auto stop_cb = [client = std::weak_ptr{clt}](snd_seq_addr_t id) {
|
||||
if (auto clt = client.lock())
|
||||
{
|
||||
clt->events.push(
|
||||
{.type = shared_handler::event_type::callback_removed, .payload = std::move(id)});
|
||||
clt->queue_event.notify();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return {
|
||||
.context = clt,
|
||||
.observer = alsa_seq::
|
||||
observer_configuration{.context = clt->client, .manual_poll = cb, .stop_poll = stop_cb},
|
||||
.in = alsa_seq::
|
||||
input_configuration{.context = clt->client, .manual_poll = cb, .stop_poll = stop_cb},
|
||||
.out = alsa_seq::output_configuration{.context = clt->client},
|
||||
};
|
||||
}
|
||||
|
||||
int64_t index_of_address(snd_seq_addr_t addr)
|
||||
{
|
||||
auto it = std::find_if(addresses.begin(), addresses.end(), [=](snd_seq_addr_t other) {
|
||||
return equals_addr{}(addr, other);
|
||||
});
|
||||
if (it != addresses.end())
|
||||
{
|
||||
return std::distance(addresses.begin(), it);
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void process()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
int err = poll(fds.data(), fds.size(), -1);
|
||||
if (err < 0)
|
||||
return;
|
||||
// Check for termination signal
|
||||
if (termination_event.ready(fds[fds.size() - 2]))
|
||||
return;
|
||||
|
||||
// Check for queue processing signal
|
||||
if (queue_event.ready(fds[fds.size() - 1]))
|
||||
{
|
||||
this->queue_event.consume();
|
||||
|
||||
event ev;
|
||||
while (this->events.pop(ev))
|
||||
{
|
||||
switch (ev.type)
|
||||
{
|
||||
case callback_added: {
|
||||
auto [addr, cb]
|
||||
= std::move(*std::get_if<libremidi::alsa_seq::poll_parameters>(&ev.payload));
|
||||
addresses.push_back(addr);
|
||||
callbacks.push_back(std::move(cb));
|
||||
break;
|
||||
}
|
||||
case callback_removed:
|
||||
auto addr = std::move(*std::get_if<snd_seq_addr_t>(&ev.payload));
|
||||
if (auto index = index_of_address(addr); index >= 0)
|
||||
{
|
||||
addresses.erase(addresses.begin() + index);
|
||||
callbacks.erase(callbacks.begin() + index);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for who's ready
|
||||
for (int64_t i = 0, N = std::ssize(fds) - 2; i < N; i++)
|
||||
{
|
||||
if (fds[i].revents & POLLIN)
|
||||
{
|
||||
// Read alsa event
|
||||
snd_seq_event_t* ev{};
|
||||
event_handle handle{snd};
|
||||
int result = 0;
|
||||
while ((result = snd.seq.event_input(client, &ev)) > 0)
|
||||
{
|
||||
handle.reset(ev);
|
||||
|
||||
if (auto index = index_of_address(ev->dest); index >= 0)
|
||||
{
|
||||
// Dispatch the event to the correct observer or midi_in object
|
||||
int err = callbacks[index](*ev);
|
||||
if (err < 0 && err != -EAGAIN)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~shared_handler() { snd.seq.close(client); }
|
||||
|
||||
enum event_type
|
||||
{
|
||||
callback_added,
|
||||
callback_removed,
|
||||
};
|
||||
struct event
|
||||
{
|
||||
event_type type;
|
||||
std::variant<libremidi::alsa_seq::poll_parameters, snd_seq_addr_t> payload;
|
||||
};
|
||||
|
||||
snd_seq_t* client{};
|
||||
|
||||
boost::lockfree::spsc_queue<event> events{16};
|
||||
std::vector<snd_seq_addr_t> addresses;
|
||||
std::vector<std::function<int(const snd_seq_event_t&)>> callbacks;
|
||||
std::vector<pollfd> fds;
|
||||
eventfd_notifier termination_event, queue_event{false};
|
||||
std::thread thread;
|
||||
};
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user