very, very simple WP with midi CC out

This commit is contained in:
Sebastian
2024-10-09 02:01:23 +02:00
commit 3d6fc151f1
126 changed files with 19539 additions and 0 deletions
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <libremidi/backends/alsa_raw/midi_in.hpp>
#include <libremidi/backends/alsa_raw/midi_out.hpp>
#include <libremidi/backends/alsa_raw/observer.hpp>
// Credits: greatly inspired from
// https://ccrma.stanford.edu/~craig/articles/linuxmidi/alsa-1.0/alsarawmidiout.c
// https://ccrma.stanford.edu/~craig/articles/linuxmidi/alsa-1.0/alsarawportlist.c
// Thanks Craig Stuart Sapp <craig@ccrma.stanford.edu>
namespace libremidi::alsa_raw
{
struct backend
{
using midi_in = midi_in_impl;
using midi_out = midi_out_impl;
using midi_observer = observer_impl;
using midi_in_configuration = alsa_raw_input_configuration;
using midi_out_configuration = alsa_raw_output_configuration;
using midi_observer_configuration = alsa_raw_observer_configuration;
static const constexpr auto API = libremidi::API::ALSA_RAW;
static const constexpr auto name = "alsa_raw";
static const constexpr auto display_name = "ALSA (raw)";
static inline bool available() noexcept
{
static const libasound& snd = libasound::instance();
return snd.available && snd.rawmidi.available;
}
};
}
@@ -0,0 +1,79 @@
#pragma once
#include <libremidi/config.hpp>
#include <chrono>
#include <functional>
#include <optional>
#include <span>
#include <thread>
#if __has_include(<poll.h>)
#include <poll.h>
namespace libremidi
{
using poll_descriptors = pollfd;
}
#else
namespace libremidi
{
struct poll_descriptors
{
int fd;
short int events;
short int revents;
};
}
#endif
namespace libremidi
{
/**
* Used to determine how large sent messages will be chunked.
*/
struct LIBREMIDI_EXPORT chunking_parameters
{
std::chrono::milliseconds interval{};
int32_t size{};
/**
* @brief Will be called by the chunking code to allow the API user to wait.
*
* By default just calls sleep.
* Arguments are: the time that must be waited, the bytes currently written.
* Return false if you want to abort the transfer, and true otherwise.
*/
std::function<bool(std::chrono::microseconds, int64_t)> wait = chunking_parameters::default_wait;
static bool default_wait(std::chrono::microseconds time_to_wait, int64_t /*written_bytes*/)
{
std::this_thread::sleep_for(time_to_wait);
return true;
}
};
struct manual_poll_parameters
{
std::span<poll_descriptors> fds;
std::function<int64_t(std::span<poll_descriptors> fds)> callback;
};
struct alsa_raw_input_configuration
{
std::function<bool(const manual_poll_parameters&)> manual_poll;
};
struct alsa_raw_output_configuration
{
/**
* For large messages, chunk their content and wait.
* Setting a null optional will disable chunking.
*/
std::optional<chunking_parameters> chunking;
};
struct alsa_raw_observer_configuration
{
std::chrono::milliseconds poll_period{100};
};
}
@@ -0,0 +1,328 @@
#pragma once
#include <libremidi/backends/linux/alsa.hpp>
#include <libremidi/config.hpp>
#include <libremidi/detail/observer.hpp>
#include <chrono>
#include <functional>
#include <iostream>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
// Credits: greatly inspired from
// https://ccrma.stanford.edu/~craig/articles/linuxmidi/alsa-1.0/alsarawmidiout.c
// https://ccrma.stanford.edu/~craig/articles/linuxmidi/alsa-1.0/alsarawportlist.c
// Thanks Craig Stuart Sapp <craig@ccrma.stanford.edu>
namespace libremidi
{
namespace
{
struct alsa_raw_port_id
{
int card{}, dev{}, port{};
std::string to_string() const noexcept
{
return "hw:" + std::to_string(card) + "," + std::to_string(dev) + "," + std::to_string(port);
}
};
inline constexpr port_handle raw_to_port_handle(alsa_raw_port_id id) noexcept
{
return (uint64_t(id.card) << 32) + (uint64_t(id.dev) << 16) + id.port;
}
inline constexpr alsa_raw_port_id raw_from_port_handle(port_handle p) noexcept
{
alsa_raw_port_id ret;
ret.card = (p & 0x00'00'FF'FF'00'00'00'00) >> 32;
ret.dev = (p & 0x00'00'00'00'FF'FF'00'00) >> 16;
ret.port = (p & 0x00'00'00'00'00'00'FF'FF);
return ret;
}
static_assert(raw_from_port_handle(raw_to_port_handle({102, 7, 3})).card == 102);
static_assert(raw_from_port_handle(raw_to_port_handle({12, 7, 3})).dev == 7);
static_assert(raw_from_port_handle(raw_to_port_handle({12, 7, 3})).port == 3);
}
namespace alsa_raw
{
struct alsa_raw_port_info
{
std::string device;
std::string card_name;
std::string device_name;
std::string subdevice_name;
int card{}, dev{}, sub{};
std::string pretty_name() const
{
return device + ": " + card_name + " : " + device_name + " : " + subdevice_name;
}
bool operator==(const alsa_raw_port_info& other) const noexcept = default;
};
struct enumerator;
struct snd_ctl_wrapper
{
const libasound& snd;
snd_ctl_t* ctl{};
inline snd_ctl_wrapper(enumerator& self, const char* name);
~snd_ctl_wrapper()
{
if (ctl)
{
snd.ctl.close(ctl);
}
}
snd_ctl_t& operator*() const noexcept { return *ctl; }
snd_ctl_t* operator->() const noexcept { return ctl; }
operator snd_ctl_t*() const noexcept { return ctl; }
};
struct enumerator
{
const libasound& snd = libasound::instance();
std::vector<alsa_raw_port_info> inputs;
std::vector<alsa_raw_port_info> outputs;
std::function<void(std::string_view)> error_callback;
std::function<void(std::string_view)> warn_callback;
template <typename... Args>
void warning(Args&&... args)
{
std::string s;
((s += args), ...);
if (warn_callback)
{
warn_callback(std::move(s));
}
else
{
std::cerr << s << std::endl;
}
}
template <typename... Args>
void error(Args&&... args)
{
std::string s;
((s += args), ...);
if (error_callback)
{
error_callback(std::move(s));
}
else
{
throw std::runtime_error(s.c_str());
}
}
// 1: is an input / output
// 0: isn't an input / output
// < 0: error
int is(snd_rawmidi_stream_t stream, snd_ctl_t* ctl, int card, int device, int sub)
{
snd_rawmidi_info_t* info;
snd_rawmidi_info_alloca(&info);
snd.rawmidi.info_set_device(info, device);
snd.rawmidi.info_set_subdevice(info, sub);
snd.rawmidi.info_set_stream(info, stream);
const int status = snd.ctl.rawmidi.info(ctl, info);
if (status == 0)
{
return 1;
}
else if (status < 0 && status != -ENXIO)
{
error(
"alsa_raw_helpers::enumerator::is: cannot get rawmidi information:", card, device, sub,
snd.strerror(status));
return status;
}
else
{
return 0;
}
}
int is_input(snd_ctl_t* ctl, int card, int device, int sub)
{
return is(SND_RAWMIDI_STREAM_INPUT, ctl, card, device, sub);
}
int is_output(snd_ctl_t* ctl, int card, int device, int sub)
{
return is(SND_RAWMIDI_STREAM_OUTPUT, ctl, card, device, sub);
}
std::string get_card_name(int card)
{
char* card_name{};
snd.card.get_name(card, &card_name);
std::string str = card_name;
free(card_name);
return str;
}
static std::string device_identifier(int card, int device, int sub)
{
std::string s;
s.reserve(12);
s += "hw:";
s += std::to_string(card);
s += ",";
s += std::to_string(device);
s += ",";
s += std::to_string(sub);
return s;
}
void enumerate_cards()
{
int card = -1;
int status = snd.card.next(&card);
if (status < 0)
{
error(
"alsa_raw_helpers::enumerator::enumerate_cards: "
"cannot determine card number: ",
snd.strerror(status));
return;
}
if (card < 0)
{
error(
"alsa_raw_helpers::enumerator::enumerate_cards: "
"no sound cards found");
return;
}
while (card >= 0)
{
enumerate_devices(card);
if ((status = snd.card.next(&card)) < 0)
{
error(
"alsa_raw_helpers::enumerator::enumerate_cards: "
"cannot determine card number: ",
snd.strerror(status));
break;
}
}
}
virtual void enumerate_devices(int card) = 0;
};
inline snd_ctl_wrapper::snd_ctl_wrapper(enumerator& self, const char* name)
: snd{self.snd}
{
int status = snd.ctl.open(&ctl, name, 0);
if (status < 0)
{
self.error(
"alsa_raw_helpers::enumerator::snd_ctl_wrapper: "
"cannot open control for card",
name, snd.strerror(status));
}
}
struct midi1_enumerator : enumerator
{
void enumerate_devices(int card) override
{
char name[128];
sprintf(name, "hw:%d", card);
// Open card.
snd_ctl_wrapper ctl{*this, name};
if (!ctl)
return;
// Enumerate devices.
int device = -1;
do
{
const int status = snd.ctl.rawmidi.next_device(ctl, &device);
if (device == -1)
return;
if (status < 0)
{
error(
"alsa_raw::midi1_enumerator::enumerate_devices: "
"cannot determine device number: ",
snd.strerror(status));
break;
}
if (device >= 0)
enumerate_subdevices(ctl, card, device);
} while (device >= 0);
}
void enumerate_subdevices(snd_ctl_t* ctl, int card, int device)
{
snd_rawmidi_info_t* info;
snd_rawmidi_info_alloca(&info);
snd.rawmidi.info_set_device(info, device);
snd.rawmidi.info_set_stream(info, SND_RAWMIDI_STREAM_INPUT);
snd.ctl.rawmidi.info(ctl, info);
const int subs_in = snd.rawmidi.info_get_subdevices_count(info);
snd.rawmidi.info_set_stream(info, SND_RAWMIDI_STREAM_OUTPUT);
snd.ctl.rawmidi.info(ctl, info);
const int subs_out = snd.rawmidi.info_get_subdevices_count(info);
alsa_raw_port_info d;
d.card = card;
d.dev = device;
d.card_name = get_card_name(card);
d.device_name = snd.rawmidi.info_get_name(info);
auto read_subdevice_info = [&](int sub) {
snd.rawmidi.info_set_subdevice(info, sub);
snd.ctl.rawmidi.info(ctl, info);
d.device = device_identifier(card, device, sub);
d.subdevice_name = snd.rawmidi.info_get_subdevice_name(info);
d.sub = sub;
};
if (subs_in > 0)
{
snd.rawmidi.info_set_stream(info, SND_RAWMIDI_STREAM_INPUT);
for (int sub = 0; sub < subs_in; sub++)
{
read_subdevice_info(sub);
inputs.push_back(d);
}
}
if (subs_out > 0)
{
snd.rawmidi.info_set_stream(info, SND_RAWMIDI_STREAM_OUTPUT);
for (int sub = 0; sub < subs_out; sub++)
{
read_subdevice_info(sub);
outputs.push_back(d);
}
}
}
};
}
}
@@ -0,0 +1,350 @@
#pragma once
#include <libremidi/backends/alsa_raw/config.hpp>
#include <libremidi/backends/alsa_raw/helpers.hpp>
#include <libremidi/backends/linux/helpers.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
#include <alsa/asoundlib.h>
#include <chrono>
#include <thread>
namespace libremidi::alsa_raw
{
class midi_in_impl
: public midi1::in_api
, public error_handler
{
public:
struct
: input_configuration
, alsa_raw_input_configuration
{
} configuration;
const libasound& snd = libasound::instance();
explicit midi_in_impl(input_configuration&& conf, alsa_raw_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
fds_.reserve(4);
}
~midi_in_impl() override { }
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_in_alsa_raw: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_alsa_raw: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_in_alsa_raw: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW; }
// Must be a string such as: "hw:2,4,1"
[[nodiscard]] int do_init_port(const char* portname)
{
constexpr int mode = SND_RAWMIDI_NONBLOCK;
if (const int err = snd.rawmidi.open(&midiport_, nullptr, portname, mode); err < 0)
{
error<driver_error>(this->configuration, "midi_in_alsa_raw::open_port: cannot open device.");
return err;
}
snd_rawmidi_params_t* params{};
snd_rawmidi_params_alloca(&params);
if (const int err = snd.rawmidi.params_current(midiport_, params); err < 0)
return err;
if (const int err = snd.rawmidi.params_set_no_active_sensing(midiport_, params, 1); err < 0)
return err;
#if LIBREMIDI_ALSA_HAS_RAWMIDI_TREAD
if (configuration.timestamps == timestamp_mode::NoTimestamp)
{
if (const int err
= snd.rawmidi.params_set_read_mode(midiport_, params, SND_RAWMIDI_READ_STANDARD);
err < 0)
return err;
if (const int err
= snd.rawmidi.params_set_clock_type(midiport_, params, SND_RAWMIDI_CLOCK_NONE);
err < 0)
return err;
}
else
{
if (const int err
= snd.rawmidi.params_set_read_mode(midiport_, params, SND_RAWMIDI_READ_TSTAMP);
err < 0)
return err;
if (const int err
= snd.rawmidi.params_set_clock_type(midiport_, params, SND_RAWMIDI_CLOCK_MONOTONIC);
err < 0)
return err;
}
#endif
if (const int err = snd.rawmidi.params(midiport_, params); err < 0)
return err;
return init_pollfd();
}
[[nodiscard]] int init_port(const port_information& p)
{
return do_init_port(raw_from_port_handle(p.port).to_string().c_str());
}
[[nodiscard]] int init_pollfd()
{
const int num_fds = snd.rawmidi.poll_descriptors_count(this->midiport_);
this->fds_.clear();
this->fds_.resize(num_fds);
return snd.rawmidi.poll_descriptors(this->midiport_, fds_.data(), num_fds);
}
ssize_t do_read_events(auto parse_func, std::span<pollfd> fds)
{
// Read events
if (fds.empty())
{
return (this->*parse_func)();
}
else
{
unsigned short res{};
const int err = snd.rawmidi.poll_descriptors_revents(
this->midiport_, fds.data(), static_cast<unsigned int>(fds.size()), &res);
if (err < 0)
return err;
// Did we encounter an error during polling
if (res & (POLLERR | POLLHUP))
return -EIO;
// Is there data to read
if (res & POLLIN)
return (this->*parse_func)();
}
return 0;
}
ssize_t read_input_buffer()
{
static const constexpr int nbytes = 1024;
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = false,
.absolute_is_monotonic = false,
.has_samples = false,
};
unsigned char bytes[nbytes];
ssize_t err = 0;
// err is the amount of bytes read
while ((err = snd.rawmidi.read(this->midiport_, bytes, nbytes)) > 0)
{
const auto to_ns = [this] { return absolute_timestamp(); };
decoder_.on_bytes({bytes, bytes + err}, decoder_.timestamp<timestamp_info>(to_ns, 0));
}
return err;
}
#if LIBREMIDI_ALSA_HAS_RAWMIDI_TREAD
ssize_t read_input_buffer_with_timestamps()
{
static constexpr int nbytes = 1024;
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = true,
.absolute_is_monotonic = true,
.has_samples = false,
};
unsigned char bytes[nbytes];
struct timespec ts;
ssize_t err = 0;
// err is the amount of bytes read
while ((err = snd.rawmidi.tread(this->midiport_, &ts, bytes, nbytes)) > 0)
{
const auto to_ns = [ts] {
return static_cast<int64_t>(ts.tv_sec) * 1'000'000'000 + static_cast<int64_t>(ts.tv_nsec);
};
decoder_.on_bytes({bytes, bytes + err}, decoder_.timestamp<timestamp_info>(to_ns, 0));
}
return err;
}
#else
ssize_t read_input_buffer_with_timestamps() { return read_input_buffer(); }
#endif
void close_port() override
{
if (midiport_)
snd.rawmidi.close(midiport_);
midiport_ = nullptr;
}
timestamp absolute_timestamp() const noexcept final override { return system_ns(); }
snd_rawmidi_t* midiport_{};
std::vector<pollfd> fds_;
midi1::input_state_machine decoder_{this->configuration};
};
class midi_in_alsa_raw_threaded : public midi_in_impl
{
public:
midi_in_alsa_raw_threaded(input_configuration&& conf, alsa_raw_input_configuration&& apiconf)
: midi_in_impl{std::move(conf), std::move(apiconf)}
{
if (this->termination_event < 0)
{
error<driver_error>(
this->configuration, "midi_in_alsa::initialize: error creating eventfd.");
}
}
~midi_in_alsa_raw_threaded() override
{
// Close a connection if it exists.
this->midi_in_alsa_raw_threaded::close_port();
}
private:
void run_thread(auto parse_func)
{
fds_.push_back(this->termination_event);
for (;;)
{
// Poll
ssize_t err = poll(fds_.data(), fds_.size(), -1);
if (err == -EAGAIN)
continue;
else if (err < 0)
return;
else if (termination_event.ready(fds_.back()))
break;
err = do_read_events(parse_func, {fds_.data(), fds_.size() - 1});
if (err == -EAGAIN)
continue;
else if (err < 0)
return;
}
}
[[nodiscard]] ssize_t start_thread()
{
try
{
if (configuration.timestamps == timestamp_mode::NoTimestamp)
{
this->thread_ = std::thread{[this] { run_thread(&midi_in_impl::read_input_buffer); }};
}
else
{
this->thread_ = std::thread{
[this] { run_thread(&midi_in_impl::read_input_buffer_with_timestamps); }};
}
}
catch (const std::system_error& e)
{
using namespace std::literals;
error<thread_error>(
this->configuration,
"midi_in_alsa::start_thread: error starting MIDI input thread: "s + e.what());
return false;
}
return true;
}
bool open_port(const input_port& port, std::string_view /*name*/) override
{
if (const int err = midi_in_impl::init_port(port); err < 0)
return false;
if (!start_thread())
return false;
return true;
}
void close_port() override
{
termination_event.notify();
if (thread_.joinable())
thread_.join();
termination_event.consume(); // Reset to zero
midi_in_impl::close_port();
}
std::thread thread_;
eventfd_notifier termination_event{};
};
class midi_in_alsa_raw_manual : public midi_in_impl
{
public:
using midi_in_impl::midi_in_impl;
~midi_in_alsa_raw_manual()
{
// Close a connection if it exists.
this->close_port();
}
private:
void send_poll_callback()
{
if (configuration.timestamps == timestamp_mode::NoTimestamp)
{
configuration.manual_poll(manual_poll_parameters{
.fds = {this->fds_.data(), this->fds_.size()},
.callback = [this](std::span<pollfd> fds) {
return do_read_events(&midi_in_impl::read_input_buffer, fds);
}});
}
else
{
configuration.manual_poll(manual_poll_parameters{
.fds = {this->fds_.data(), this->fds_.size()},
.callback = [this](std::span<pollfd> fds) {
return do_read_events(&midi_in_impl::read_input_buffer_with_timestamps, fds);
}});
}
}
bool open_port(const input_port& p, std::string_view /*name*/) override
{
if (midi_in_impl::init_port(p) < 0)
return false;
send_poll_callback();
return true;
}
};
}
namespace libremidi
{
template <>
inline std::unique_ptr<midi_in_api> make<alsa_raw::midi_in_impl>(
libremidi::input_configuration&& conf, libremidi::alsa_raw_input_configuration&& api)
{
if (api.manual_poll)
return std::make_unique<alsa_raw::midi_in_alsa_raw_manual>(std::move(conf), std::move(api));
else
return std::make_unique<alsa_raw::midi_in_alsa_raw_threaded>(std::move(conf), std::move(api));
}
}
@@ -0,0 +1,177 @@
#pragma once
#include <libremidi/backends/alsa_raw/config.hpp>
#include <libremidi/backends/alsa_raw/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
#include <alsa/asoundlib.h>
#include <atomic>
#include <thread>
namespace libremidi::alsa_raw
{
class midi_out_impl final
: public midi1::out_api
, public error_handler
{
public:
struct
: output_configuration
, alsa_raw_output_configuration
{
} configuration;
const libasound& snd = libasound::instance();
midi_out_impl(output_configuration&& conf, alsa_raw_output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
}
~midi_out_impl() override
{
// Close a connection if it exists.
midi_out_impl::close_port();
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW; }
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_out_alsa_raw: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_alsa_raw: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_out_alsa_raw: set_port_name unsupported");
}
int connect_port(const char* portname)
{
constexpr int mode = SND_RAWMIDI_SYNC;
int status = snd.rawmidi.open(NULL, &midiport_, portname, mode);
if (status < 0)
{
error<driver_error>(
this->configuration, "midi_out_alsa_raw::open_port: cannot open device.");
return status;
}
return status;
}
bool open_port(const output_port& p, std::string_view) override
{
return connect_port(raw_from_port_handle(p.port).to_string().c_str()) == 0;
}
void close_port() override
{
if (midiport_)
snd.rawmidi.close(midiport_);
midiport_ = nullptr;
}
void send_message(const unsigned char* message, size_t size) override
{
if (!midiport_)
error<invalid_use_error>(
this->configuration,
"midi_out_alsa_raw::send_message: trying to send a message without an open "
"port.");
if (!this->configuration.chunking)
{
write(message, size);
}
else
{
write_chunked(message, size);
}
}
bool write(const unsigned char* message, size_t size)
{
if (snd.rawmidi.write(midiport_, message, size) < 0)
{
error<driver_error>(
this->configuration, "midi_out_alsa_raw::send_message: cannot write message.");
return false;
}
return true;
}
std::size_t get_chunk_size() const noexcept
{
snd_rawmidi_params_t* param;
snd_rawmidi_params_alloca(&param);
snd.rawmidi.params_current(midiport_, param);
std::size_t buffer_size = snd.rawmidi.params_get_buffer_size(param);
return std::min(buffer_size, (std::size_t)configuration.chunking->size);
}
std::size_t get_available_bytes_to_write() const noexcept
{
snd_rawmidi_status_t* st{};
snd_rawmidi_status_alloca(&st);
snd.rawmidi.status(midiport_, st);
return snd.rawmidi.status_get_avail(st);
}
// inspired from ALSA amidi.c source code
void write_chunked(const unsigned char* const begin, size_t size)
{
const unsigned char* data = begin;
const unsigned char* end = begin + size;
const std::size_t chunk_size = std::min(get_chunk_size(), size);
// Send the first buffer
std::size_t len = chunk_size;
if (!write(data, len))
return;
data += len;
while (data < end)
{
// Wait for the buffer to have some space available
const std::size_t written_bytes = data - begin;
std::size_t available{};
while ((available = get_available_bytes_to_write()) < chunk_size)
{
if (!configuration.chunking->wait(
std::chrono::microseconds((chunk_size - available) * 320), written_bytes))
return;
};
if (!configuration.chunking->wait(configuration.chunking->interval, written_bytes))
return;
// Write more data
len = end - data;
// Maybe until the end of the sysex
if (const auto sysex_end = static_cast<const unsigned char*>(memchr(data, 0xf7, len)))
len = sysex_end - data + 1;
if (len > chunk_size)
len = chunk_size;
if (!write(data, len))
return;
data += len;
}
}
snd_rawmidi_t* midiport_{};
};
}
@@ -0,0 +1,240 @@
#pragma once
#include <libremidi/backends/alsa_raw/config.hpp>
#include <libremidi/backends/alsa_raw/helpers.hpp>
#include <libremidi/backends/dummy.hpp>
#if LIBREMIDI_HAS_UDEV
#include <libremidi/backends/linux/helpers.hpp>
#include <libremidi/backends/linux/udev.hpp>
#include <libremidi/detail/observer.hpp>
#include <stdexcept>
namespace libremidi::alsa_raw
{
template <typename Enumerator>
class observer_impl_base : public observer_api
{
public:
struct
: observer_configuration
, alsa_raw_observer_configuration
{
} configuration;
const libasound& snd = libasound::instance();
explicit observer_impl_base(
observer_configuration&& conf, alsa_raw_observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (!configuration.has_callbacks())
return;
fds[0] = this->udev;
fds[1] = termination_event;
fds[2] = timer_fd;
// Set-up initial state
if (configuration.notify_in_constructor)
this->check_devices();
// Start thread
thread = std::thread{[this] { this->run(); }};
}
~observer_impl_base()
{
termination_event.notify();
if (thread.joinable())
thread.join();
}
std::vector<libremidi::input_port> get_input_ports() const noexcept override
{
std::vector<libremidi::input_port> ret;
Enumerator new_devs;
new_devs.enumerate_cards();
for (auto& d : new_devs.inputs)
{
ret.push_back(to_port_info<true>(d));
}
return ret;
}
std::vector<libremidi::output_port> get_output_ports() const noexcept override
{
std::vector<libremidi::output_port> ret;
Enumerator new_devs;
new_devs.enumerate_cards();
for (auto& d : new_devs.outputs)
{
ret.push_back(to_port_info<false>(d));
}
return ret;
}
private:
void run()
{
for (;;)
{
if (int err = poll(fds, 3, -1); err < 0)
{
if (err == -EAGAIN)
continue;
else
return;
}
// Check udev
if (fds[0].revents & POLLIN)
{
udev_device* dev = udev.udev.monitor_receive_device(udev.monitor);
if (!dev)
continue;
std::string_view act = udev.udev.device_get_action(dev);
std::string_view ss = udev.udev.device_get_subsystem(dev);
if (!act.empty() && ss == "snd_seq")
{
if (act == "add" || act == "remove")
{
// Check every 100 milliseconds for ten seconds
this->timer_fd.restart(configuration.poll_period.count());
timer_check_counts = 100;
}
}
udev.udev.device_unref(dev);
fds[0].revents = 0;
}
// Check eventfd
if (fds[1].revents & POLLIN)
{
break;
}
// Check timer
if (fds[2].revents & POLLIN)
{
if (this->timer_check_counts-- <= 0)
this->timer_fd.cancel();
fds[2].revents = 0;
check_devices();
}
}
}
template <bool Input>
auto to_port_info(alsa_raw::alsa_raw_port_info p) const noexcept
-> std::conditional_t<Input, input_port, output_port>
{
return {
{.client = 0,
.port = raw_to_port_handle({p.card, p.dev, p.sub}),
.manufacturer = p.card_name,
.device_name = p.device_name,
.port_name = p.subdevice_name,
.display_name = p.subdevice_name}};
}
void check_devices()
{
Enumerator new_devs;
new_devs.enumerate_cards();
for (auto& in_prev : current_inputs)
{
if (auto it = std::find(new_devs.inputs.begin(), new_devs.inputs.end(), in_prev);
it == new_devs.inputs.end())
{
if (auto& cb = this->configuration.input_removed)
{
cb(to_port_info<true>(in_prev));
}
}
}
for (auto& in_next : new_devs.inputs)
{
if (auto it = std::find(current_inputs.begin(), current_inputs.end(), in_next);
it == current_inputs.end())
{
if (auto& cb = this->configuration.input_added)
{
cb(to_port_info<true>(in_next));
}
}
}
for (auto& out_prev : current_outputs)
{
if (auto it = std::find(new_devs.outputs.begin(), new_devs.outputs.end(), out_prev);
it == new_devs.outputs.end())
{
if (auto& cb = this->configuration.output_removed)
{
cb(to_port_info<false>(out_prev));
}
}
}
for (auto& out_next : new_devs.outputs)
{
if (auto it = std::find(current_outputs.begin(), current_outputs.end(), out_next);
it == current_outputs.end())
{
if (auto& cb = this->configuration.output_added)
{
cb(to_port_info<false>(out_next));
}
}
}
current_inputs = std::move(new_devs.inputs);
current_outputs = std::move(new_devs.outputs);
}
udev_helper udev{};
eventfd_notifier termination_event{};
timerfd_timer timer_fd{};
int timer_check_counts = 0;
std::thread thread;
std::vector<alsa_raw_port_info> current_inputs;
std::vector<alsa_raw_port_info> current_outputs;
pollfd fds[3]{};
};
}
#else
#include <libremidi/backends/dummy.hpp>
namespace libremidi::alsa_raw
{
template <typename Enumerator>
struct observer_impl_base : observer_dummy
{
explicit observer_impl_base(
[[maybe_unused]] observer_configuration&& conf,
[[maybe_unused]] alsa_raw_observer_configuration&& apiconf)
: observer_dummy{dummy_configuration{}, dummy_configuration{}}
{
}
};
}
#endif
namespace libremidi::alsa_raw
{
struct observer_impl : observer_impl_base<alsa_raw::midi1_enumerator>
{
using alsa_raw::observer_impl_base<midi1_enumerator>::observer_impl_base;
libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW; }
};
}
@@ -0,0 +1,30 @@
#pragma once
#include <libremidi/backends/alsa_raw/observer.hpp>
#include <libremidi/backends/alsa_raw_ump/midi_in.hpp>
#include <libremidi/backends/alsa_raw_ump/midi_out.hpp>
#include <libremidi/backends/alsa_raw_ump/observer.hpp>
#include <libremidi/backends/dummy.hpp>
namespace libremidi::alsa_raw_ump
{
struct backend
{
using midi_in = midi_in_impl;
using midi_out = midi_out_impl;
using midi_observer = observer_impl;
using midi_in_configuration = alsa_raw_ump::input_configuration;
using midi_out_configuration = alsa_raw_ump::output_configuration;
struct midi_observer_configuration : alsa_raw_observer_configuration
{
};
static const constexpr auto API = libremidi::API::ALSA_RAW_UMP;
static const constexpr auto name = "alsa_raw_ump";
static const constexpr auto display_name = "ALSA (raw UMP)";
static inline bool available() noexcept
{
static const libasound& snd = libasound::instance();
return snd.available && snd.rawmidi.available && snd.ump.available;
}
};
}
@@ -0,0 +1,24 @@
#pragma once
#include <libremidi/backends/alsa_raw/config.hpp>
namespace libremidi::alsa_raw_ump
{
struct input_configuration
{
std::function<bool(const manual_poll_parameters&)> manual_poll;
};
struct output_configuration
{
/**
* For large messages, chunk their content and wait.
* Setting a null optional will disable chunking.
*/
std::optional<chunking_parameters> chunking;
};
struct observer_configuration
{
std::chrono::milliseconds poll_period{100};
};
}
@@ -0,0 +1,66 @@
#pragma once
#include <libremidi/backends/alsa_raw/helpers.hpp>
#include <libremidi/config.hpp>
#include <libremidi/detail/observer.hpp>
namespace libremidi::alsa_raw_ump
{
struct midi2_enumerator : alsa_raw::enumerator
{
void enumerate_devices(int card) override
{
char name[128];
sprintf(name, "hw:%d", card);
// Open card.
alsa_raw::snd_ctl_wrapper ctl{*this, name};
if (!ctl)
return;
// Enumerate devices.
int device = -1;
do
{
const int status = snd.ctl.ump.next_device(ctl, &device);
if (device == -1)
return;
if (status < 0)
{
error(
"alsa_raw_ump::midi2_enumerator::enumerate_devices: "
"cannot determine device number: ",
snd.strerror(status));
break;
}
if (device >= 0)
{
enumerate_blocks(ctl, card, device);
enumerate_endpoints(ctl, card, device);
}
} while (device >= 0);
}
void enumerate_endpoints(snd_ctl_t* ctl, [[maybe_unused]] int card, [[maybe_unused]] int device)
{
snd_ump_endpoint_info_t* info{};
snd_ump_endpoint_info_alloca(&info);
snd.ctl.ump.endpoint_info(ctl, info);
fprintf(stderr, "UMP endpoint: %s", snd.ump.endpoint_info_get_name(info));
}
void enumerate_blocks(snd_ctl_t* ctl, [[maybe_unused]] int card, [[maybe_unused]] int device)
{
snd_ump_block_info_t* info{};
snd_ump_block_info_alloca(&info);
snd.ctl.ump.block_info(ctl, info);
fprintf(stderr, "UMP block: %s", snd.ump.block_info_get_name(info));
}
};
}
@@ -0,0 +1,383 @@
#pragma once
#include <libremidi/backends/alsa_raw_ump/config.hpp>
#include <libremidi/backends/alsa_raw_ump/helpers.hpp>
#include <libremidi/backends/linux/helpers.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
#include <alsa/asoundlib.h>
#include <atomic>
#include <thread>
namespace libremidi::alsa_raw_ump
{
class midi_in_impl
: public midi2::in_api
, public error_handler
{
public:
struct
: libremidi::ump_input_configuration
, alsa_raw_ump::input_configuration
{
} configuration;
const libasound& snd = libasound::instance();
explicit midi_in_impl(
libremidi::ump_input_configuration&& conf, alsa_raw_ump::input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
fds_.reserve(4);
assert(snd.ump.available);
}
~midi_in_impl() override { }
bool open_virtual_port(std::string_view) override
{
warning(configuration, "alsa_raw_ump::ump: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "alsa_raw_ump::ump: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "alsa_raw_ump::ump: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW_UMP; }
// Must be a string such as: "hw:2,4,1"
[[nodiscard]] int do_init_port(const char* portname)
{
constexpr int mode = 0;
SND_RAWMIDI_NONBLOCK;
if (int err = snd.ump.open(&midiport_, 0, portname, mode); err < 0)
{
error<driver_error>(
this->configuration, "alsa_raw_ump::ump::open_port: cannot open device.");
return err;
}
snd_rawmidi_params_t* params{};
snd_rawmidi_params_alloca(&params);
auto rawmidi = snd.ump.rawmidi(midiport_);
if (int err = snd.ump.rawmidi_params_current(midiport_, params); err < 0)
return err;
if (int err = snd.rawmidi.params_set_no_active_sensing(rawmidi, params, 1); err < 0)
return err;
if (configuration.timestamps == timestamp_mode::NoTimestamp)
{
if (int err = snd.rawmidi.params_set_read_mode(rawmidi, params, SND_RAWMIDI_READ_STANDARD);
err < 0)
return err;
if (int err = snd.rawmidi.params_set_clock_type(rawmidi, params, SND_RAWMIDI_CLOCK_NONE);
err < 0)
return err;
}
else
{
if (int err = snd.rawmidi.params_set_read_mode(rawmidi, params, SND_RAWMIDI_READ_TSTAMP);
err < 0)
return err;
if (int err
= snd.rawmidi.params_set_clock_type(rawmidi, params, SND_RAWMIDI_CLOCK_MONOTONIC);
err < 0)
return err;
}
if (int err = snd.ump.rawmidi_params(midiport_, params); err < 0)
return err;
return init_pollfd();
}
[[nodiscard]] int init_port(const port_information& p)
{
return do_init_port(raw_from_port_handle(p.port).to_string().c_str());
}
[[nodiscard]] int init_pollfd()
{
int num_fds = snd.ump.poll_descriptors_count(this->midiport_);
this->fds_.clear();
this->fds_.resize(num_fds);
return snd.ump.poll_descriptors(this->midiport_, fds_.data(), num_fds);
}
ssize_t do_read_events(auto parse_func, std::span<pollfd> fds)
{
// Read events
if (fds.empty())
{
return (this->*parse_func)();
}
else
{
unsigned short res{};
ssize_t err = snd.ump.poll_descriptors_revents(
this->midiport_, fds.data(), static_cast<unsigned int>(fds.size()), &res);
if (err < 0)
return err;
// Did we encounter an error during polling
if (res & (POLLERR | POLLHUP))
return -EIO;
// Is there data to read
if (res & POLLIN)
return (this->*parse_func)();
}
return 0;
}
ssize_t read_input_buffer()
{
static const constexpr int nbytes = 1024;
unsigned char bytes[nbytes];
ssize_t err = 0;
while ((err = snd.ump.read(this->midiport_, bytes, nbytes)) > 0)
{
std::cerr << "We read: " << nbytes << "bytes !!!!";
return 1;
// err is the amount of bytes read
// decoder_.add_bytes(bytes, err);
}
return err;
}
void set_timestamp(struct timespec ts, timestamp& res)
{
static constexpr int64_t nanos = 1e9;
switch (configuration.timestamps)
{
// Unneeded here
case timestamp_mode::NoTimestamp:
break;
case timestamp_mode::Relative: {
const auto t = static_cast<int64_t>(ts.tv_sec) * nanos + static_cast<int64_t>(ts.tv_nsec);
if (firstMessage == true)
{
firstMessage = false;
res = 0;
}
else
{
res = t - last_time;
}
last_time = t;
break;
}
case timestamp_mode::Absolute:
case timestamp_mode::SystemMonotonic:
res = static_cast<int64_t>(ts.tv_sec) * nanos + static_cast<int64_t>(ts.tv_nsec);
break;
case timestamp_mode::Custom:
res = configuration.get_timestamp(
static_cast<int64_t>(ts.tv_sec) * nanos + static_cast<int64_t>(ts.tv_nsec));
break;
}
}
ssize_t read_input_buffer_with_timestamps()
{
static const constexpr int nbytes = 1024;
unsigned char bytes[nbytes];
struct timespec ts;
ssize_t err = 0;
while ((err = snd.ump.tread(this->midiport_, &ts, bytes, nbytes)) > 0)
{
// err is the amount of bytes read
int64_t ns{};
set_timestamp(ts, ns);
std::cerr << "We read: " << nbytes << "bytes !!!!";
return 1;
// decoder_.add_bytes(bytes, err, ns);
}
return err;
}
void close_port() override
{
if (midiport_)
snd.ump.close(midiport_);
midiport_ = nullptr;
}
timestamp absolute_timestamp() const noexcept override { return system_ns(); }
midi2_enumerator get_device_enumerator() const noexcept
{
midi2_enumerator device_list;
device_list.error_callback
= [this](std::string_view text) { this->error<driver_error>(this->configuration, text); };
return device_list;
}
snd_ump_t* midiport_{};
std::vector<pollfd> fds_;
// midi_stream_decoder decoder_{this->configuration.on_message};
int64_t last_time{};
};
class midi_in_impl_threaded : public midi_in_impl
{
public:
midi_in_impl_threaded(
libremidi::ump_input_configuration&& conf, alsa_raw_ump::input_configuration&& apiconf)
: midi_in_impl{std::move(conf), std::move(apiconf)}
{
if (this->termination_event < 0)
{
error<driver_error>(
this->configuration, "midi_in_alsa::initialize: error creating eventfd.");
}
}
~midi_in_impl_threaded()
{
// Close a connection if it exists.
this->close_port();
}
private:
void run_thread(auto parse_func)
{
fds_.push_back(this->termination_event);
for (;;)
{
// Poll
ssize_t err = poll(fds_.data(), fds_.size(), -1);
if (err == -EAGAIN)
continue;
else if (err < 0)
return;
else if (termination_event.ready(fds_.back()))
break;
err = do_read_events(parse_func, {fds_.data(), fds_.size() - 1});
if (err == -EAGAIN)
continue;
else if (err < 0)
return;
}
}
[[nodiscard]] int start_thread()
{
try
{
if (configuration.timestamps == timestamp_mode::NoTimestamp)
{
this->thread_ = std::thread{[this] { run_thread(&midi_in_impl::read_input_buffer); }};
}
else
{
this->thread_ = std::thread{
[this] { run_thread(&midi_in_impl::read_input_buffer_with_timestamps); }};
}
}
catch (const std::system_error& e)
{
using namespace std::literals;
error<thread_error>(
this->configuration,
"midi_in_alsa::start_thread: error starting MIDI input thread: "s + e.what());
return false;
}
return true;
}
bool open_port(const input_port& port, [[maybe_unused]] std::string_view name) override
{
if (int err = midi_in_impl::init_port(port); err < 0)
return false;
if (!start_thread())
return false;
return true;
}
void close_port() override
{
termination_event.notify();
if (thread_.joinable())
thread_.join();
termination_event.consume(); // Reset to zero
midi_in_impl::close_port();
}
std::thread thread_;
eventfd_notifier termination_event{};
};
class midi_in_impl_manual : public midi_in_impl
{
public:
using midi_in_impl::midi_in_impl;
~midi_in_impl_manual()
{
// Close a connection if it exists.
this->close_port();
}
private:
void send_poll_callback()
{
if (configuration.timestamps == timestamp_mode::NoTimestamp)
{
configuration.manual_poll(manual_poll_parameters{
.fds = {this->fds_.data(), this->fds_.size()},
.callback = [this](std::span<pollfd> fds) {
return do_read_events(&midi_in_impl::read_input_buffer, fds);
}});
}
else
{
configuration.manual_poll(manual_poll_parameters{
.fds = {this->fds_.data(), this->fds_.size()},
.callback = [this](std::span<pollfd> fds) {
return do_read_events(&midi_in_impl::read_input_buffer_with_timestamps, fds);
}});
}
}
bool open_port(const input_port& p, [[maybe_unused]] std::string_view name) override
{
if (midi_in_impl::init_port(p) < 0)
return false;
send_poll_callback();
return true;
}
};
}
namespace libremidi
{
template <>
inline std::unique_ptr<midi_in_api> make<alsa_raw_ump::midi_in_impl>(
libremidi::ump_input_configuration&& conf, libremidi::alsa_raw_ump::input_configuration&& api)
{
if (api.manual_poll)
return std::make_unique<alsa_raw_ump::midi_in_impl_manual>(std::move(conf), std::move(api));
else
return std::make_unique<alsa_raw_ump::midi_in_impl_threaded>(std::move(conf), std::move(api));
}
}
@@ -0,0 +1,107 @@
#pragma once
#include <libremidi/backends/alsa_raw_ump/config.hpp>
#include <libremidi/backends/alsa_raw_ump/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
#include <alsa/asoundlib.h>
#include <atomic>
#include <thread>
namespace libremidi::alsa_raw_ump
{
class midi_out_impl final
: public midi2::out_api
, public error_handler
{
public:
struct
: libremidi::output_configuration
, libremidi::alsa_raw_ump::output_configuration
{
} configuration;
const libasound& snd = libasound::instance();
midi_out_impl(
libremidi::output_configuration&& conf,
libremidi::alsa_raw_ump::output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
assert(snd.ump.available);
}
~midi_out_impl() override
{
// Close a connection if it exists.
midi_out_impl::close_port();
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW; }
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_out_alsa_raw: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_alsa_raw: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_out_alsa_raw: set_port_name unsupported");
}
int connect_port(const char* portname)
{
constexpr int mode = SND_RAWMIDI_SYNC;
int status = snd.ump.open(NULL, &midiport_, portname, mode);
if (status < 0)
{
error<driver_error>(
this->configuration, "midi_out_alsa_raw::open_port: cannot open device.");
return status;
}
return status;
}
bool open_port(const output_port& p, std::string_view) override
{
return connect_port(raw_from_port_handle(p.port).to_string().c_str()) == 0;
}
void close_port() override
{
if (midiport_)
snd.ump.close(midiport_);
midiport_ = nullptr;
}
void send_ump(const uint32_t* ump_stream, std::size_t count) override
{
if (!midiport_)
error<invalid_use_error>(
this->configuration,
"midi_out_alsa_raw::send_message: trying to send a message without an open "
"port.");
write(ump_stream, count * sizeof(uint32_t));
}
bool write(const uint32_t* ump_stream, size_t bytes)
{
if (snd.ump.write(midiport_, ump_stream, bytes) < 0)
{
error<driver_error>(
this->configuration, "midi_out_alsa_raw::send_message: cannot write message.");
return false;
}
return true;
}
snd_ump_t* midiport_{};
};
}
@@ -0,0 +1,14 @@
#pragma once
#include <libremidi/backends/alsa_raw/observer.hpp>
#include <libremidi/backends/alsa_raw_ump/helpers.hpp>
namespace libremidi::alsa_raw_ump
{
class observer_impl : public alsa_raw::observer_impl_base<midi2_enumerator>
{
using alsa_raw::observer_impl_base<midi2_enumerator>::observer_impl_base;
libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW_UMP; }
};
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
//*********************************************************************//
// API: LINUX ALSA SEQUENCER
//*********************************************************************//
// API information found at:
// - http://www.alsa-project.org/documentation.php#Library
// The ALSA Sequencer API is based on the use of a callback function for
// MIDI input.
//
// Thanks to Pedro Lopez-Cabanillas for help with the ALSA sequencer
// time stamps and other assorted fixes!!!
#include <libremidi/backends/alsa_seq/midi_in.hpp>
#include <libremidi/backends/alsa_seq/midi_out.hpp>
#include <libremidi/backends/alsa_seq/observer.hpp>
namespace libremidi::alsa_seq
{
struct backend
{
using midi_in
= alsa_seq::midi_in_impl<libremidi::input_configuration, alsa_seq::input_configuration>;
using midi_out = alsa_seq::midi_out_impl;
using midi_observer = alsa_seq::observer_impl<alsa_seq::observer_configuration>;
using midi_in_configuration = alsa_seq::input_configuration;
using midi_out_configuration = alsa_seq::output_configuration;
using midi_observer_configuration = alsa_seq::observer_configuration;
static const constexpr auto API = libremidi::API::ALSA_SEQ;
static const constexpr auto name = "alsa_seq";
static const constexpr auto display_name = "ALSA (sequencer)";
static inline bool available() noexcept
{
static const libasound& snd = libasound::instance();
return snd.available && snd.seq.available;
}
};
}
@@ -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
@@ -0,0 +1,62 @@
#pragma once
#include <libremidi/backends/alsa_seq/midi_in.hpp>
#include <libremidi/backends/alsa_seq/observer.hpp>
#include <libremidi/backends/alsa_seq_ump/config.hpp>
#include <libremidi/backends/alsa_seq_ump/midi_out.hpp>
namespace libremidi
{
template <>
inline std::unique_ptr<observer_api>
make<alsa_seq::observer_impl<alsa_seq_ump::observer_configuration>>(
libremidi::observer_configuration&& conf,
libremidi::alsa_seq_ump::observer_configuration&& api)
{
if (api.manual_poll)
return std::make_unique<alsa_seq::observer_manual<alsa_seq_ump::observer_configuration>>(
std::move(conf), std::move(api));
else
return std::make_unique<alsa_seq::observer_threaded<alsa_seq_ump::observer_configuration>>(
std::move(conf), std::move(api));
}
template <>
inline std::unique_ptr<midi_in_api> make<
alsa_seq::midi_in_impl<libremidi::ump_input_configuration, alsa_seq_ump::input_configuration>>(
libremidi::ump_input_configuration&& conf, libremidi::alsa_seq_ump::input_configuration&& api)
{
if (api.manual_poll)
return std::make_unique<alsa_seq::midi_in_alsa_manual<
libremidi::ump_input_configuration, alsa_seq_ump::input_configuration>>(
std::move(conf), std::move(api));
else
return std::make_unique<alsa_seq::midi_in_alsa_threaded<
libremidi::ump_input_configuration, alsa_seq_ump::input_configuration>>(
std::move(conf), std::move(api));
}
}
namespace libremidi::alsa_seq_ump
{
struct backend
{
using midi_in = alsa_seq::midi_in_impl<
libremidi::ump_input_configuration, alsa_seq_ump::input_configuration>;
using midi_out = alsa_seq_ump::midi_out_impl;
using midi_observer = alsa_seq::observer_impl<alsa_seq_ump::observer_configuration>;
using midi_in_configuration = alsa_seq_ump::input_configuration;
using midi_out_configuration = alsa_seq_ump::output_configuration;
using midi_observer_configuration = alsa_seq_ump::observer_configuration;
static const constexpr auto API = libremidi::API::ALSA_SEQ_UMP;
static const constexpr auto name = "alsa_seq_ump";
static const constexpr auto display_name = "ALSA (sequencer, UMP)";
static inline bool available() noexcept
{
static const libasound& snd = libasound::instance();
return snd.available && snd.seq.available && snd.seq.ump.available && snd.ump.available;
}
};
}
@@ -0,0 +1,53 @@
#pragma once
#include <libremidi/backends/alsa_seq/config.hpp>
#if __has_include(<alsa/asoundlib.h>)
#include <alsa/asoundlib.h>
#endif
// Necessary for the versions of ALSA that did have UMP
// through snd_seq_event, before snd_seq_ump_event existed.
extern "C" {
typedef struct snd_seq_ump_event snd_seq_ump_event_t;
}
namespace libremidi::alsa_seq_ump
{
struct poll_parameters
{
snd_seq_addr_t addr{};
std::function<int(const snd_seq_ump_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 = 2;
};
struct output_configuration
{
std::string client_name = "libremidi client";
snd_seq_t* context{};
static constexpr int midi_version = 2;
};
struct observer_configuration
{
std::string client_name = "libremidi client";
snd_seq_t* context{};
std::function<bool(const libremidi::alsa_seq::poll_parameters&)> manual_poll;
std::function<bool(snd_seq_addr_t)> stop_poll;
static constexpr int midi_version = 2;
};
}
@@ -0,0 +1,151 @@
#pragma once
#include <libremidi/backends/alsa_seq/helpers.hpp>
#include <libremidi/backends/alsa_seq_ump/config.hpp>
#include <libremidi/backends/alsa_seq_ump/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
#include <libremidi/detail/ump_stream.hpp>
namespace libremidi::alsa_seq_ump
{
class midi_out_impl final
: public midi2::out_api
, private alsa_seq::alsa_data
, public error_handler
{
public:
struct
: libremidi::output_configuration
, alsa_seq_ump::output_configuration
{
} configuration;
midi_out_impl(
libremidi::output_configuration&& conf, alsa_seq_ump::output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
assert(snd.seq.ump.available);
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_CAP_UMP_ENDPOINT,
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_ump(const uint32_t* ump_stream, std::size_t count) override
{
snd_seq_ump_event_t ev;
memset(&ev, 0, sizeof(snd_seq_ump_event_t));
snd_seq_ev_set_ump(&ev);
snd_seq_ev_set_source(&ev, this->vport);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_set_direct(&ev);
auto write_func = [this, &ev](const uint32_t* ump, int64_t bytes) {
std::memcpy(ev.ump, ump, bytes);
const int result = snd.seq.ump.event_output_direct(this->seq, &ev);
if (result < 0)
{
warning(
this->configuration,
"midi_out_alsa::send_message: error sending MIDI message to port.");
return libremidi::segmentation_error::other;
}
return libremidi::segmentation_error::no_error;
};
segment_ump_stream(ump_stream, count, write_func, []() {});
snd.seq.drain_output(this->seq);
}
private:
unsigned int bufferSize{32};
};
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <libremidi/backends/coremidi/midi_in.hpp>
#include <libremidi/backends/coremidi/midi_out.hpp>
#include <libremidi/backends/coremidi/observer.hpp>
namespace libremidi
{
struct core_backend
{
using midi_in = midi_in_core;
using midi_out = midi_out_core;
using midi_observer = observer_core;
using midi_in_configuration = coremidi_input_configuration;
using midi_out_configuration = coremidi_output_configuration;
using midi_observer_configuration = coremidi_observer_configuration;
static const constexpr auto API = libremidi::API::COREMIDI;
static const constexpr auto name = "core";
static const constexpr auto display_name = "CoreMIDI";
static constexpr inline bool available() noexcept { return true; }
};
}
@@ -0,0 +1,41 @@
#pragma once
#include <libremidi/config.hpp>
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#if defined(__APPLE__)
#if __LP64__
typedef unsigned int UInt32;
#else
typedef unsigned long UInt32;
#endif
typedef UInt32 MIDIObjectRef;
typedef MIDIObjectRef MIDIClientRef;
#else
using MIDIClientRef = uint32_t;
#endif
namespace libremidi
{
struct coremidi_input_configuration
{
std::string client_name = "libremidi client";
std::optional<MIDIClientRef> context{};
};
struct coremidi_output_configuration
{
std::string client_name = "libremidi client";
std::optional<MIDIClientRef> context{};
};
struct coremidi_observer_configuration
{
std::string client_name = "libremidi client";
std::function<void(MIDIClientRef)> on_create_context{};
};
}
@@ -0,0 +1,324 @@
#pragma once
#include <libremidi/detail/memory.hpp>
#include <libremidi/detail/midi_api.hpp>
#include <libremidi/input_configuration.hpp>
#include <CoreMIDI/CoreMIDI.h>
#include <CoreServices/CoreServices.h>
#include <cmath>
#include <bit>
#if TARGET_OS_IPHONE
#include <CoreAudio/CoreAudioTypes.h>
#include <mach/mach_time.h>
#define LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME mach_absolute_time
#else
#include <CoreAudio/HostTime.h>
#define LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME AudioGetCurrentHostTime
#endif
namespace libremidi
{
using CFString_handle = unique_handle<const __CFString, CFRelease>;
using CFStringMutable_handle = unique_handle<__CFString, CFRelease>;
namespace
{
static inline std::string get_string_property(MIDIObjectRef object, CFStringRef property) noexcept
{
CFStringRef res;
MIDIObjectGetStringProperty(object, property, &res);
char name[256];
CFStringGetCString(res, name, sizeof(name), kCFStringEncodingUTF8);
CFRelease(res);
return name;
}
static inline int32_t get_int_property(MIDIObjectRef object, CFStringRef property) noexcept
{
SInt32 res;
MIDIObjectGetIntegerProperty(object, property, &res);
return res;
}
static inline CFString_handle toCFString(std::string_view str) noexcept
{
return CFString_handle{CFStringCreateWithCString(nullptr, str.data(), kCFStringEncodingASCII)};
}
#if TARGET_OS_IPHONE
inline uint64_t AudioConvertHostTimeToNanos(uint64_t hostTime)
{
static const struct mach_timebase_info timebase = [] {
struct mach_timebase_info theTimeBaseInfo;
mach_timebase_info(&theTimeBaseInfo);
return theTimeBaseInfo;
}();
const auto numer = timebase.numer;
const auto denom = timebase.denom;
__uint128_t res = hostTime;
if (numer != denom)
{
res *= numer;
res /= denom;
}
return static_cast<uint64_t>(res);
}
#endif
// This function was submitted by Douglas Casey Tucker and apparently
// derived largely from PortMidi.
inline CFStringRef EndpointName(MIDIEndpointRef endpoint, bool isExternal)
{
CFMutableStringRef result = CFStringCreateMutable(nullptr, 0);
static constexpr auto getProp = [](MIDIObjectRef prop) {
CFStringRef str = nullptr;
MIDIObjectGetStringProperty(prop, kMIDIPropertyName, &str);
return CFString_handle{str};
};
// Begin with the endpoint's name.
if (auto endpoint_name = getProp(endpoint))
{
CFStringAppend(result, endpoint_name.get());
}
// some MIDI devices have a leading space in endpoint name. trim
CFStringTrim(result, CFSTR(" "));
MIDIEntityRef entity = 0;
MIDIDeviceRef device = 0;
MIDIEndpointGetEntity(endpoint, &entity);
if (entity == 0)
goto finish;
if (CFStringGetLength(result) == 0)
{
// endpoint name has zero length -- try the entity
if (auto entity_name = getProp(entity))
{
CFStringAppend(result, entity_name.get());
}
}
// now consider the device's name
MIDIEntityGetDevice(entity, &device);
if (device == 0)
goto finish;
if (auto dev_name = getProp(device))
{
const auto dev_strlen = CFStringGetLength(dev_name.get());
// if an external device has only one entity, throw away
// the endpoint name and just use the device name
if (CFStringGetLength(result) == 0
|| (isExternal && MIDIDeviceGetNumberOfEntities(device) < 2))
{
CFStringAppend(result, dev_name.get());
goto finish;
}
// does the entity name already start with the device name?
// (some drivers do this though they shouldn't)
// if so, do not prepend
if (CFStringCompareWithOptions(result, dev_name.get(), CFRangeMake(0, dev_strlen), 0)
!= kCFCompareEqualTo)
{
// prepend the device name to the entity name
if (CFStringGetLength(result) > 0)
CFStringInsert(result, 0, CFSTR(" "));
CFStringInsert(result, 0, dev_name.get());
}
}
finish:
if (CFStringGetLength(result) == 0)
return CFSTR("No name");
else
return result;
}
// This function was submitted by Douglas Casey Tucker and apparently
// derived largely from PortMidi.
inline CFStringRef ConnectedEndpointName(MIDIEndpointRef endpoint)
{
CFMutableStringRef result = CFStringCreateMutable(nullptr, 0);
CFStringRef str{};
// Does the endpoint have connections?
CFDataRef connections = nullptr;
std::size_t nConnected = 0;
bool anyStrings = false;
MIDIObjectGetDataProperty(endpoint, kMIDIPropertyConnectionUniqueID, &connections);
if (connections != nullptr)
{
// It has connections, follow them
// Concatenate the names of all connected devices
nConnected = CFDataGetLength(connections) / sizeof(MIDIUniqueID);
if (nConnected)
{
const SInt32* pid = (const SInt32*)(CFDataGetBytePtr(connections));
for (std::size_t i = 0; i < nConnected; ++i, ++pid)
{
MIDIUniqueID id = CFSwapInt32BigToHost(*pid);
MIDIObjectRef connObject;
MIDIObjectType connObjectType;
auto err = MIDIObjectFindByUniqueID(id, &connObject, &connObjectType);
if (err == noErr)
{
if (connObjectType == kMIDIObjectType_ExternalSource
|| connObjectType == kMIDIObjectType_ExternalDestination)
{
// Connected to an external device's endpoint (10.3 and later).
str = EndpointName((MIDIEndpointRef)(connObject), true);
}
else
{
// Connected to an external device (10.2) (or something else,
// catch-
str = nullptr;
MIDIObjectGetStringProperty(connObject, kMIDIPropertyName, &str);
}
if (str != nullptr)
{
if (anyStrings)
CFStringAppend(result, CFSTR(", "));
else
anyStrings = true;
CFStringAppend(result, str);
CFRelease(str);
}
}
}
}
CFRelease(connections);
}
if (anyStrings)
return result;
CFRelease(result);
// Here, either the endpoint had no connections, or we failed to obtain names
return EndpointName(endpoint, false);
}
inline MIDIObjectRef
locate_object(auto& self, const port_information& info, MIDIObjectType requested_type)
{
auto uid = std::bit_cast<std::int32_t>((uint32_t)info.port);
MIDIObjectRef object{};
MIDIObjectType type{};
auto ret = MIDIObjectFindByUniqueID(uid, &object, &type);
if (ret != noErr)
{
self.template error<invalid_parameter_error>(
self.configuration, "coremidi::locate_object: cannot find port: " + info.port_name);
return 0;
}
if (type != requested_type || object == 0)
{
self.template error<invalid_parameter_error>(
self.configuration, "coremidi::locate_object: invalid object: " + info.port_name + " : "
+ std::to_string(object));
return 0;
}
return object;
}
}
// A structure to hold variables related to the CoreMIDI API
// implementation.
struct coremidi_data
{
MIDIClientRef client{};
MIDIPortRef port{};
MIDIEndpointRef endpoint = 0;
[[nodiscard]] OSStatus init_client(auto& configuration)
{
if (configuration.context)
{
client = *configuration.context;
return noErr;
}
else
{
// Set up our client.
return MIDIClientCreate(
toCFString(configuration.client_name).get(), nullptr, nullptr, &client);
}
}
static uint64_t time_in_nanos(MIDITimeStamp tp) noexcept
{
if (tp == 0)
{ // this happens when receiving asynchronous sysex messages
return clock_gettime_nsec_np(CLOCK_UPTIME_RAW);
}
else
{
return AudioConvertHostTimeToNanos(tp);
}
}
static void set_timestamp(auto& self, MIDITimeStamp packet, timestamp& msg) noexcept
{
// packet.timeStamp is in mach_absolute_time units
// We want a timestamp in nanoseconds
switch (self.configuration.timestamps)
{
case timestamp_mode::NoTimestamp:
msg = 0;
return;
case timestamp_mode::Relative: {
if (self.firstMessage)
{
self.firstMessage = false;
msg = 0;
return;
}
else
{
if constexpr (requires { self.continueSysex; })
{
if (self.continueSysex)
return;
}
auto time = time_in_nanos(packet);
time -= self.last_time;
msg = time;
}
break;
}
case timestamp_mode::Absolute:
case timestamp_mode::SystemMonotonic:
if constexpr (requires { self.continueSysex; })
{
if (self.continueSysex)
return;
}
msg = time_in_nanos(packet);
break;
case timestamp_mode::Custom: {
if constexpr (requires { self.continueSysex; })
{
if (self.continueSysex)
return;
}
msg = self.configuration.get_timestamp(time_in_nanos(packet));
break;
}
}
}
};
}
@@ -0,0 +1,178 @@
#pragma once
#include <libremidi/backends/coremidi/config.hpp>
#include <libremidi/backends/coremidi/helpers.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
namespace libremidi
{
class midi_in_core final
: public midi1::in_api
, private coremidi_data
, public error_handler
{
public:
struct
: input_configuration
, coremidi_input_configuration
{
} configuration;
midi_in_core(input_configuration&& conf, coremidi_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (auto result = init_client(configuration); result != noErr)
{
error<driver_error>(
this->configuration,
"midi_in_core: error creating MIDI client object: " + std::to_string(result));
return;
}
}
~midi_in_core() override
{
// Close a connection if it exists.
midi_in_core::close_port();
if (this->endpoint)
MIDIEndpointDispose(this->endpoint);
close_client();
}
void close_client()
{
if (!configuration.context)
MIDIClientDispose(this->client);
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_core: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_in_core: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI; }
bool open_port(const input_port& info, std::string_view portName) override
{
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
auto source = locate_object(*this, info, kMIDIObjectType_Source);
if (source == 0)
return false;
// Create our local sink
MIDIPortRef port;
OSStatus result = MIDIInputPortCreate(
this->client, toCFString(portName).get(), midiInputCallback, (void*)this, &port);
if (result != noErr)
{
close_client();
error<driver_error>(
this->configuration, "midi_in_core::open_port: error creating macOS MIDI input port: "
+ std::to_string(result));
return false;
}
// Make the connection.
if (result = MIDIPortConnectSource(port, source, nullptr); result != noErr)
{
MIDIPortDispose(port);
close_client();
error<driver_error>(
this->configuration, "midi_in_core::open_port: error connecting macOS MIDI input port.");
return false;
}
// Save our api-specific port information.
this->port = port;
return true;
}
bool open_virtual_port(std::string_view portName) override
{
// Create a virtual MIDI input destination.
MIDIEndpointRef endpoint;
OSStatus result = MIDIDestinationCreate(
this->client, toCFString(portName).get(), midiInputCallback, (void*)this, &endpoint);
if (result != noErr)
{
error<driver_error>(
this->configuration,
"midi_in_core::open_virtual_port: error creating virtual macOS MIDI "
"destination.");
return false;
}
// Save our api-specific connection information.
this->endpoint = endpoint;
return true;
}
void close_port() override
{
if (this->endpoint)
{
MIDIEndpointDispose(this->endpoint);
this->endpoint = 0;
}
if (this->port)
{
MIDIPortDispose(this->port);
this->port = 0;
}
}
timestamp absolute_timestamp() const noexcept override
{
return coremidi_data::time_in_nanos(LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME());
}
static void midiInputCallback(const MIDIPacketList* list, void* procRef, void* /*srcRef*/)
{
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = true,
.absolute_is_monotonic = false,
.has_samples = false,
};
auto& self = *(midi_in_core*)procRef;
const MIDIPacket* packet = &list->packet[0];
for (unsigned int i = 0; i < list->numPackets; ++i)
{
// My interpretation of the CoreMIDI documentation: all message
// types, except sysex, are complete within a packet and there may
// be several of them in a single packet. Sysex messages can be
// broken across multiple packets and PacketLists but are bundled
// alone within each packet (these packets do not contain other
// message types). If sysex messages are split across multiple
// MIDIPacketLists, they must be handled by multiple calls to this
// function.
if (packet->length == 0)
{
packet = MIDIPacketNext(packet);
continue;
}
auto to_ns = [packet] { return time_in_nanos(packet->timeStamp); };
self.m_processing.on_bytes_multi(
{packet->data, packet->data + packet->length},
self.m_processing.timestamp<timestamp_info>(to_ns, 0));
packet = MIDIPacketNext(packet);
}
}
midi1::input_state_machine m_processing{this->configuration};
};
}
@@ -0,0 +1,203 @@
#pragma once
#include <libremidi/backends/coremidi/config.hpp>
#include <libremidi/backends/coremidi/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
namespace libremidi
{
class midi_out_core final
: public midi1::out_api
, private coremidi_data
, public error_handler
{
public:
struct
: output_configuration
, coremidi_output_configuration
{
} configuration;
midi_out_core(output_configuration&& conf, coremidi_output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (auto result = init_client(configuration); result != noErr)
{
error<driver_error>(
this->configuration,
"midi_out_core: error creating MIDI client object: " + std::to_string(result));
return;
}
}
~midi_out_core()
{
midi_out_core::close_port();
if (this->endpoint)
MIDIEndpointDispose(this->endpoint);
close_client();
}
void close_client()
{
if (!configuration.context)
MIDIClientDispose(this->client);
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_core: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_out_core: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI; }
bool open_port(const output_port& info, std::string_view portName) override
{
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
// Find where we want to send
auto destination = locate_object(*this, info, kMIDIObjectType_Destination);
if (destination == 0)
return false;
// Create our local source
MIDIPortRef port;
OSStatus result = MIDIOutputPortCreate(this->client, toCFString(portName).get(), &port);
if (result != noErr)
{
close_client();
error<driver_error>(
this->configuration, "midi_out_core::open_port: error creating macOS MIDI output port.");
return false;
}
// Save our api-specific connection information.
this->port = port;
this->destinationId = destination;
return true;
}
bool open_virtual_port(std::string_view portName) override
{
if (this->endpoint)
{
warning(
configuration,
"midi_out_core::open_virtual_port: a virtual output port already "
"exists!");
return false;
}
// Create a virtual MIDI output source.
MIDIEndpointRef endpoint;
OSStatus result = MIDISourceCreate(this->client, toCFString(portName).get(), &endpoint);
if (result != noErr)
{
error<driver_error>(
this->configuration,
"midi_out_core::initialize: error creating macOS virtual MIDI source.");
return false;
}
// Save our api-specific connection information.
this->endpoint = endpoint;
return true;
}
void close_port() override
{
if (this->endpoint)
{
MIDIEndpointDispose(this->endpoint);
this->endpoint = 0;
}
if (this->port)
{
MIDIPortDispose(this->port);
this->port = 0;
}
}
void send_message(const unsigned char* message, size_t size) override
{
unsigned int nBytes = static_cast<unsigned int>(size);
if (nBytes == 0)
{
warning(configuration, "midi_out_core::send_message: no data in message argument!");
return;
}
if (message[0] != 0xF0 && nBytes > 3)
{
warning(
configuration,
"midi_out_core::send_message: message format problem ... not sysex but "
"> 3 bytes?");
return;
}
const MIDITimeStamp timestamp = LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME();
const ByteCount bufsize = nBytes > 65535 ? 65535 : nBytes;
Byte buffer[bufsize + 16]; // pad for other struct members
ByteCount listSize = sizeof(buffer);
MIDIPacketList* packetList = (MIDIPacketList*)buffer;
ByteCount remainingBytes = nBytes;
while (remainingBytes)
{
MIDIPacket* packet = MIDIPacketListInit(packetList);
// A MIDIPacketList can only contain a maximum of 64K of data, so if our message is longer,
// break it up into chunks of 64K or less and send out as a MIDIPacketList with only one
// MIDIPacket. Here, we reuse the memory allocated above on the stack for all.
ByteCount bytesForPacket = remainingBytes > 65535 ? 65535 : remainingBytes;
const Byte* dataStartPtr = (const Byte*)&message[nBytes - remainingBytes];
packet = MIDIPacketListAdd(
packetList, listSize, packet, timestamp, bytesForPacket, dataStartPtr);
remainingBytes -= bytesForPacket;
if (!packet)
{
error<driver_error>(
this->configuration, "midi_out_core::send_message: could not allocate packet list");
return;
}
// Send to any destinations that may have connected to us.
if (this->endpoint)
{
auto result = MIDIReceived(this->endpoint, packetList);
if (result != noErr)
{
warning(
this->configuration,
"midi_out_core::send_message: error sending MIDI to virtual "
"destinations.");
}
}
// And send to an explicit destination port if we're connected.
if (this->destinationId != 0)
{
auto result = MIDISend(this->port, this->destinationId, packetList);
if (result != noErr)
{
warning(
this->configuration,
"midi_out_core::send_message: error sending MIDI message to port.");
}
}
}
}
MIDIEndpointRef destinationId{};
};
}
@@ -0,0 +1,175 @@
#pragma once
#include <libremidi/backends/coremidi/config.hpp>
#include <libremidi/backends/coremidi/helpers.hpp>
#include <libremidi/detail/observer.hpp>
#include <CoreMIDI/CoreMIDI.h>
#include <bit>
namespace libremidi
{
class observer_core
: public observer_api
, public error_handler
{
public:
struct
: observer_configuration
, coremidi_observer_configuration
{
} configuration;
explicit observer_core(observer_configuration&& conf, coremidi_observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (configuration.client_name.empty())
configuration.client_name = "libremidi observer";
if (!configuration.has_callbacks())
return;
auto result = MIDIClientCreate(
toCFString(configuration.client_name).get(),
+[](const MIDINotification* message, void* ctx) {
((observer_core*)ctx)->notify(message);
},
this, &client);
if (result != noErr)
{
error<driver_error>(
this->configuration,
"midi_in_core: error creating MIDI client object: " + std::to_string(result));
return;
}
if (configuration.on_create_context)
configuration.on_create_context(client);
if (configuration.notify_in_constructor)
{
if (this->configuration.input_added)
for (auto& p : get_input_ports())
this->configuration.input_added(p);
if (this->configuration.output_added)
for (auto& p : get_output_ports())
this->configuration.output_added(p);
}
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI; }
template <bool Input>
auto to_port_info(MIDIObjectRef obj) const noexcept
-> std::optional<std::conditional_t<Input, input_port, output_port>>
{
MIDIEntityRef e{};
MIDIEndpointGetEntity(obj, &e);
bool physical = bool(e);
bool ok = false;
if (physical && this->configuration.track_hardware)
ok = true;
else if ((!physical) && this->configuration.track_virtual)
ok = true;
if (!ok)
return {};
return std::conditional_t<Input, input_port, output_port>{
{.client = (std::uintptr_t)this->client,
.port = std::bit_cast<uint32_t>(get_int_property(obj, kMIDIPropertyUniqueID)),
.manufacturer = get_string_property(obj, kMIDIPropertyManufacturer),
.device_name = get_string_property(obj, kMIDIPropertyModel),
.port_name = get_string_property(obj, kMIDIPropertyName),
.display_name = get_string_property(obj, kMIDIPropertyDisplayName)}};
}
std::vector<libremidi::input_port> get_input_ports() const noexcept override
{
std::vector<libremidi::input_port> ret;
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
for (ItemCount i = 0; i < MIDIGetNumberOfSources(); i++)
{
if (auto p = to_port_info<true>(MIDIGetSource(i)))
ret.push_back(std::move(*p));
}
return ret;
}
std::vector<libremidi::output_port> get_output_ports() const noexcept override
{
std::vector<libremidi::output_port> ret;
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
for (ItemCount i = 0; i < MIDIGetNumberOfDestinations(); i++)
{
if (auto p = to_port_info<false>(MIDIGetDestination(i)))
ret.push_back(std::move(*p));
}
return ret;
}
void notify(const MIDINotification* message)
{
switch (message->messageID)
{
case kMIDIMsgObjectAdded: {
auto obj = reinterpret_cast<const MIDIObjectAddRemoveNotification*>(message);
switch (obj->childType)
{
case kMIDIObjectType_Source:
if (auto& cb = configuration.input_added)
if (auto p = to_port_info<true>(obj->child))
cb(std::move(*p));
break;
case kMIDIObjectType_Destination:
if (auto& cb = configuration.output_added)
if (auto p = to_port_info<false>(obj->child))
cb(std::move(*p));
break;
default:
break;
}
break;
}
case kMIDIMsgObjectRemoved: {
auto obj = reinterpret_cast<const MIDIObjectAddRemoveNotification*>(message);
switch (obj->childType)
{
case kMIDIObjectType_Source:
if (auto& cb = configuration.input_removed)
if (auto p = to_port_info<true>(obj->child))
cb(std::move(*p));
break;
case kMIDIObjectType_Destination:
if (auto& cb = configuration.output_removed)
if (auto p = to_port_info<false>(obj->child))
cb(std::move(*p));
break;
default:
break;
}
break;
}
default:
break;
}
}
~observer_core() { MIDIClientDispose(this->client); }
private:
MIDIClientRef client{};
};
}
@@ -0,0 +1,22 @@
#pragma once
#include <libremidi/backends/coremidi_ump/midi_in.hpp>
#include <libremidi/backends/coremidi_ump/midi_out.hpp>
#include <libremidi/backends/coremidi_ump/observer.hpp>
namespace libremidi::coremidi_ump
{
struct backend
{
using midi_in = midi_in_impl;
using midi_out = midi_out_impl;
using midi_observer = observer_impl;
using midi_in_configuration = coremidi_input_configuration;
using midi_out_configuration = coremidi_output_configuration;
using midi_observer_configuration = coremidi_observer_configuration;
static const constexpr auto API = libremidi::API::COREMIDI_UMP;
static const constexpr auto name = "core_ump";
static const constexpr auto display_name = "CoreMIDI UMP";
static constexpr inline bool available() noexcept { return true; /* todo? */ }
};
}
@@ -0,0 +1,9 @@
#pragma once
#include <libremidi/backends/coremidi/config.hpp>
namespace libremidi::coremidi_ump
{
using input_configuration = coremidi_input_configuration;
using output_configuration = coremidi_output_configuration;
using observer_configuration = coremidi_observer_configuration;
}
@@ -0,0 +1,2 @@
#pragma once
#include <libremidi/backends/coremidi/helpers.hpp>
@@ -0,0 +1,183 @@
#pragma once
#include <libremidi/backends/coremidi_ump/config.hpp>
#include <libremidi/backends/coremidi_ump/helpers.hpp>
#include <libremidi/detail/midi_in.hpp>
namespace libremidi::coremidi_ump
{
class midi_in_impl final
: public midi2::in_api
, private coremidi_data
, public error_handler
{
public:
struct
: ump_input_configuration
, coremidi_ump::input_configuration
{
} configuration;
midi_in_impl(ump_input_configuration&& conf, coremidi_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (auto result = init_client(configuration); result != noErr)
{
error<driver_error>(
this->configuration,
"midi_in_core: error creating MIDI client object: " + std::to_string(result));
return;
}
}
~midi_in_impl() override
{
// Close a connection if it exists.
close_port();
if (this->endpoint)
MIDIEndpointDispose(this->endpoint);
close_client();
}
void close_client()
{
if (!configuration.context)
MIDIClientDispose(this->client);
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_core: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_in_core: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI_UMP; }
bool open_port(const input_port& info, std::string_view portName) override
{
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
auto source = locate_object(*this, info, kMIDIObjectType_Source);
if (source == 0)
return false;
// Create our local sink
MIDIPortRef port;
OSStatus result = MIDIInputPortCreateWithProtocol(
this->client, toCFString(portName).get(), kMIDIProtocol_2_0, &port,
^(const MIDIEventList* evtlist, void* __nullable srcConnRefCon) {
this->midiInputCallback(evtlist, srcConnRefCon);
});
if (result != noErr)
{
close_client();
error<driver_error>(
this->configuration, "midi_in_core::open_port: error creating macOS MIDI input port: "
+ std::to_string(result));
return false;
}
// Make the connection.
if (result = MIDIPortConnectSource(port, source, nullptr); result != noErr)
{
MIDIPortDispose(port);
close_client();
error<driver_error>(
this->configuration, "midi_in_core::open_port: error connecting macOS MIDI input port.");
return false;
}
// Save our api-specific port information.
this->port = port;
return true;
}
bool open_virtual_port(std::string_view portName) override
{
// Create a virtual MIDI input destination.
MIDIEndpointRef endpoint;
OSStatus result = MIDIDestinationCreateWithProtocol(
this->client, toCFString(portName).get(), kMIDIProtocol_2_0, &endpoint,
^(const MIDIEventList* evtlist, void* __nullable srcConnRefCon) {
this->midiInputCallback(evtlist, srcConnRefCon);
});
if (result != noErr)
{
error<driver_error>(
this->configuration,
"midi_in_core::open_virtual_port: error creating virtual macOS MIDI "
"destination.");
return false;
}
// Save our api-specific connection information.
this->endpoint = endpoint;
return true;
}
void close_port() override
{
if (this->endpoint)
{
MIDIEndpointDispose(this->endpoint);
this->endpoint = 0;
}
if (this->port)
{
MIDIPortDispose(this->port);
this->port = 0;
}
}
void set_timestamp(
[[maybe_unused]] const MIDIEventPacket& packet,
[[maybe_unused]] libremidi::ump& msg) noexcept
{
}
timestamp absolute_timestamp() const noexcept override
{
return coremidi_data::time_in_nanos(LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME());
}
void midiInputCallback(const MIDIEventList* list, void* /*srcRef*/)
{
unsigned short nBytes{};
const MIDIEventPacket* packet = &list->packet[0];
for (unsigned int i = 0; i < list->numPackets; ++i)
{
nBytes = packet->wordCount;
if (nBytes == 0)
{
packet = MIDIEventPacketNext(packet);
continue;
}
libremidi::ump msg;
coremidi_data::set_timestamp(*this, packet->timeStamp, msg.timestamp);
if (packet->wordCount <= 4)
{
std::copy_n(packet->words, packet->wordCount, msg.data);
configuration.on_message(std::move(msg));
}
last_time = time_in_nanos(packet->timeStamp);
packet = MIDIEventPacketNext(packet);
}
}
unsigned long long last_time{};
};
}
@@ -0,0 +1,185 @@
#pragma once
#include <libremidi/backends/coremidi_ump/config.hpp>
#include <libremidi/backends/coremidi_ump/helpers.hpp>
#include <libremidi/cmidi2.hpp>
#include <libremidi/detail/midi_out.hpp>
#include <libremidi/detail/ump_stream.hpp>
namespace libremidi::coremidi_ump
{
class midi_out_impl final
: public midi2::out_api
, private coremidi_data
, public error_handler
{
public:
struct
: libremidi::output_configuration
, coremidi_ump::output_configuration
{
} configuration;
midi_out_impl(
libremidi::output_configuration&& conf, coremidi_ump::output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (auto result = init_client(configuration); result != noErr)
{
error<driver_error>(
this->configuration,
"midi_out_impl: error creating MIDI client object: " + std::to_string(result));
return;
}
}
~midi_out_impl()
{
midi_out_impl::close_port();
if (this->endpoint)
MIDIEndpointDispose(this->endpoint);
close_client();
}
void close_client()
{
if (!configuration.context)
MIDIClientDispose(this->client);
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_impl: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_out_impl: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI_UMP; }
bool open_port(const output_port& info, std::string_view portName) override
{
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false);
// Find where we want to send
auto destination = locate_object(*this, info, kMIDIObjectType_Destination);
if (destination == 0)
return false;
// Create our local source
MIDIPortRef port;
OSStatus result = MIDIOutputPortCreate(this->client, toCFString(portName).get(), &port);
if (result != noErr)
{
close_client();
error<driver_error>(
this->configuration, "midi_out_impl::open_port: error creating macOS MIDI output port.");
return false;
}
// Save our api-specific connection information.
this->port = port;
this->destinationId = destination;
return true;
}
bool open_virtual_port(std::string_view portName) override
{
if (this->endpoint)
{
warning(
configuration,
"midi_out_impl::open_virtual_port: a virtual output port already "
"exists!");
return false;
}
// Create a virtual MIDI output source.
OSStatus result = MIDISourceCreateWithProtocol(
this->client, toCFString(portName).get(), kMIDIProtocol_2_0, &this->endpoint);
if (result != noErr)
{
this->endpoint = 0;
error<driver_error>(
this->configuration,
"midi_out_impl::initialize: error creating macOS virtual MIDI source.");
return false;
}
return true;
}
void close_port() override
{
if (this->endpoint)
{
MIDIEndpointDispose(this->endpoint);
this->endpoint = 0;
}
if (this->port)
{
MIDIPortDispose(this->port);
this->port = 0;
}
}
void send_ump(const uint32_t* ump_stream, std::size_t count) override
{
MIDIEventList* eventList = reinterpret_cast<MIDIEventList*>(m_eventListBuffer);
MIDIEventPacket* packet = MIDIEventListInit(eventList, kMIDIProtocol_2_0);
const MIDITimeStamp ts = LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME();
auto write_fun = [ts, &packet, &eventList](const uint32_t* ump, int bytes) {
packet = MIDIEventListAdd(eventList, event_list_max_size, packet, ts, bytes / 4, ump);
if (packet)
return segmentation_error::no_error;
else
return segmentation_error::need_space;
};
auto realloc_fun = [this, &packet, &eventList] {
push_event_list(eventList);
packet = MIDIEventListInit(eventList, kMIDIProtocol_2_0);
};
segment_ump_stream(ump_stream, count, write_fun, realloc_fun);
push_event_list(eventList);
}
void push_event_list(MIDIEventList* eventList)
{
if (this->endpoint)
{
auto result = MIDIReceivedEventList(this->endpoint, eventList);
if (result != noErr)
{
warning(
this->configuration,
"midi_out_core::send_message: error sending MIDI to virtual "
"destinations.");
}
}
if (this->destinationId != 0)
{
auto result = MIDISendEventList(this->port, this->destinationId, eventList);
if (result != noErr)
{
warning(
this->configuration,
"midi_out_core::send_message: error sending MIDI message to port.");
}
}
}
MIDIEndpointRef destinationId{};
static constexpr int event_list_max_size = 65535;
unsigned char m_eventListBuffer[sizeof(MIDIEventList) + event_list_max_size];
};
}
@@ -0,0 +1,13 @@
#pragma once
#include <libremidi/backends/coremidi/observer.hpp>
namespace libremidi::coremidi_ump
{
class observer_impl final : public libremidi::observer_core
{
using observer_core::observer_core;
libremidi::API get_current_api() const noexcept override { return libremidi::API::COREMIDI_UMP; }
};
}
+80
View File
@@ -0,0 +1,80 @@
#pragma once
#include <libremidi/configurations.hpp>
#include <libremidi/detail/midi_api.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_out.hpp>
#include <libremidi/detail/observer.hpp>
namespace libremidi
{
class observer_dummy : public observer_api
{
public:
explicit observer_dummy(const auto& /*configuration*/, const auto&) { }
~observer_dummy() { }
libremidi::API get_current_api() const noexcept override { return libremidi::API::DUMMY; }
std::vector<libremidi::input_port> get_input_ports() const noexcept override { return {}; }
std::vector<libremidi::output_port> get_output_ports() const noexcept override { return {}; }
};
class midi_in_dummy final
: public midi1::in_api
, public error_handler
{
public:
explicit midi_in_dummy(const auto& configuration, const auto&)
{
warning(configuration, "midi_in_dummy: This class provides no functionality.");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::DUMMY; }
bool open_port(const input_port& /*pt*/, std::string_view /*local_port_name*/) override
{
return true;
}
bool open_virtual_port(std::string_view /*portName*/) override { return true; }
void close_port() override { }
void set_client_name(std::string_view /*clientName*/) override { }
void set_port_name(std::string_view /*portName*/) override { }
timestamp absolute_timestamp() const noexcept override { return 0; }
};
class midi_out_dummy final
: public midi1::out_api
, public error_handler
{
public:
explicit midi_out_dummy(const auto& configuration, const auto&)
{
warning(configuration, "midi_out_dummy: This class provides no functionality.");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::DUMMY; }
bool open_port(const output_port& /*pt*/, std::string_view /*local_port_name*/) override
{
return true;
}
bool open_virtual_port(std::string_view /*portName*/) override { return true; }
void close_port() override { }
void set_client_name(std::string_view /*clientName*/) override { }
void set_port_name(std::string_view /*portName*/) override { }
void send_message(const unsigned char* /*message*/, size_t /*size*/) override { }
};
struct dummy_backend
{
using midi_in = midi_in_dummy;
using midi_out = midi_out_dummy;
using midi_observer = observer_dummy;
using midi_in_configuration = dummy_configuration;
using midi_out_configuration = dummy_configuration;
using midi_observer_configuration = dummy_configuration;
static const constexpr auto API = libremidi::API::DUMMY;
static const constexpr auto name = "dummy";
static const constexpr auto display_name = "Dummy";
static constexpr inline bool available() noexcept { return true; }
};
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#if defined(__EMSCRIPTEN__)
#include <libremidi/backends/emscripten/config.hpp>
#include <libremidi/backends/emscripten/midi_in.hpp>
#include <libremidi/backends/emscripten/midi_out.hpp>
#include <libremidi/backends/emscripten/observer.hpp>
namespace libremidi
{
struct emscripten_backend
{
using midi_in = midi_in_emscripten;
using midi_out = midi_out_emscripten;
using midi_observer = observer_emscripten;
using midi_in_configuration = emscripten_input_configuration;
using midi_out_configuration = emscripten_output_configuration;
using midi_observer_configuration = emscripten_observer_configuration;
static const constexpr auto API = libremidi::API::WEBMIDI;
static const constexpr auto name = "webmidi";
static const constexpr auto display_name = "WebMIDI";
static constexpr inline bool available() noexcept { return true; }
};
}
#endif
@@ -0,0 +1,17 @@
#pragma once
#include <libremidi/config.hpp>
namespace libremidi
{
struct emscripten_input_configuration
{
};
struct emscripten_output_configuration
{
};
struct emscripten_observer_configuration
{
};
}
@@ -0,0 +1,17 @@
#pragma once
#include <libremidi/config.hpp>
#include <string>
namespace libremidi
{
namespace webmidi_helpers
{
struct device_information
{
std::string id;
std::string name;
bool connected{};
};
}
}
@@ -0,0 +1,22 @@
#if defined(__EMSCRIPTEN__)
#include <libremidi/backends/emscripten/midi_access.hpp>
extern "C" {
LIBREMIDI_EXPORT
LIBREMIDI_INLINE
EMSCRIPTEN_KEEPALIVE
void libremidi_devices_poll()
{
libremidi::webmidi_helpers::midi_access_emscripten::instance().devices_poll();
}
LIBREMIDI_EXPORT
LIBREMIDI_INLINE
EMSCRIPTEN_KEEPALIVE
void libremidi_devices_input(int port, double timestamp, int len, char* bytes)
{
libremidi::webmidi_helpers::midi_access_emscripten::instance().devices_input(
port, timestamp, len, bytes);
}
}
#endif
@@ -0,0 +1,303 @@
#pragma once
#include <libremidi/backends/emscripten/midi_in.hpp>
#include <libremidi/backends/emscripten/midi_out.hpp>
#include <libremidi/backends/emscripten/observer.hpp>
#include <libremidi/libremidi.hpp>
#include <emscripten.h>
#include <map>
extern "C" {
EMSCRIPTEN_KEEPALIVE
void libremidi_devices_poll();
EMSCRIPTEN_KEEPALIVE
void libremidi_devices_input(int port, double timestamp, int len, char* bytes);
}
namespace libremidi
{
namespace webmidi_helpers
{
class midi_access_emscripten
{
public:
static midi_access_emscripten& instance() noexcept
{
static midi_access_emscripten inst;
return inst;
}
bool available() const noexcept
{
return EM_ASM_INT(return typeof globalThis.__libreMidi_access !== undefined;);
}
int input_count() const noexcept
{
return EM_ASM_INT(return globalThis.__libreMidi_currentInputs.length;);
}
int output_count() const noexcept
{
return EM_ASM_INT(return globalThis.__libreMidi_currentOutputs.length;);
}
void load_current_infos() noexcept
{
#define get_js_string(variable_to_read, ...) \
(char*)EM_ASM_INT( \
{ \
var jsstr = variable_to_read; \
var bytes = lengthBytesUTF8(jsstr) + 1; \
var str = _malloc(bytes); \
stringToUTF8(jsstr, str, bytes); \
return str; \
}, \
__VA_ARGS__);
EM_ASM_INT({
let inputs = [];
let outputs = [];
for (let inpt of globalThis.__libreMidi_access.inputs.values())
{
inputs.push(inpt);
}
for (let outpt of globalThis.__libreMidi_access.outputs.values())
{
outputs.push(outpt);
}
globalThis.__libreMidi_currentInputs = inputs;
globalThis.__libreMidi_currentOutputs = outputs;
});
const int inputs = input_count();
const int outputs = output_count();
m_current_inputs.resize(inputs);
m_current_outputs.resize(outputs);
for (int i = 0; i < inputs; i++)
{
int device_index = -1;
char* midi_id = get_js_string(globalThis.__libreMidi_currentInputs[$0].id, i);
auto it = m_input_indices.find(midi_id); // TODO transparent comparator, string_view...
if (it == m_input_indices.end())
{
device_index = m_input_indices.size();
m_input_indices[midi_id] = device_index;
}
else
{
device_index = it->second;
}
char* midi_name = get_js_string(globalThis.__libreMidi_currentInputs[$0].name, i);
const bool connected
= EM_ASM_INT(return globalThis.__libreMidi_currentInputs[$0].state === "connected", i);
m_current_inputs[device_index].id = midi_id;
m_current_inputs[device_index].name = midi_name;
m_current_inputs[device_index].connected = connected;
free(midi_id);
free(midi_name);
}
for (int i = 0; i < outputs; i++)
{
int device_index = -1;
char* midi_id = get_js_string(globalThis.__libreMidi_currentOutputs[$0].id, i);
auto it = m_output_indices.find(midi_id); // TODO transparent comparator, string_view...
if (it == m_output_indices.end())
{
device_index = m_output_indices.size();
m_output_indices[midi_id] = device_index;
}
else
{
device_index = it->second;
}
char* midi_name = get_js_string(globalThis.__libreMidi_currentOutputs[$0].name, i);
const bool connected
= EM_ASM_INT(return globalThis.__libreMidi_currentOutputs[$0].state === "connected", i);
m_current_outputs[device_index].id = midi_id;
m_current_outputs[device_index].name = midi_name;
m_current_outputs[device_index].connected = connected;
free(midi_id);
free(midi_name);
}
#undef get_js_string
}
void register_observer(observer_emscripten& obs)
{
m_observers.push_back(&obs);
if (m_observers.size() == 1)
{
start_observing();
}
}
void unregister_observer(observer_emscripten& obs)
{
if (m_observers.size() == 1)
{
stop_observing();
}
auto it = std::find(m_observers.begin(), m_observers.end(), &obs);
if (it != m_observers.end())
{
m_observers.erase(it);
}
}
void devices_poll()
{
load_current_infos();
for (auto& obs : m_observers)
{
obs->update(m_current_inputs, m_current_outputs);
}
}
void open_input(int port_index, midi_in_emscripten& input)
{
auto& vec = m_opened_inputs[port_index];
vec.push_back(&input);
if (vec.size() != 1)
return;
start_stream(port_index);
}
void close_input(int port_index, midi_in_emscripten& input)
{
auto& vec = m_opened_inputs[port_index];
auto it = std::find(vec.begin(), vec.end(), &input);
if (it != vec.end())
{
vec.erase(it);
}
if (vec.empty())
{
stop_stream(port_index);
}
}
void devices_input(int port, double timestamp, int len, char* data)
{
unsigned char* bytes = reinterpret_cast<unsigned char*>(data);
for (auto input : m_opened_inputs[port])
{
input->on_input(timestamp, bytes, bytes + len);
}
}
void send_message(int port_index, const char* bytes, int len)
{
const auto& id = m_current_outputs[port_index].id;
EM_ASM(
{
let data = HEAPU8.subarray($0, $0 + $1);
const id = UTF8ToString($2);
let output = globalThis.__libreMidi_access.outputs.get(id);
let bytes = HEAPU8.subarray($0, $0 + $1);
output.send(Array.from(bytes));
},
bytes, len, id.c_str());
}
const std::vector<device_information>& inputs() const noexcept { return m_current_inputs; }
const std::vector<device_information>& outputs() const noexcept { return m_current_outputs; }
private:
midi_access_emscripten() noexcept
{
EM_ASM(
if (navigator.requestMIDIAccess) {
navigator.requestMIDIAccess().then(
(midiAccess) => { globalThis.__libreMidi_access = midiAccess; },
() => console.log('MIDI support rejected, MIDI will not be available;'));
} else { console.log('WebMIDI is not supported in this browser.'); });
}
~midi_access_emscripten() { stop_observing(); }
void start_observing()
{
EM_ASM(
let id = setInterval(Module._libremidi_devices_poll, 100);
globalThis.__libreMidi_timer = id;);
}
void stop_observing()
{
EM_ASM(clearInterval(globalThis.__libreMidi_timer); globalThis.__libreMidi_timer = undefined;);
}
void start_stream(int port_index)
{
// Isn't life great...
// https://github.com/Planeshifter/emscripten-examples/tree/master/01_PassingArrays
const auto& id = m_current_inputs[port_index].id;
EM_ASM(
const port_index = $0;
const id = UTF8ToString($1);
let input = globalThis.__libreMidi_access.inputs.get(id);
function _arrayToHeap(typedArray){
const numBytes = typedArray.length * typedArray.BYTES_PER_ELEMENT;
const ptr = Module._malloc(numBytes);
const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes);
heapBytes.set(new Uint8Array(typedArray.buffer));
return heapBytes;
}
function _freeArray(heapBytes){
Module._free(heapBytes.byteOffset);
}
input.onmidimessage = (message) => {
let bytes = message.data;
var heapBytes = _arrayToHeap(bytes);
Module._libremidi_devices_input(port_index, message.timeStamp, bytes.length, heapBytes.byteOffset);
_freeArray(heapBytes);
};
, port_index
, id.c_str()
);
}
void stop_stream(int port_index)
{
const auto& id = m_current_inputs[port_index].id;
EM_ASM(const id = UTF8ToString($1);
let input = globalThis.__libreMidi_access.inputs.get(id);
input.onmidimessage = undefined;, id.c_str());
}
std::vector<observer_emscripten*> m_observers;
std::vector<device_information> m_current_inputs;
std::vector<device_information> m_current_outputs;
std::map<int, std::vector<midi_in_emscripten*>> m_opened_inputs;
std::map<std::string, int> m_input_indices;
std::map<std::string, int> m_output_indices;
};
}
}
@@ -0,0 +1,91 @@
#if defined(__EMSCRIPTEN__)
#include <libremidi/backends/emscripten/midi_access.hpp>
#include <libremidi/backends/emscripten/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
#include <chrono>
namespace libremidi
{
LIBREMIDI_INLINE midi_in_emscripten::midi_in_emscripten(
input_configuration&& conf, emscripten_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
}
LIBREMIDI_INLINE midi_in_emscripten::~midi_in_emscripten()
{
// Close a connection if it exists.
midi_in_emscripten::close_port();
}
LIBREMIDI_INLINE libremidi::API midi_in_emscripten::get_current_api() const noexcept
{
return libremidi::API::WEBMIDI;
}
LIBREMIDI_INLINE bool midi_in_emscripten::open_port(int portNumber, std::string_view)
{
auto& midi = webmidi_helpers::midi_access_emscripten::instance();
if (portNumber < 0 || portNumber >= midi.input_count())
{
error<no_devices_found_error>(
this->configuration, "midi_in_emscripten::open_port: no MIDI output sources found.");
return false;
}
midi.open_input(portNumber, *this);
portNumber_ = portNumber;
return true;
}
LIBREMIDI_INLINE bool
midi_in_emscripten::open_port(const libremidi::input_port& p, std::string_view nm)
{
return open_port(p.port, nm);
}
LIBREMIDI_INLINE bool midi_in_emscripten::open_virtual_port(std::string_view)
{
warning(configuration, "midi_in_emscripten::open_virtual_port: unsupported.");
return false;
}
LIBREMIDI_INLINE void midi_in_emscripten::close_port()
{
auto& midi = webmidi_helpers::midi_access_emscripten::instance();
midi.close_input(portNumber_, *this);
}
LIBREMIDI_INLINE void midi_in_emscripten::set_client_name(std::string_view)
{
warning(configuration, "midi_in_emscripten::set_client_name: unsupported.");
}
LIBREMIDI_INLINE void midi_in_emscripten::set_port_name(std::string_view)
{
warning(configuration, "midi_in_emscripten::set_port_name: unsupported.");
}
LIBREMIDI_INLINE int64_t midi_in_emscripten::absolute_timestamp() const noexcept
{
return system_ns();
}
LIBREMIDI_INLINE void
midi_in_emscripten::on_input(double ts, unsigned char* begin, unsigned char* end)
{
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = true,
.absolute_is_monotonic = true,
.has_samples = false,
};
const auto to_ns = [=] { return 1e6 * ts; };
m_processing.on_bytes({begin, end}, m_processing.timestamp<timestamp_info>(to_ns, 0));
}
}
#endif
@@ -0,0 +1,41 @@
#pragma once
#include <libremidi/backends/emscripten/config.hpp>
#include <libremidi/backends/emscripten/helpers.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
namespace libremidi
{
class midi_in_emscripten final
: public midi1::in_api
, public error_handler
{
public:
struct
: input_configuration
, emscripten_input_configuration
{
} configuration;
midi_in_emscripten(input_configuration&& conf, emscripten_input_configuration&& apiconf);
~midi_in_emscripten() override;
libremidi::API get_current_api() const noexcept override;
bool open_port(int portNumber, std::string_view);
bool open_port(const input_port& p, std::string_view) override;
bool open_virtual_port(std::string_view) override;
void close_port() override;
void set_client_name(std::string_view clientName) override;
void set_port_name(std::string_view portName) override;
timestamp absolute_timestamp() const noexcept override;
void on_input(double ts, unsigned char* begin, unsigned char* end);
private:
int portNumber_{};
midi1::input_state_machine m_processing{this->configuration};
};
}
@@ -0,0 +1,74 @@
#if defined(__EMSCRIPTEN__)
#include <libremidi/backends/emscripten/midi_access.hpp>
#include <libremidi/backends/emscripten/midi_out.hpp>
namespace libremidi
{
LIBREMIDI_INLINE midi_out_emscripten::midi_out_emscripten(
output_configuration&& conf, emscripten_output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
}
LIBREMIDI_INLINE midi_out_emscripten::~midi_out_emscripten()
{
// Close a connection if it exists.
midi_out_emscripten::close_port();
}
LIBREMIDI_INLINE libremidi::API midi_out_emscripten::get_current_api() const noexcept
{
return libremidi::API::WEBMIDI;
}
LIBREMIDI_INLINE bool midi_out_emscripten::open_port(unsigned int portNumber, std::string_view)
{
auto& midi = webmidi_helpers::midi_access_emscripten::instance();
if (portNumber >= midi.output_count())
{
error<no_devices_found_error>(
this->configuration, "midi_out_emscripten::open_port: no MIDI output sources found.");
return false;
}
portNumber_ = portNumber;
return true;
}
LIBREMIDI_INLINE bool midi_out_emscripten::open_port(const output_port& p, std::string_view nm)
{
return open_port(p.port, nm);
}
LIBREMIDI_INLINE void midi_out_emscripten::close_port() { }
LIBREMIDI_INLINE void midi_out_emscripten::set_client_name(std::string_view clientName)
{
warning(configuration, "midi_out_emscripten::set_client_name: unsupported.");
}
LIBREMIDI_INLINE void midi_out_emscripten::set_port_name(std::string_view portName)
{
warning(configuration, "midi_out_emscripten::set_port_name: unsupported.");
}
LIBREMIDI_INLINE bool midi_out_emscripten::open_virtual_port(std::string_view)
{
warning(configuration, "midi_in_emscripten::open_virtual_port: unsupported.");
return false;
}
LIBREMIDI_INLINE void midi_out_emscripten::send_message(const unsigned char* message, size_t size)
{
if (portNumber_ < 0)
error<invalid_use_error>(
this->configuration,
"midi_out_emscripten::send_message: trying to send a message without an open "
"port.");
webmidi_helpers::midi_access_emscripten::instance().send_message(
portNumber_, reinterpret_cast<const char*>(message), size);
}
}
#endif
@@ -0,0 +1,37 @@
#pragma once
#include <libremidi/backends/emscripten/config.hpp>
#include <libremidi/backends/emscripten/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
namespace libremidi
{
class midi_out_emscripten final
: public midi1::out_api
, public error_handler
{
public:
struct
: output_configuration
, emscripten_output_configuration
{
} configuration;
midi_out_emscripten(output_configuration&& conf, emscripten_output_configuration&& apiconf);
~midi_out_emscripten() override;
libremidi::API get_current_api() const noexcept override;
bool open_port(unsigned int portNumber, std::string_view);
bool open_port(const output_port& p, std::string_view) override;
bool open_virtual_port(std::string_view) override;
void close_port() override;
void set_client_name(std::string_view clientName) override;
void set_port_name(std::string_view portName) override;
void send_message(const unsigned char* message, size_t size) override;
private:
int portNumber_{-1};
};
}
@@ -0,0 +1,105 @@
#if defined(__EMSCRIPTEN__)
#include <libremidi/backends/emscripten/midi_access.hpp>
#include <libremidi/backends/emscripten/observer.hpp>
#include <cassert>
namespace libremidi
{
LIBREMIDI_INLINE observer_emscripten::observer_emscripten(
observer_configuration&& conf, emscripten_observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (!configuration.has_callbacks())
return;
auto& webmidi = webmidi_helpers::midi_access_emscripten::instance();
webmidi.register_observer(*this);
// Trigger an initial notification
webmidi.load_current_infos();
if (configuration.notify_in_constructor)
update(webmidi.inputs(), webmidi.outputs());
}
LIBREMIDI_INLINE observer_emscripten::~observer_emscripten()
{
if (!configuration.has_callbacks())
return;
webmidi_helpers::midi_access_emscripten::instance().unregister_observer(*this);
}
LIBREMIDI_INLINE
libremidi::API observer_emscripten::get_current_api() const noexcept
{
return libremidi::API::WEBMIDI;
}
template <bool Input>
static auto to_port_info(int index, const webmidi_helpers::device_information& dev)
-> std::conditional_t<Input, input_port, output_port>
{
return {
{.client = 0,
.port = (uint64_t)index,
.manufacturer = "",
.device_name = "",
.port_name = dev.name,
.display_name = dev.name}};
}
LIBREMIDI_INLINE
std::vector<libremidi::input_port> observer_emscripten::get_input_ports() const noexcept
{
std::vector<libremidi::input_port> ret;
auto& webmidi = webmidi_helpers::midi_access_emscripten::instance();
webmidi.load_current_infos();
for (std::size_t i = 0, n = webmidi.input_count(); i < n; i++)
{
ret.push_back(to_port_info<true>(i, webmidi.inputs()[i]));
}
return ret;
}
LIBREMIDI_INLINE
std::vector<libremidi::output_port> observer_emscripten::get_output_ports() const noexcept
{
std::vector<libremidi::output_port> ret;
auto& webmidi = webmidi_helpers::midi_access_emscripten::instance();
webmidi.load_current_infos();
for (std::size_t i = 0, n = webmidi.output_count(); i < n; i++)
{
ret.push_back(to_port_info<false>(i, webmidi.outputs()[i]));
}
return ret;
}
LIBREMIDI_INLINE void observer_emscripten::update(
const std::vector<observer_emscripten::device>& current_inputs,
const std::vector<observer_emscripten::device>& current_outputs)
{
// WebMIDI never remove inputs, it just marks them as disconnected.
// At least in known browsers...
assert(current_inputs.size() >= m_known_inputs.size());
assert(current_outputs.size() >= m_known_outputs.size());
for (std::size_t i = m_known_inputs.size(); i < current_inputs.size(); i++)
{
m_known_inputs.push_back(current_inputs[i]);
configuration.input_added(to_port_info<true>(i, m_known_inputs[i]));
}
for (std::size_t i = m_known_outputs.size(); i < current_outputs.size(); i++)
{
m_known_outputs.push_back(current_outputs[i]);
configuration.output_added(to_port_info<false>(i, m_known_outputs[i]));
}
}
}
#endif
@@ -0,0 +1,37 @@
#pragma once
#include <libremidi/backends/emscripten/config.hpp>
#include <libremidi/backends/emscripten/helpers.hpp>
#include <libremidi/detail/observer.hpp>
#include <vector>
namespace libremidi
{
class observer_emscripten final : public observer_api
{
using device = webmidi_helpers::device_information;
public:
struct
: observer_configuration
, emscripten_observer_configuration
{
} configuration;
explicit observer_emscripten(
observer_configuration&& conf, emscripten_observer_configuration&& apiconf);
~observer_emscripten();
void
update(const std::vector<device>& current_inputs, const std::vector<device>& current_outputs);
libremidi::API get_current_api() const noexcept override;
std::vector<libremidi::input_port> get_input_ports() const noexcept override;
std::vector<libremidi::output_port> get_output_ports() const noexcept override;
private:
std::vector<device> m_known_inputs;
std::vector<device> m_known_outputs;
};
}
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include <libremidi/backends/jack/midi_in.hpp>
#include <libremidi/backends/jack/midi_out.hpp>
#include <libremidi/backends/jack/observer.hpp>
//*********************************************************************//
// API: UNIX JACK
//
// Written primarily by Alexander Svetalkin, with updates for delta
// time by Gary Scavone, April 2011.
//
// *********************************************************************//
namespace libremidi
{
struct jack_backend
{
using midi_in = midi_in_jack;
using midi_out = midi_out_jack;
using midi_observer = observer_jack;
using midi_in_configuration = jack_input_configuration;
using midi_out_configuration = jack_output_configuration;
using midi_observer_configuration = jack_observer_configuration;
static const constexpr auto API = libremidi::API::JACK_MIDI;
static const constexpr auto name = "jack";
static const constexpr auto display_name = "JACK";
static inline bool available() noexcept
{
#if LIBREMIDI_WEAKJACK
return WeakJack::instance().available() == 0;
#else
return true;
#endif
}
};
}
@@ -0,0 +1,50 @@
#pragma once
#include <libremidi/config.hpp>
#include <cinttypes>
#include <cstdint>
#include <functional>
#include <string>
extern "C" {
typedef struct _jack_client jack_client_t;
typedef uint32_t jack_nframes_t;
typedef int (*JackProcessCallback)(jack_nframes_t nframes, void* arg);
}
namespace libremidi
{
using jack_callback_function = std::function<void(int nframes)>;
struct jack_callback
{
int64_t token;
std::function<void(int nframes)> callback;
};
struct jack_input_configuration
{
std::string client_name = "libremidi client";
jack_client_t* context{};
std::function<void(jack_callback)> set_process_func;
std::function<void(int64_t)> clear_process_func;
};
struct jack_output_configuration
{
std::string client_name = "libremidi client";
jack_client_t* context{};
std::function<void(jack_callback)> set_process_func;
std::function<void(int64_t)> clear_process_func;
int32_t ringbuffer_size = 16384;
bool direct = false;
};
struct jack_observer_configuration
{
std::string client_name = "libremidi client";
jack_client_t* context{};
};
}
+241
View File
@@ -0,0 +1,241 @@
#pragma once
#if __has_include(<weakjack/weak_libjack.h>)
#include <weakjack/weak_libjack.h>
#elif __has_include(<weak_libjack.h>)
#include <weak_libjack.h>
#elif __has_include(<jack/jack.h> )
#include <jack/jack.h>
#include <jack/midiport.h>
#include <jack/ringbuffer.h>
#endif
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/semaphore.hpp>
#include <atomic>
#include <semaphore>
namespace libremidi
{
struct jack_client
{
jack_client_t* client{};
static std::string get_port_display_name(jack_port_t* port)
{
auto p1 = std::make_unique<char[]>(jack_port_name_size());
auto p2 = std::make_unique<char[]>(jack_port_name_size());
char* aliases[3] = {p1.get(), p2.get(), nullptr};
int n = jack_port_get_aliases(port, aliases);
if (n > 1)
{
return aliases[1];
}
else if (n > 0)
{
std::string str = aliases[0];
if (str.starts_with("alsa_pcm:"))
str.erase(0, strlen("alsa_pcm:"));
return str;
}
else
{
const auto short_name = jack_port_short_name(port);
if (short_name && strlen(short_name) > 0)
return short_name;
return jack_port_name(port);
}
}
template <bool Input>
static auto to_port_info(jack_client_t* client, jack_port_t* port)
-> std::conditional_t<Input, input_port, output_port>
{
return {{
.client = reinterpret_cast<std::uintptr_t>(client),
.port = 0,
.manufacturer = "",
.device_name = "",
.port_name = jack_port_name(port),
.display_name = get_port_display_name(port),
}};
}
template <bool Input>
static auto
get_ports(jack_client_t* client, const char* pattern, const JackPortFlags flags) noexcept
-> std::vector<std::conditional_t<Input, input_port, output_port>>
{
std::vector<std::conditional_t<Input, input_port, output_port>> ret;
if (!client)
return {};
const char** ports = jack_get_ports(client, pattern, JACK_DEFAULT_MIDI_TYPE, flags);
if (ports == nullptr)
return {};
int i = 0;
while (ports[i] != nullptr)
{
// FIXME this does not take into account filtering sw / hw ports
auto port = jack_port_by_name(client, ports[i]);
ret.push_back(to_port_info<Input>(client, port));
i++;
}
jack_free(ports);
return ret;
}
};
struct jack_helpers : jack_client
{
struct port_handle
{
port_handle& operator=(jack_port_t* p)
{
impl.get()->store(p);
return *this;
}
operator jack_port_t*() const noexcept
{
if (impl)
return impl.get()->load();
return {};
}
std::shared_ptr<std::atomic<jack_port_t*>> impl
= std::make_shared<std::atomic<jack_port_t*>>(nullptr);
} port;
int64_t this_instance{};
semaphore_pair_lock thread_lock;
jack_helpers()
{
static std::atomic_int64_t instance{};
this_instance = ++instance;
}
template <typename Self>
jack_status_t connect(Self& self)
{
auto& configuration = self.configuration;
if (this->client)
return jack_status_t{};
// Initialize JACK client
if (configuration.context)
{
if (!configuration.set_process_func)
return JackFailure;
configuration.set_process_func(
{.token = this_instance,
.callback = [&self, p = std::weak_ptr{this->port.impl}](jack_nframes_t nf) -> int {
if (auto pt = p.lock())
if (auto ppt = pt->load())
self.process(nf);
self.thread_lock.check_client_released();
return 0;
}});
this->client = configuration.context;
return jack_status_t{};
}
else
{
jack_status_t status{};
this->client
= jack_client_open(configuration.client_name.c_str(), JackNoStartServer, &status);
if (this->client != nullptr)
{
jack_set_process_callback(
this->client,
+[](jack_nframes_t nf, void* ctx) -> int {
auto& self = *static_cast<Self*>(ctx);
jack_port_t* port = self.port;
// Is port created?
if (port == nullptr)
return 0;
self.process(nf);
self.thread_lock.check_client_released();
return 0;
},
&self);
jack_activate(this->client);
}
return status;
}
}
template <typename Self>
void disconnect(Self& self)
{
if (self.configuration.context)
{
if (self.configuration.clear_process_func)
{
self.configuration.clear_process_func(this_instance);
}
}
if (this->client && !self.configuration.context)
jack_client_close(this->client);
}
bool create_local_port(const auto& self, std::string_view portName, JackPortFlags flags)
{
// full name: "client_name:port_name\0"
if (portName.empty())
portName = flags & JackPortIsInput ? "i" : "o";
if (self.configuration.client_name.size() + portName.size() + 2u
>= static_cast<size_t>(jack_port_name_size()))
{
self.template error<invalid_use_error>(
self.configuration, "JACK: port name length limit exceeded");
return false;
}
if (!this->port)
{
this->port
= jack_port_register(this->client, portName.data(), JACK_DEFAULT_MIDI_TYPE, flags, 0);
}
if (!this->port)
{
self.template error<driver_error>(self.configuration, "JACK: error creating port");
return false;
}
return true;
}
void do_close_port()
{
if (this->port == nullptr)
return;
// 1. Ensure that the next time the cycle runs it sees the port as nullptr
jack_port_t* port_ptr = this->port.impl->load();
this->port = nullptr;
// 2. Signal through the semaphore and wait for the signal return
this->thread_lock.prepare_release_client();
// 3. Now we are sure that the client is not going to use the port anymore
jack_port_unregister(this->client, port_ptr);
}
};
}
+114
View File
@@ -0,0 +1,114 @@
#pragma once
#include <libremidi/backends/jack/config.hpp>
#include <libremidi/backends/jack/helpers.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
#include <chrono>
namespace libremidi
{
class midi_in_jack final
: public midi1::in_api
, public jack_helpers
, public error_handler
{
public:
struct
: input_configuration
, jack_input_configuration
{
} configuration;
explicit midi_in_jack(input_configuration&& conf, jack_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
auto status = connect(*this);
if (status != jack_status_t{})
warning(configuration, "midi_in_jack: " + std::to_string((int)jack_status_t{}));
}
~midi_in_jack() override
{
midi_in_jack::close_port();
disconnect(*this);
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_jack: set_client_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::JACK_MIDI; }
bool open_port(const input_port& port, std::string_view portName) override
{
if (!create_local_port(*this, portName, JackPortIsInput))
return false;
if (auto ret = jack_connect(this->client, port.port_name.c_str(), jack_port_name(this->port));
ret != 0)
{
error<invalid_parameter_error>(
configuration, "JACK: could not connect to port: " + port.port_name + " -> "
+ jack_port_name(this->port));
return false;
}
return true;
}
bool open_virtual_port(std::string_view portName) override
{
return create_local_port(*this, portName, JackPortIsInput);
}
void close_port() override { return do_close_port(); }
void set_port_name(std::string_view portName) override
{
jack_port_rename(this->client, this->port, portName.data());
}
timestamp absolute_timestamp() const noexcept override
{
return 1000 * jack_frames_to_time(client, jack_frame_time(client));
}
int process(jack_nframes_t nframes)
{
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = true,
.absolute_is_monotonic = true,
.has_samples = true,
};
void* buff = jack_port_get_buffer(this->port, nframes);
// Timing
jack_nframes_t current_frames{};
jack_time_t current_usecs{}; // roughly CLOCK_MONOTONIC
jack_time_t next_usecs{};
float period_usecs{};
jack_get_cycle_times(
this->client, &current_frames, &current_usecs, &next_usecs, &period_usecs);
// We have midi events in buffer
uint32_t evCount = jack_midi_get_event_count(buff);
for (uint32_t j = 0; j < evCount; j++)
{
jack_midi_event_t event{};
jack_midi_event_get(&event, buff, j);
const auto to_ns
= [=, this] { return 1000 * jack_frames_to_time(client, current_frames + event.time); };
m_processing.on_bytes(
{event.buffer, event.buffer + event.size},
m_processing.timestamp<timestamp_info>(to_ns, event.time));
}
return 0;
}
midi1::input_state_machine m_processing{this->configuration};
};
}
@@ -0,0 +1,231 @@
#pragma once
#include <libremidi/backends/jack/config.hpp>
#include <libremidi/backends/jack/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
#include <semaphore>
namespace libremidi
{
struct jack_queue
{
public:
static constexpr auto size_sz = sizeof(int32_t);
jack_queue() = default;
jack_queue(const jack_queue&) = delete;
jack_queue(jack_queue&&) = delete;
jack_queue& operator=(const jack_queue&) = delete;
jack_queue& operator=(jack_queue&& other) noexcept
{
ringbuffer = other.ringbuffer;
ringbuffer_space = other.ringbuffer_space;
other.ringbuffer = nullptr;
return *this;
}
explicit jack_queue(int64_t sz) noexcept
{
ringbuffer = jack_ringbuffer_create(sz);
ringbuffer_space = jack_ringbuffer_write_space(ringbuffer);
}
~jack_queue() noexcept
{
if (ringbuffer)
jack_ringbuffer_free(ringbuffer);
}
void write(const unsigned char* data, int64_t sz) const noexcept
{
if (static_cast<std::size_t>(sz + size_sz) > ringbuffer_space)
return;
while (jack_ringbuffer_write_space(ringbuffer) < sz + size_sz)
sched_yield();
jack_ringbuffer_write(ringbuffer, reinterpret_cast<char*>(&sz), size_sz);
jack_ringbuffer_write(ringbuffer, reinterpret_cast<const char*>(data), sz);
}
void read(void* jack_events) const noexcept
{
int32_t sz;
while (jack_ringbuffer_peek(ringbuffer, reinterpret_cast<char*>(&sz), size_sz) == size_sz
&& jack_ringbuffer_read_space(ringbuffer) >= size_sz + sz)
{
jack_ringbuffer_read_advance(ringbuffer, size_sz);
if (auto midi = jack_midi_event_reserve(jack_events, 0, sz))
jack_ringbuffer_read(ringbuffer, reinterpret_cast<char*>(midi), sz);
else
jack_ringbuffer_read_advance(ringbuffer, sz);
}
}
jack_ringbuffer_t* ringbuffer{};
std::size_t ringbuffer_space{}; // actual writable size, usually 1 less than ringbuffer
};
class midi_out_jack
: public midi1::out_api
, public jack_helpers
, public error_handler
{
public:
struct
: output_configuration
, jack_output_configuration
{
} configuration;
midi_out_jack(output_configuration&& conf, jack_output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
}
~midi_out_jack() override { }
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_jack: set_client_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::JACK_MIDI; }
bool open_port(const output_port& port, std::string_view portName) override
{
if (!create_local_port(*this, portName, JackPortIsOutput))
return false;
// Connecting to the output
if (jack_connect(this->client, jack_port_name(this->port), port.port_name.c_str()) != 0)
{
error<invalid_parameter_error>(
configuration, "JACK: could not connect to port" + port.port_name);
return false;
}
return true;
}
bool open_virtual_port(std::string_view portName) override
{
return create_local_port(*this, portName, JackPortIsOutput);
}
void close_port() override { return do_close_port(); }
void set_port_name(std::string_view portName) override
{
jack_port_rename(this->client, this->port, portName.data());
}
};
class midi_out_jack_queued final : public midi_out_jack
{
public:
midi_out_jack_queued(output_configuration&& conf, jack_output_configuration&& apiconf)
: midi_out_jack{std::move(conf), std::move(apiconf)}
, queue{configuration.ringbuffer_size}
{
auto status = connect(*this);
if (status != jack_status_t{})
warning(configuration, "midi_out_jack_queued: " + std::to_string((int)jack_status_t{}));
}
~midi_out_jack_queued() override
{
midi_out_jack::close_port();
disconnect(*this);
}
void send_message(const unsigned char* message, std::size_t size) override
{
queue.write(message, size);
}
int process(jack_nframes_t nframes)
{
void* buff = jack_port_get_buffer(this->port, nframes);
jack_midi_clear_buffer(buff);
this->queue.read(buff);
return 0;
}
private:
jack_queue queue;
};
class midi_out_jack_direct final : public midi_out_jack
{
public:
midi_out_jack_direct(output_configuration&& conf, jack_output_configuration&& apiconf)
: midi_out_jack{std::move(conf), std::move(apiconf)}
{
auto status = connect(*this);
if (status != jack_status_t{})
warning(configuration, "midi_out_jack_direct: " + std::to_string((int)jack_status_t{}));
buffer_size = jack_get_buffer_size(this->client);
}
~midi_out_jack_direct() override
{
midi_out_jack::close_port();
disconnect(*this);
}
int process(jack_nframes_t nframes)
{
void* buff = jack_port_get_buffer(this->port, nframes);
jack_midi_clear_buffer(buff);
return 0;
}
void send_message(const unsigned char* message, size_t size) override
{
void* buff = jack_port_get_buffer(this->port, buffer_size);
jack_midi_event_write(buff, 0, message, size);
}
int convert_timestamp(int64_t user) const noexcept
{
switch (configuration.timestamps)
{
case timestamp_mode::AudioFrame:
return static_cast<int>(user);
default:
// TODO
return 0;
}
}
void schedule_message(int64_t ts, const unsigned char* message, size_t size) override
{
void* buff = jack_port_get_buffer(this->port, buffer_size);
jack_midi_event_write(buff, convert_timestamp(ts), message, size);
}
int buffer_size{};
};
}
namespace libremidi
{
template <>
inline std::unique_ptr<midi_out_api> make<midi_out_jack>(
libremidi::output_configuration&& conf, libremidi::jack_output_configuration&& api)
{
if (api.direct)
return std::make_unique<midi_out_jack_direct>(std::move(conf), std::move(api));
else
return std::make_unique<midi_out_jack_queued>(std::move(conf), std::move(api));
}
}
@@ -0,0 +1,224 @@
#pragma once
#include <libremidi/backends/jack/config.hpp>
#include <libremidi/backends/jack/helpers.hpp>
#include <libremidi/detail/observer.hpp>
#include <unordered_set>
namespace libremidi
{
class observer_jack final
: public observer_api
, private jack_client
, private error_handler
{
public:
struct
: observer_configuration
, jack_observer_configuration
{
} configuration;
explicit observer_jack(observer_configuration&& conf, jack_observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
// Initialize JACK client
if (configuration.context)
{
this->client = configuration.context;
set_callbacks();
}
else
{
jack_status_t status{};
this->client
= jack_client_open(configuration.client_name.c_str(), JackNoStartServer, &status);
if (status != jack_status_t{})
warning(configuration, "observer_jack: " + std::to_string((int)jack_status_t{}));
if (this->client != nullptr)
{
set_callbacks();
jack_activate(this->client);
}
}
}
void initial_callback()
{
{
const char** ports
= jack_get_ports(client, nullptr, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput);
if (ports != nullptr)
{
int i = 0;
while (ports[i] != nullptr)
{
auto port = jack_port_by_name(client, ports[i]);
auto flags = jack_port_flags(port);
bool physical = flags & JackPortIsPhysical;
bool ok = configuration.track_any;
if (configuration.track_hardware)
ok |= physical;
if (configuration.track_virtual)
ok |= !physical;
if (ok)
{
seen_input_ports.insert(ports[i]);
if (this->configuration.input_added && configuration.notify_in_constructor)
this->configuration.input_added(to_port_info<true>(client, port));
}
i++;
}
}
jack_free(ports);
}
{
const char** ports
= jack_get_ports(client, nullptr, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput);
if (ports != nullptr)
{
int i = 0;
while (ports[i] != nullptr)
{
auto port = jack_port_by_name(client, ports[i]);
auto flags = jack_port_flags(port);
bool physical = flags & JackPortIsPhysical;
bool ok = configuration.track_any;
if (configuration.track_hardware)
ok |= physical;
if (configuration.track_virtual)
ok |= !physical;
if (ok)
{
seen_output_ports.insert(ports[i]);
if (this->configuration.output_added && configuration.notify_in_constructor)
this->configuration.output_added(to_port_info<false>(client, port));
}
i++;
}
}
jack_free(ports);
}
}
void on_port_callback(jack_port_t* port, bool reg)
{
auto flags = jack_port_flags(port);
std::string name = jack_port_name(port);
if (reg)
{
std::string_view type = jack_port_type(port);
if (type != JACK_DEFAULT_MIDI_TYPE)
return;
bool physical = flags & JackPortIsPhysical;
bool ok = configuration.track_any;
if (configuration.track_hardware)
ok |= physical;
if (configuration.track_virtual)
ok |= !physical;
if (!ok)
return;
// Note: we keep track of the ports as
// when disconnecting, jack_port_type and jack_port_flags aren't correctly
// set anymore.
if (flags & JackPortIsOutput)
{
seen_input_ports.insert(name);
if (this->configuration.input_added)
this->configuration.input_added(to_port_info<true>(client, port));
}
else if (flags & JackPortIsInput)
{
seen_output_ports.insert(name);
if (this->configuration.output_added)
this->configuration.output_added(to_port_info<false>(client, port));
}
}
else
{
if (auto it = seen_input_ports.find(name); it != seen_input_ports.end())
{
if (this->configuration.input_removed)
this->configuration.input_removed(to_port_info<true>(client, port));
seen_input_ports.erase(it);
}
if (auto it = seen_output_ports.find(name); it != seen_output_ports.end())
{
if (this->configuration.output_removed)
this->configuration.output_removed(to_port_info<false>(client, port));
seen_output_ports.erase(it);
}
}
}
void set_callbacks()
{
initial_callback();
if (!configuration.has_callbacks())
return;
jack_set_port_registration_callback(
this->client,
+[](jack_port_id_t p, int r, void* arg) {
auto& self = *(observer_jack*)arg;
if (auto port = jack_port_by_id(self.client, p))
{
self.on_port_callback(port, r != 0);
}
},
this);
jack_set_port_rename_callback(
this->client,
+[](jack_port_id_t p, const char* /*old_name*/, const char* /*new_name*/, void* arg) {
const auto& self = *static_cast<observer_jack*>(arg);
auto port = jack_port_by_id(self.client, p);
if (!port)
return;
},
this);
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::JACK_MIDI; }
std::vector<libremidi::input_port> get_input_ports() const noexcept override
{
return get_ports<true>(this->client, nullptr, JackPortIsOutput);
}
std::vector<libremidi::output_port> get_output_ports() const noexcept override
{
return get_ports<false>(this->client, nullptr, JackPortIsInput);
}
~observer_jack()
{
if (client && !configuration.context)
{
// If we own the client, deactivate it
jack_deactivate(this->client);
jack_client_close(this->client);
this->client = nullptr;
}
}
std::unordered_set<std::string> seen_input_ports;
std::unordered_set<std::string> seen_output_ports;
};
}
@@ -0,0 +1,153 @@
#pragma once
#if __has_include(<boost/lockfree/spsc_queue.hpp>)
#include <libremidi/backends/jack/config.hpp>
#include <libremidi/backends/jack/helpers.hpp>
#include <libremidi/shared_context.hpp>
#include <boost/lockfree/spsc_queue.hpp>
#include <variant>
namespace libremidi::jack
{
// Create a JACK client which will be shared across objects
struct shared_handler : public libremidi::shared_context
{
explicit shared_handler(std::string_view v)
{
midiin_callbacks.reserve(64);
midiout_callbacks.reserve(64);
jack_status_t status{};
client = jack_client_open(v.data(), JackNoStartServer, &status);
assert(client);
assert(status == 0);
jack_set_process_callback(
client,
+[](jack_nframes_t cnt, void* ctx) -> int {
((shared_handler*)ctx)->jack_callback(cnt);
return 0;
},
this);
}
virtual void start_processing() override { jack_activate(client); }
virtual void stop_processing() override { jack_deactivate(client); }
static shared_configurations make(std::string_view client_name)
{
auto clt = std::make_shared<shared_handler>(client_name);
auto add_in_cb = [client = std::weak_ptr{clt}](libremidi::jack_callback cb) {
if (auto clt = client.lock())
clt->events.push({shared_handler::event_type::in_callback_added, std::move(cb)});
};
auto clear_in_cb = [client = std::weak_ptr{clt}](int64_t index) {
if (auto clt = client.lock())
clt->events.push({shared_handler::event_type::in_callback_removed, index});
};
auto add_out_cb = [client = std::weak_ptr{clt}](libremidi::jack_callback cb) {
if (auto clt = client.lock())
clt->events.push({shared_handler::event_type::out_callback_added, std::move(cb)});
};
auto clear_out_cb = [client = std::weak_ptr{clt}](int64_t index) {
if (auto clt = client.lock())
clt->events.push({shared_handler::event_type::out_callback_removed, index});
};
return {
.context = clt,
.observer = jack_observer_configuration{.context = clt->client},
.in
= jack_input_configuration{.context = clt->client, .set_process_func = add_in_cb, .clear_process_func = clear_in_cb},
.out
= jack_output_configuration{.context = clt->client, .set_process_func = add_out_cb, .clear_process_func = clear_out_cb},
};
}
int jack_callback(jack_nframes_t cnt)
{
// 1. Process the events that will change the callback list
event ev;
while (events.pop(ev))
{
switch (ev.type)
{
case in_callback_added:
midiin_callbacks.push_back(
std::move(*std::get_if<libremidi::jack_callback>(&ev.payload)));
break;
case in_callback_removed: {
auto idx = *std::get_if<int64_t>(&ev.payload);
for (auto it = midiin_callbacks.begin(); it != midiin_callbacks.end();)
{
if (it->token == idx)
{
midiin_callbacks.erase(it);
break;
}
else
{
++it;
}
}
break;
}
case out_callback_added:
midiout_callbacks.push_back(
std::move(*std::get_if<libremidi::jack_callback>(&ev.payload)));
break;
case out_callback_removed:
auto idx = *std::get_if<int64_t>(&ev.payload);
for (auto it = midiout_callbacks.begin(); it != midiout_callbacks.end();)
{
if (it->token == idx)
{
midiout_callbacks.erase(it);
break;
}
else
{
++it;
}
}
break;
}
}
for (auto& cb : midiin_callbacks)
cb.callback(cnt);
for (auto& cb : midiout_callbacks)
cb.callback(cnt);
return 0;
}
~shared_handler()
{
jack_deactivate(client);
jack_client_close(client);
}
jack_client_t* client{};
enum event_type
{
in_callback_added,
in_callback_removed,
out_callback_added,
out_callback_removed,
};
struct event
{
event_type type;
std::variant<libremidi::jack_callback, int64_t> payload;
};
boost::lockfree::spsc_queue<event> events{16};
std::vector<libremidi::jack_callback> midiin_callbacks;
std::vector<libremidi::jack_callback> midiout_callbacks;
};
}
#endif
+459
View File
@@ -0,0 +1,459 @@
#pragma once
#include <libremidi/backends/linux/dylib_loader.hpp>
#include <alsa/asoundlib.h>
#if defined(SND_LIB_VERSION)
#if __has_include(<alsa/rawmidi.h>) && SND_LIB_VERSION >= ((1 << 16) | (2 << 8) | 6)
#define LIBREMIDI_ALSA_HAS_RAMWIDI 1
#define LIBREMIDI_ALSA_HAS_RAWMIDI_TREAD 1
#endif
#if __has_include(<alsa/ump.h>) && SND_LIB_VERSION >= ((1 << 16) | (2 << 8) | 10)
#define LIBREMIDI_ALSA_HAS_UMP 1
#endif
#endif
namespace libremidi
{
struct libasound
{
// Useful one-liner:
// nm -A * | grep ' snd_' | grep -v '@' | cut -f 2 -d 'U' | sort | uniq | sed 's/ snd_//' | sed 's/_/, /' | awk ' { print "LIBREMIDI_SYMBOL_DEF(snd_"$1 " " $2 ");" }'
explicit libasound()
: library{"libasound.so.2"}
{
if (!library)
{
available = false;
return;
}
strerror = library.symbol<decltype(&::snd_strerror)>("snd_strerror");
if (!strerror)
available = false;
}
static const libasound& instance()
{
static const libasound self;
return self;
}
dylib_loader library;
decltype(&::snd_strerror) strerror{};
bool available{true};
struct card_t
{
explicit card_t(const dylib_loader& library)
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_card, get_name);
LIBREMIDI_SYMBOL_INIT(snd_card, next);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_card, get_name);
LIBREMIDI_SYMBOL_DEF(snd_card, next);
} card{library};
struct ctl_t
{
explicit ctl_t(const dylib_loader& library)
: rawmidi{library}
#if LIBREMIDI_ALSA_HAS_UMP
, ump{library}
#endif
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_ctl, close);
LIBREMIDI_SYMBOL_INIT(snd_ctl, open);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_ctl, close);
LIBREMIDI_SYMBOL_DEF(snd_ctl, open);
struct rawmidi_t
{
explicit rawmidi_t(const dylib_loader& library)
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_ctl_rawmidi, info);
LIBREMIDI_SYMBOL_INIT(snd_ctl_rawmidi, next_device);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_ctl_rawmidi, info);
LIBREMIDI_SYMBOL_DEF(snd_ctl_rawmidi, next_device);
} rawmidi;
#if LIBREMIDI_ALSA_HAS_UMP
struct ump_t
{
explicit ump_t(const dylib_loader& library)
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_ctl_ump, block_info);
LIBREMIDI_SYMBOL_INIT(snd_ctl_ump, endpoint_info);
LIBREMIDI_SYMBOL_INIT(snd_ctl_ump, next_device);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_ctl_ump, block_info);
LIBREMIDI_SYMBOL_DEF(snd_ctl_ump, endpoint_info);
LIBREMIDI_SYMBOL_DEF(snd_ctl_ump, next_device);
} ump;
#endif
} ctl{library};
struct midi_t
{
explicit midi_t(const dylib_loader& library)
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_midi, event_decode);
LIBREMIDI_SYMBOL_INIT(snd_midi, event_encode);
LIBREMIDI_SYMBOL_INIT(snd_midi, event_free);
LIBREMIDI_SYMBOL_INIT(snd_midi, event_init);
LIBREMIDI_SYMBOL_INIT(snd_midi, event_new);
LIBREMIDI_SYMBOL_INIT(snd_midi, event_no_status);
LIBREMIDI_SYMBOL_INIT(snd_midi, event_resize_buffer);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_midi, event_decode);
LIBREMIDI_SYMBOL_DEF(snd_midi, event_encode);
LIBREMIDI_SYMBOL_DEF(snd_midi, event_free);
LIBREMIDI_SYMBOL_DEF(snd_midi, event_init);
LIBREMIDI_SYMBOL_DEF(snd_midi, event_new);
LIBREMIDI_SYMBOL_DEF(snd_midi, event_no_status);
LIBREMIDI_SYMBOL_DEF(snd_midi, event_resize_buffer);
} midi{library};
#if LIBREMIDI_ALSA_HAS_RAMWIDI
struct rawmidi_t
{
explicit rawmidi_t(const dylib_loader& library)
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, close);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, info_get_name);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, info_get_subdevice_name);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, info_get_subdevices_count);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, info_set_device);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, info_set_stream);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, info_set_subdevice);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, info_sizeof);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, open);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, params);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, params_current);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, params_get_buffer_size);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, params_set_clock_type);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, params_set_no_active_sensing);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, params_set_read_mode);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, params_sizeof);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, poll_descriptors);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, poll_descriptors_count);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, poll_descriptors_revents);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, read);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, status);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, status_get_avail);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, status_sizeof);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, tread);
LIBREMIDI_SYMBOL_INIT(snd_rawmidi, write);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, close);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, info_get_name);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, info_get_subdevice_name);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, info_get_subdevices_count);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, info_set_device);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, info_set_stream);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, info_set_subdevice);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, info_sizeof);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, open);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, params);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, params_current);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, params_get_buffer_size);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, params_set_clock_type);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, params_set_no_active_sensing);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, params_set_read_mode);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, params_sizeof);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, poll_descriptors);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, poll_descriptors_count);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, poll_descriptors_revents);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, read);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, status);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, status_get_avail);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, status_sizeof);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, tread);
LIBREMIDI_SYMBOL_DEF(snd_rawmidi, write);
} rawmidi{library};
#endif
struct seq_t
{
explicit seq_t(const dylib_loader& library)
#if LIBREMIDI_ALSA_HAS_UMP
: ump{library}
#endif
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_seq, alloc_queue);
LIBREMIDI_SYMBOL_INIT(snd_seq, client_id);
LIBREMIDI_SYMBOL_INIT(snd_seq, client_info_get_client);
LIBREMIDI_SYMBOL_INIT(snd_seq, client_info_get_name);
LIBREMIDI_SYMBOL_INIT(snd_seq, client_info_set_client);
LIBREMIDI_SYMBOL_INIT(snd_seq, client_info_sizeof);
LIBREMIDI_SYMBOL_INIT(snd_seq, close);
LIBREMIDI_SYMBOL_INIT(snd_seq, connect_from);
LIBREMIDI_SYMBOL_INIT(snd_seq, control_queue);
LIBREMIDI_SYMBOL_INIT(snd_seq, create_port);
LIBREMIDI_SYMBOL_INIT(snd_seq, delete_port);
LIBREMIDI_SYMBOL_INIT(snd_seq, drain_output);
LIBREMIDI_SYMBOL_INIT(snd_seq, event_input);
LIBREMIDI_SYMBOL_INIT(snd_seq, event_input_pending);
LIBREMIDI_SYMBOL_INIT(snd_seq, event_output);
LIBREMIDI_SYMBOL_INIT(snd_seq, free_event);
LIBREMIDI_SYMBOL_INIT(snd_seq, free_queue);
LIBREMIDI_SYMBOL_INIT(snd_seq, get_any_client_info);
LIBREMIDI_SYMBOL_INIT(snd_seq, get_any_port_info);
LIBREMIDI_SYMBOL_INIT(snd_seq, get_port_info);
LIBREMIDI_SYMBOL_INIT(snd_seq, open);
LIBREMIDI_SYMBOL_INIT(snd_seq, poll_descriptors);
LIBREMIDI_SYMBOL_INIT(snd_seq, poll_descriptors_count);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_get_addr);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_get_capability);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_get_name);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_get_port);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_get_type);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_capability);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_client);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_midi_channels);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_name);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_port);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_timestamping);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_timestamp_queue);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_timestamp_real);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_set_type);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_info_sizeof);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_subscribe_free);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_subscribe_malloc);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_subscribe_set_dest);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_subscribe_set_sender);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_subscribe_set_time_real);
LIBREMIDI_SYMBOL_INIT(snd_seq, port_subscribe_set_time_update);
LIBREMIDI_SYMBOL_INIT(snd_seq, query_next_client);
LIBREMIDI_SYMBOL_INIT(snd_seq, query_next_port);
LIBREMIDI_SYMBOL_INIT(snd_seq, queue_tempo_set_ppq);
LIBREMIDI_SYMBOL_INIT(snd_seq, queue_tempo_set_tempo);
LIBREMIDI_SYMBOL_INIT(snd_seq, queue_tempo_sizeof);
LIBREMIDI_SYMBOL_INIT(snd_seq, set_client_name);
LIBREMIDI_SYMBOL_INIT(snd_seq, set_port_info);
LIBREMIDI_SYMBOL_INIT(snd_seq, set_queue_tempo);
LIBREMIDI_SYMBOL_INIT(snd_seq, subscribe_port);
LIBREMIDI_SYMBOL_INIT(snd_seq, unsubscribe_port);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_seq, alloc_queue);
LIBREMIDI_SYMBOL_DEF(snd_seq, client_id);
LIBREMIDI_SYMBOL_DEF(snd_seq, client_info_get_client);
LIBREMIDI_SYMBOL_DEF(snd_seq, client_info_get_name);
LIBREMIDI_SYMBOL_DEF(snd_seq, client_info_set_client);
LIBREMIDI_SYMBOL_DEF(snd_seq, client_info_sizeof);
LIBREMIDI_SYMBOL_DEF(snd_seq, close);
LIBREMIDI_SYMBOL_DEF(snd_seq, connect_from);
LIBREMIDI_SYMBOL_DEF(snd_seq, control_queue);
LIBREMIDI_SYMBOL_DEF(snd_seq, create_port);
LIBREMIDI_SYMBOL_DEF(snd_seq, delete_port);
LIBREMIDI_SYMBOL_DEF(snd_seq, drain_output);
LIBREMIDI_SYMBOL_DEF(snd_seq, event_input);
LIBREMIDI_SYMBOL_DEF(snd_seq, event_input_pending);
LIBREMIDI_SYMBOL_DEF(snd_seq, event_output);
LIBREMIDI_SYMBOL_DEF(snd_seq, free_event);
LIBREMIDI_SYMBOL_DEF(snd_seq, free_queue);
LIBREMIDI_SYMBOL_DEF(snd_seq, get_any_client_info);
LIBREMIDI_SYMBOL_DEF(snd_seq, get_any_port_info);
LIBREMIDI_SYMBOL_DEF(snd_seq, get_port_info);
LIBREMIDI_SYMBOL_DEF(snd_seq, open);
LIBREMIDI_SYMBOL_DEF(snd_seq, poll_descriptors);
LIBREMIDI_SYMBOL_DEF(snd_seq, poll_descriptors_count);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_get_addr);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_get_capability);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_get_name);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_get_port);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_get_type);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_capability);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_client);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_midi_channels);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_name);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_port);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_timestamping);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_timestamp_queue);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_timestamp_real);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_set_type);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_info_sizeof);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_subscribe_free);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_subscribe_malloc);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_subscribe_set_dest);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_subscribe_set_sender);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_subscribe_set_time_real);
LIBREMIDI_SYMBOL_DEF(snd_seq, port_subscribe_set_time_update);
LIBREMIDI_SYMBOL_DEF(snd_seq, query_next_client);
LIBREMIDI_SYMBOL_DEF(snd_seq, query_next_port);
LIBREMIDI_SYMBOL_DEF(snd_seq, queue_tempo_set_ppq);
LIBREMIDI_SYMBOL_DEF(snd_seq, queue_tempo_set_tempo);
LIBREMIDI_SYMBOL_DEF(snd_seq, queue_tempo_sizeof);
LIBREMIDI_SYMBOL_DEF(snd_seq, set_client_name);
LIBREMIDI_SYMBOL_DEF(snd_seq, set_port_info);
LIBREMIDI_SYMBOL_DEF(snd_seq, set_queue_tempo);
LIBREMIDI_SYMBOL_DEF(snd_seq, subscribe_port);
LIBREMIDI_SYMBOL_DEF(snd_seq, unsubscribe_port);
#if LIBREMIDI_ALSA_HAS_UMP
struct ump_t
{
explicit ump_t(const dylib_loader& library)
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_seq, set_client_midi_version);
LIBREMIDI_SYMBOL_INIT(snd_seq_ump, event_input);
LIBREMIDI_SYMBOL_INIT(snd_seq_ump, event_output);
LIBREMIDI_SYMBOL_INIT(snd_seq_ump, event_output_direct);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_seq, set_client_midi_version);
LIBREMIDI_SYMBOL_DEF(snd_seq_ump, event_input);
LIBREMIDI_SYMBOL_DEF(snd_seq_ump, event_output);
LIBREMIDI_SYMBOL_DEF(snd_seq_ump, event_output_direct);
} ump;
#endif
} seq{library};
#if LIBREMIDI_ALSA_HAS_UMP
struct ump_t
{
explicit ump_t(const dylib_loader& library)
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(snd_ump, block_info_get_name);
LIBREMIDI_SYMBOL_INIT(snd_ump, block_info_sizeof);
LIBREMIDI_SYMBOL_INIT(snd_ump, close);
LIBREMIDI_SYMBOL_INIT(snd_ump, endpoint_info_get_name);
LIBREMIDI_SYMBOL_INIT(snd_ump, endpoint_info_sizeof);
LIBREMIDI_SYMBOL_INIT(snd_ump, open);
LIBREMIDI_SYMBOL_INIT(snd_ump, poll_descriptors);
LIBREMIDI_SYMBOL_INIT(snd_ump, poll_descriptors_count);
LIBREMIDI_SYMBOL_INIT(snd_ump, poll_descriptors_revents);
LIBREMIDI_SYMBOL_INIT(snd_ump, rawmidi);
LIBREMIDI_SYMBOL_INIT(snd_ump, rawmidi_params);
LIBREMIDI_SYMBOL_INIT(snd_ump, rawmidi_params_current);
LIBREMIDI_SYMBOL_INIT(snd_ump, read);
LIBREMIDI_SYMBOL_INIT(snd_ump, tread);
LIBREMIDI_SYMBOL_INIT(snd_ump, write);
}
bool available{true};
LIBREMIDI_SYMBOL_DEF(snd_ump, block_info_get_name);
LIBREMIDI_SYMBOL_DEF(snd_ump, block_info_sizeof);
LIBREMIDI_SYMBOL_DEF(snd_ump, close);
LIBREMIDI_SYMBOL_DEF(snd_ump, endpoint_info_get_name);
LIBREMIDI_SYMBOL_DEF(snd_ump, endpoint_info_sizeof);
LIBREMIDI_SYMBOL_DEF(snd_ump, open);
LIBREMIDI_SYMBOL_DEF(snd_ump, poll_descriptors);
LIBREMIDI_SYMBOL_DEF(snd_ump, poll_descriptors_count);
LIBREMIDI_SYMBOL_DEF(snd_ump, poll_descriptors_revents);
LIBREMIDI_SYMBOL_DEF(snd_ump, rawmidi);
LIBREMIDI_SYMBOL_DEF(snd_ump, rawmidi_params);
LIBREMIDI_SYMBOL_DEF(snd_ump, rawmidi_params_current);
LIBREMIDI_SYMBOL_DEF(snd_ump, read);
LIBREMIDI_SYMBOL_DEF(snd_ump, tread);
LIBREMIDI_SYMBOL_DEF(snd_ump, write);
} ump{library};
#endif
};
#undef snd_dylib_alloca
#define snd_dylib_alloca(ptr, access, type) \
{ \
*ptr = (snd_##access##_##type##_t*)alloca(snd.access.type##_sizeof()); \
memset(*ptr, 0, snd.access.type##_sizeof()); \
}
#define snd_dylib_alloca2(ptr, access1, access2, type) \
{ \
*ptr = (snd_##access1##_access2##_##type##_t*)alloca(snd.access1.access2.type##_sizeof()); \
memset(*ptr, 0, snd.access1.access2.type##_sizeof()); \
}
#undef snd_rawmidi_info_alloca
#define snd_rawmidi_info_alloca(ptr) snd_dylib_alloca(ptr, rawmidi, info)
#undef snd_rawmidi_params_alloca
#define snd_rawmidi_params_alloca(ptr) snd_dylib_alloca(ptr, rawmidi, params)
#undef snd_rawmidi_status_alloca
#define snd_rawmidi_status_alloca(ptr) snd_dylib_alloca(ptr, rawmidi, status)
#undef snd_seq_client_info_alloca
#define snd_seq_client_info_alloca(ptr) snd_dylib_alloca(ptr, seq, client_info)
#undef snd_seq_port_info_alloca
#define snd_seq_port_info_alloca(ptr) snd_dylib_alloca(ptr, seq, port_info)
#undef snd_seq_queue_tempo_alloca
#define snd_seq_queue_tempo_alloca(ptr) snd_dylib_alloca(ptr, seq, queue_tempo)
#if LIBREMIDI_ALSA_HAS_UMP
#undef snd_ump_block_info_alloca
#define snd_ump_block_info_alloca(ptr) snd_dylib_alloca(ptr, ump, block_info)
#undef snd_ump_endpoint_info_alloca
#define snd_ump_endpoint_info_alloca(ptr) snd_dylib_alloca(ptr, ump, endpoint_info)
#endif
}
@@ -0,0 +1,85 @@
#pragma once
#if __has_include(<dlfcn.h>)
#include <dlfcn.h>
#include <cassert>
namespace libremidi
{
class dylib_loader
{
public:
explicit dylib_loader(const char* const so)
{
impl = dlopen(so, RTLD_LAZY | RTLD_LOCAL | RTLD_NODELETE);
}
dylib_loader(const dylib_loader&) noexcept = delete;
dylib_loader& operator=(const dylib_loader&) noexcept = delete;
dylib_loader(dylib_loader&& other) noexcept
{
impl = other.impl;
other.impl = nullptr;
}
dylib_loader& operator=(dylib_loader&& other) noexcept
{
impl = other.impl;
other.impl = nullptr;
return *this;
}
~dylib_loader()
{
if (impl)
{
dlclose(impl);
}
}
template <typename T>
T symbol(const char* const sym) const noexcept
{
assert(impl);
return reinterpret_cast<T>(dlsym(impl, sym));
}
operator bool() const noexcept { return bool(impl); }
private:
void* impl{};
};
}
#define LIBREMIDI_SYMBOL_NAME_S(prefix, name) #prefix "_" #name
#define LIBREMIDI_SYMBOL_NAME(prefix, name) prefix##_##name
#define LIBREMIDI_SYMBOL_DEF(prefix, name) \
decltype(&::LIBREMIDI_SYMBOL_NAME(prefix, name)) name{};
#define LIBREMIDI_SYMBOL_INIT(prefix, name) \
{ \
name = library.symbol<decltype(&::LIBREMIDI_SYMBOL_NAME(prefix, name))>( \
LIBREMIDI_SYMBOL_NAME_S(prefix, name)); \
if (!name) \
{ \
available = false; \
return; \
} \
}
// Because some libs have names that are C++ keywords, e.g. udev_new:
#define LIBREMIDI_SYMBOL_NAME2_S(prefix, name, varname) #prefix "_" #name
#define LIBREMIDI_SYMBOL_NAME2(prefix, name, varname) prefix##_##name
#define LIBREMIDI_SYMBOL_DEF2(prefix, name, varname) \
decltype(&::LIBREMIDI_SYMBOL_NAME2(prefix, name, varname)) varname{};
#define LIBREMIDI_SYMBOL_INIT2(prefix, name, varname) \
{ \
varname = library.symbol<decltype(&::LIBREMIDI_SYMBOL_NAME2(prefix, name, varname))>( \
LIBREMIDI_SYMBOL_NAME2_S(prefix, name, varname)); \
if (!varname) \
{ \
available = false; \
return; \
} \
}
#endif
@@ -0,0 +1,77 @@
#pragma once
#include <sys/eventfd.h>
#include <sys/timerfd.h>
#include <poll.h>
#include <unistd.h>
#include <iostream>
namespace libremidi
{
struct eventfd_notifier
{
eventfd_notifier(bool semaphore = true)
{
if (semaphore)
this->fd = eventfd(0, EFD_SEMAPHORE | EFD_NONBLOCK);
else
this->fd = eventfd(0, EFD_NONBLOCK);
}
~eventfd_notifier() { close(this->fd); }
eventfd_notifier(const eventfd_notifier&) = delete;
eventfd_notifier(eventfd_notifier&&) = delete;
eventfd_notifier& operator=(const eventfd_notifier&) = delete;
eventfd_notifier& operator=(eventfd_notifier&&) = delete;
void notify() noexcept { eventfd_write(fd, 1); }
static bool ready(pollfd res) noexcept { return res.revents & POLLIN; }
eventfd_t consume() noexcept
{
eventfd_t val;
eventfd_read(fd, &val);
return val;
}
operator int() const noexcept { return fd; }
operator pollfd() const noexcept { return {.fd = fd, .events = POLLIN, .revents = 0}; }
int fd{-1};
};
struct timerfd_timer
{
timerfd_timer() { this->fd = timerfd_create(CLOCK_MONOTONIC, 0); }
~timerfd_timer() { close(this->fd); }
timerfd_timer(const timerfd_timer&) = delete;
timerfd_timer(timerfd_timer&&) = delete;
timerfd_timer& operator=(const timerfd_timer&) = delete;
timerfd_timer& operator=(timerfd_timer&&) = delete;
void oneshot(int64_t nsec)
{
itimerspec t{};
t.it_value.tv_nsec = nsec;
timerfd_settime(this->fd, 0, &t, nullptr);
}
void restart(int64_t nsec)
{
itimerspec t{};
t.it_value.tv_nsec = nsec;
t.it_interval.tv_nsec = nsec;
timerfd_settime(this->fd, 0, &t, nullptr);
}
void cancel()
{
itimerspec t{};
timerfd_settime(this->fd, 0, &t, nullptr);
}
operator int() const noexcept { return fd; }
operator pollfd() const noexcept { return {.fd = fd, .events = POLLIN, .revents = 0}; }
int fd{-1};
};
}
@@ -0,0 +1,169 @@
#pragma once
#include <libremidi/backends/linux/dylib_loader.hpp>
#include <pipewire/pipewire.h>
namespace libremidi
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
class libpipewire
{
public:
decltype(&::pw_init) init{};
decltype(&::pw_deinit) deinit{};
decltype(&::pw_context_new) context_new{};
decltype(&::pw_context_connect) context_connect{};
decltype(&::pw_context_destroy) context_destroy{};
decltype(&::pw_core_disconnect) core_disconnect{};
decltype(&::pw_proxy_add_listener) proxy_add_listener{};
decltype(&::pw_proxy_destroy) proxy_destroy{};
decltype(&::pw_main_loop_new) main_loop_new{};
decltype(&::pw_main_loop_destroy) main_loop_destroy{};
decltype(&::pw_main_loop_quit) main_loop_quit{};
decltype(&::pw_main_loop_run) main_loop_run{};
decltype(&::pw_main_loop_get_loop) main_loop_get_loop{};
/*
decltype(&::pw_thread_loop_new) thread_loop_new{};
decltype(&::pw_thread_loop_destroy) thread_loop_destroy{};
decltype(&::pw_thread_loop_wait) thread_loop_wait{};
decltype(&::pw_thread_loop_start) thread_loop_start{};
decltype(&::pw_thread_loop_stop) thread_loop_stop;
decltype(&::pw_thread_loop_lock) thread_loop_lock{};
decltype(&::pw_thread_loop_unlock) thread_loop_unlock;
decltype(&::pw_thread_loop_get_loop) thread_loop_get_loop{};
*/
decltype(&::pw_properties_new) properties_new{};
decltype(&::pw_properties_free) properties_free{};
decltype(&::pw_properties_get) properties_get{};
decltype(&::pw_filter_new_simple) filter_new_simple{};
decltype(&::pw_filter_get_node_id) filter_get_node_id{};
decltype(&::pw_filter_get_properties) filter_get_properties{};
decltype(&::pw_filter_add_port) filter_add_port{};
decltype(&::pw_filter_remove_port) filter_remove_port{};
decltype(&::pw_filter_update_properties) filter_update_properties{};
decltype(&::pw_filter_update_params) filter_update_params{};
decltype(&::pw_filter_get_time) filter_get_time{};
decltype(&::pw_filter_destroy) filter_destroy{};
decltype(&::pw_filter_connect) filter_connect{};
decltype(&::pw_filter_get_dsp_buffer) filter_get_dsp_buffer{};
decltype(&::pw_filter_queue_buffer) filter_queue_buffer{};
decltype(&::pw_filter_dequeue_buffer) filter_dequeue_buffer{};
decltype(&::pw_filter_flush) filter_flush{};
static const libpipewire& instance()
{
static const libpipewire self;
return self;
}
bool available{true};
private:
dylib_loader library;
libpipewire()
: library("libpipewire-0.3.so.0")
{
if (!library)
{
available = false;
return;
}
// in terms of regex:
// decltype\(&::([a-z_]+)\) [a-z_]+{};
// \1 = library.symbol<decltype(&::\1)>("\1");
init = library.symbol<decltype(&::pw_init)>("pw_init");
deinit = library.symbol<decltype(&::pw_deinit)>("pw_deinit");
context_new = library.symbol<decltype(&::pw_context_new)>("pw_context_new");
context_connect = library.symbol<decltype(&::pw_context_connect)>("pw_context_connect");
context_destroy = library.symbol<decltype(&::pw_context_destroy)>("pw_context_destroy");
core_disconnect = library.symbol<decltype(&::pw_core_disconnect)>("pw_core_disconnect");
proxy_add_listener
= library.symbol<decltype(&::pw_proxy_add_listener)>("pw_proxy_add_listener");
proxy_destroy = library.symbol<decltype(&::pw_proxy_destroy)>("pw_proxy_destroy");
main_loop_new = library.symbol<decltype(&::pw_main_loop_new)>("pw_main_loop_new");
main_loop_destroy = library.symbol<decltype(&::pw_main_loop_destroy)>("pw_main_loop_destroy");
main_loop_quit = library.symbol<decltype(&::pw_main_loop_quit)>("pw_main_loop_quit");
main_loop_run = library.symbol<decltype(&::pw_main_loop_run)>("pw_main_loop_run");
main_loop_get_loop
= library.symbol<decltype(&::pw_main_loop_get_loop)>("pw_main_loop_get_loop");
properties_new = library.symbol<decltype(&::pw_properties_new)>("pw_properties_new");
properties_free = library.symbol<decltype(&::pw_properties_free)>("pw_properties_free");
properties_get = library.symbol<decltype(&::pw_properties_get)>("pw_properties_get");
filter_new_simple = library.symbol<decltype(&::pw_filter_new_simple)>("pw_filter_new_simple");
filter_get_node_id
= library.symbol<decltype(&::pw_filter_get_node_id)>("pw_filter_get_node_id");
filter_get_properties
= library.symbol<decltype(&::pw_filter_get_properties)>("pw_filter_get_properties");
filter_add_port = library.symbol<decltype(&::pw_filter_add_port)>("pw_filter_add_port");
filter_remove_port
= library.symbol<decltype(&::pw_filter_remove_port)>("pw_filter_remove_port");
filter_update_properties
= library.symbol<decltype(&::pw_filter_update_properties)>("pw_filter_update_properties");
filter_update_params
= library.symbol<decltype(&::pw_filter_update_params)>("pw_filter_update_params");
filter_get_time = library.symbol<decltype(&::pw_filter_get_time)>("pw_filter_get_time");
filter_destroy = library.symbol<decltype(&::pw_filter_destroy)>("pw_filter_destroy");
filter_connect = library.symbol<decltype(&::pw_filter_connect)>("pw_filter_connect");
filter_get_dsp_buffer
= library.symbol<decltype(&::pw_filter_get_dsp_buffer)>("pw_filter_get_dsp_buffer");
filter_dequeue_buffer
= library.symbol<decltype(&::pw_filter_dequeue_buffer)>("pw_filter_dequeue_buffer");
filter_queue_buffer
= library.symbol<decltype(&::pw_filter_queue_buffer)>("pw_filter_queue_buffer");
filter_flush = library.symbol<decltype(&::pw_filter_flush)>("pw_filter_flush");
assert(init);
assert(deinit);
assert(context_new);
assert(context_connect);
assert(context_destroy);
assert(core_disconnect);
assert(proxy_destroy);
assert(main_loop_new);
assert(main_loop_destroy);
assert(main_loop_quit);
assert(main_loop_run);
assert(main_loop_get_loop);
assert(properties_new);
assert(properties_free);
assert(properties_get);
assert(filter_new_simple);
assert(filter_get_node_id);
assert(filter_get_properties);
assert(filter_add_port);
assert(filter_remove_port);
assert(filter_update_properties);
assert(filter_update_params);
assert(filter_get_time);
assert(filter_destroy);
assert(filter_connect);
assert(filter_get_dsp_buffer);
assert(filter_dequeue_buffer);
assert(filter_queue_buffer);
assert(filter_flush);
}
};
#pragma GCC diagnostic pop
}
+92
View File
@@ -0,0 +1,92 @@
#pragma once
#include <libremidi/backends/linux/dylib_loader.hpp>
#include <libremidi/backends/linux/helpers.hpp>
#include <libudev.h>
#include <cassert>
namespace libremidi
{
struct libudev
{
// Useful one-liner:
// nm -A * | grep ' udev_' | grep -v '@' | cut -f 2 -d 'U' | sort | uniq | sed 's/ udev_//'| awk ' { print "LIBREMIDI_SYMBOL_DEF(udev, "$1");" }'
explicit libudev()
: library{"libudev.so.1"}
{
if (!library)
{
available = false;
return;
}
LIBREMIDI_SYMBOL_INIT(udev, device_get_action);
LIBREMIDI_SYMBOL_INIT(udev, device_get_subsystem);
LIBREMIDI_SYMBOL_INIT(udev, device_unref);
LIBREMIDI_SYMBOL_INIT(udev, monitor_enable_receiving);
LIBREMIDI_SYMBOL_INIT(udev, monitor_get_fd);
LIBREMIDI_SYMBOL_INIT(udev, monitor_new_from_netlink);
LIBREMIDI_SYMBOL_INIT(udev, monitor_receive_device);
LIBREMIDI_SYMBOL_INIT(udev, monitor_unref);
LIBREMIDI_SYMBOL_INIT2(udev, new, create);
LIBREMIDI_SYMBOL_INIT(udev, unref);
}
static const libudev& instance()
{
static const libudev self;
return self;
}
dylib_loader library;
bool available{true};
LIBREMIDI_SYMBOL_DEF(udev, device_get_action);
LIBREMIDI_SYMBOL_DEF(udev, device_get_subsystem);
LIBREMIDI_SYMBOL_DEF(udev, device_unref);
LIBREMIDI_SYMBOL_DEF(udev, monitor_enable_receiving);
LIBREMIDI_SYMBOL_DEF(udev, monitor_get_fd);
LIBREMIDI_SYMBOL_DEF(udev, monitor_new_from_netlink);
LIBREMIDI_SYMBOL_DEF(udev, monitor_receive_device);
LIBREMIDI_SYMBOL_DEF(udev, monitor_unref);
LIBREMIDI_SYMBOL_DEF2(udev, new, create);
LIBREMIDI_SYMBOL_DEF(udev, unref);
};
struct udev_helper
{
udev_helper()
{
instance = udev.create();
assert(instance);
monitor = udev.monitor_new_from_netlink(instance, "udev");
assert(monitor);
udev.monitor_enable_receiving(monitor);
}
~udev_helper()
{
udev.monitor_unref(monitor);
udev.unref(instance);
}
udev_helper(const udev_helper&) = delete;
udev_helper(udev_helper&&) = delete;
udev_helper& operator=(const udev_helper&) = delete;
udev_helper& operator=(udev_helper&&) = delete;
operator pollfd() const noexcept
{
return {.fd = udev.monitor_get_fd(monitor), .events = POLLIN, .revents = 0};
}
const libudev& udev = libudev::instance();
struct udev* instance{};
udev_monitor* monitor{};
};
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <libremidi/backends/dummy.hpp>
#include <libremidi/backends/linux/pipewire.hpp>
#include <libremidi/backends/pipewire/config.hpp>
#include <libremidi/backends/pipewire/helpers.hpp>
#include <libremidi/backends/pipewire/midi_in.hpp>
#include <libremidi/backends/pipewire/midi_out.hpp>
#include <libremidi/backends/pipewire/observer.hpp>
namespace libremidi::pipewire
{
struct backend
{
using midi_in = midi_in_pipewire;
using midi_out = midi_out_pipewire;
using midi_observer = observer_pipewire;
using midi_in_configuration = pipewire_input_configuration;
using midi_out_configuration = pipewire_output_configuration;
using midi_observer_configuration = pipewire_observer_configuration;
static const constexpr auto API = libremidi::API::PIPEWIRE;
static const constexpr auto name = "pipewire";
static const constexpr auto display_name = "PipeWire";
static inline bool available() noexcept
{
static const libpipewire& pw = libpipewire::instance();
return pw.available;
}
};
}
@@ -0,0 +1,53 @@
#pragma once
#include <libremidi/config.hpp>
#include <cinttypes>
#include <cstdint>
#include <functional>
#include <string>
extern "C" {
struct pw_main_loop;
struct pw_filter;
struct spa_io_position;
}
namespace libremidi
{
using pipewire_callback_function = std::function<void(spa_io_position*)>;
struct pipewire_callback
{
int64_t token;
pipewire_callback_function callback;
};
struct pipewire_input_configuration
{
std::string client_name = "libremidi client";
pw_main_loop* context{};
pw_filter* filter{};
std::function<void(pipewire_callback)> set_process_func;
std::function<void(int64_t)> clear_process_func;
};
struct pipewire_output_configuration
{
std::string client_name = "libremidi client";
pw_main_loop* context{};
pw_filter* filter{};
std::function<void(pipewire_callback)> set_process_func;
std::function<void(int64_t)> clear_process_func;
int64_t output_buffer_size{65536};
};
struct pipewire_observer_configuration
{
std::string client_name = "libremidi client";
pw_main_loop* context{};
};
}
@@ -0,0 +1,600 @@
#pragma once
#include <libremidi/backends/linux/pipewire.hpp>
#include <libremidi/detail/memory.hpp>
#include <pipewire/filter.h>
#include <pipewire/pipewire.h>
#include <spa/control/control.h>
#include <spa/param/props.h>
#include <spa/utils/defs.h>
#include <spa/utils/result.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include <spa/param/audio/format-utils.h>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
namespace libremidi
{
template <typename K, typename V>
using hash_map = std::unordered_map<K, V>;
struct pipewire_instance
{
const libpipewire& pw = libpipewire::instance();
pipewire_instance()
{
/// Initialize the PipeWire main loop, context, etc.
int argc = 0;
char* argv[] = {NULL};
char** aa = argv;
pw.init(&argc, &aa);
}
~pipewire_instance() { pw.deinit(); }
};
struct pipewire_context
{
struct listened_port
{
uint32_t id{};
pw_port* port{};
std::unique_ptr<spa_hook> listener;
};
struct port_info
{
uint32_t id{};
std::string format;
std::string port_name;
std::string port_alias;
std::string object_path;
std::string node_id;
std::string port_id;
bool physical{};
bool terminal{};
bool monitor{};
pw_direction direction{};
};
struct node
{
std::vector<port_info> inputs;
std::vector<port_info> outputs;
};
struct graph
{
mutable std::mutex mtx;
libremidi::hash_map<uint32_t, node> physical_audio;
libremidi::hash_map<uint32_t, node> physical_midi;
libremidi::hash_map<uint32_t, node> software_audio;
libremidi::hash_map<uint32_t, node> software_midi;
libremidi::hash_map<uint32_t, port_info> port_cache;
void for_each_port(auto func)
{
for (auto& map : {physical_audio, physical_midi, software_audio, software_midi})
{
for (auto& [id, node] : map)
{
for (auto& port : node.inputs)
func(port);
for (auto& port : node.outputs)
func(port);
}
}
}
void remove_port(uint32_t id)
{
port_cache.erase(id);
for (auto map : {&physical_audio, &physical_midi, &software_audio, &software_midi})
{
for (auto& [_, node] : *map)
{
std::erase_if(node.inputs, [id](const port_info& p) { return p.id == id; });
std::erase_if(node.outputs, [id](const port_info& p) { return p.id == id; });
}
}
}
} current_graph;
explicit pipewire_context(pw_main_loop* inst)
: main_loop{inst}
, owns_main_loop{false}
{
assert(main_loop);
initialize();
}
explicit pipewire_context(std::shared_ptr<pipewire_instance> inst)
: global_instance{inst}
, owns_main_loop{true}
{
this->main_loop = pw.main_loop_new(nullptr);
if (!this->main_loop)
{
// libremidi::logger().error("PipeWire: main_loop_new failed!");
return;
}
initialize();
}
void initialize()
{
this->lp = pw.main_loop_get_loop(this->main_loop);
if (!lp)
{
// libremidi::logger().error("PipeWire: main_loop_get_loop failed!");
return;
}
this->context = pw.context_new(lp, nullptr, 0);
if (!this->context)
{
// libremidi::logger().error("PipeWire: context_new failed!");
return;
}
this->core = pw.context_connect(this->context, nullptr, 0);
if (!this->core)
{
// libremidi::logger().error("PipeWire: context_connect failed!");
return;
}
this->registry = pw_core_get_registry(this->core, PW_VERSION_REGISTRY, 0);
if (!this->registry)
{
// libremidi::logger().error("PipeWire: core_get_registry failed!");
return;
}
initialize_observation();
synchronize();
// Add a manual 1ms event loop iteration at the end of
// ctor to ensure synchronous clients will still see the ports
pw_loop_iterate(this->lp, 1);
}
void initialize_observation()
{
// Register a listener which will listen on when ports are added / removed
spa_zero(registry_listener);
static constexpr const struct pw_registry_events registry_events = {
.version = PW_VERSION_REGISTRY_EVENTS,
.global =
[](void* object, uint32_t id, uint32_t /*permissions*/, const char* type,
uint32_t /*version*/, const struct spa_dict* /*props*/) {
pipewire_context& self = *(pipewire_context*)object;
if (strcmp(type, PW_TYPE_INTERFACE_Port) == 0)
self.register_port(id, type);
},
.global_remove =
[](void* object, uint32_t id) {
pipewire_context& self = *(pipewire_context*)object;
self.unregister_port(id);
},
};
// Start listening
pw_registry_add_listener(this->registry, &this->registry_listener, &registry_events, this);
}
void register_port(uint32_t id, const char* type)
{
auto port = (pw_port*)pw_registry_bind(registry, id, type, PW_VERSION_PORT, 0);
port_listener.push_back({id, port, std::make_unique<spa_hook>()});
auto& l = port_listener.back();
static constexpr const struct pw_port_events port_events = {
.version = PW_VERSION_PORT_EVENTS,
.info
= [](void* object,
const pw_port_info* info) { ((pipewire_context*)object)->update_port_info(info); },
};
pw_port_add_listener(l.port, l.listener.get(), &port_events, this);
}
void unregister_port(uint32_t id)
{
// When a port is removed:
// Notify
std::unique_lock _{current_graph.mtx, std::defer_lock};
if (on_port_removed)
{
_.lock();
if (auto it = current_graph.port_cache.find(id); it != current_graph.port_cache.end())
{
auto copy = it->second;
_.unlock();
on_port_removed(copy);
}
else
{
_.unlock();
}
}
// Remove from the graph
{
_.lock();
current_graph.remove_port(id);
_.unlock();
}
// Remove from the listeners
auto it
= std::find_if(port_listener.begin(), port_listener.end(), [&](const listened_port& l) {
return l.id == id;
});
if (it != port_listener.end())
{
pw.proxy_destroy((pw_proxy*)it->port);
port_listener.erase(it);
}
}
void synchronize()
{
pending = 0;
done = 0;
if (!core)
return;
spa_hook core_listener;
static constexpr struct pw_core_events core_events = {
.version = PW_VERSION_CORE_EVENTS,
.done =
[](void* object, uint32_t id, int seq) {
auto& self = *(pipewire_context*)object;
if(id == PW_ID_CORE && seq == self.pending)
{
self.done = 1;
libpipewire::instance().main_loop_quit(self.main_loop);
}
},
};
spa_zero(core_listener);
pw_core_add_listener(core, &core_listener, &core_events, this);
pending = pw_core_sync(core, PW_ID_CORE, 0);
while (!done)
{
pw.main_loop_run(this->main_loop);
}
spa_hook_remove(&core_listener);
}
[[nodiscard]] pw_proxy* link_ports(uint32_t out_port, uint32_t in_port)
{
auto props = pw.properties_new(
PW_KEY_LINK_OUTPUT_PORT, std::to_string(out_port).c_str(), PW_KEY_LINK_INPUT_PORT,
std::to_string(in_port).c_str(), nullptr);
auto proxy = (pw_proxy*)pw_core_create_object(
this->core, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0);
if (!proxy)
{
std::cerr << "PipeWire: could not allocate link\n";
pw.properties_free(props);
return nullptr;
}
synchronize();
pw.properties_free(props);
return proxy;
}
void unlink_ports(pw_proxy* link) { pw.proxy_destroy(link); }
void update_port_info(const pw_port_info* info)
{
const spa_dict_item* item{};
port_info p;
p.id = info->id;
spa_dict_for_each(item, info->props)
{
std::string_view k{item->key}, v{item->value};
if (k == "format.dsp")
p.format = v;
else if (k == "port.name")
p.port_name = v;
else if (k == "port.alias")
p.port_alias = v;
else if (k == "object.path")
p.object_path = v;
else if (k == "port.id")
p.port_id = v;
else if (k == "node.id")
p.node_id = v;
else if (k == "port.physical" && v == "true")
p.physical = true;
else if (k == "port.terminal" && v == "true")
p.terminal = true;
else if (k == "port.monitor" && v == "true")
p.monitor = true;
else if (k == "port.direction")
{
if (v == "out")
{
p.direction = pw_direction::SPA_DIRECTION_OUTPUT;
}
else
{
p.direction = pw_direction::SPA_DIRECTION_INPUT;
}
}
}
if (p.node_id.empty())
return;
const auto nid = std::stoul(p.node_id);
auto get_node = [&]() -> node* {
if (p.physical)
{
if (p.format.find("audio") != p.format.npos)
return &this->current_graph.physical_audio[nid];
else if (p.format.find("midi") != p.format.npos)
return &this->current_graph.physical_midi[nid];
}
else
{
if (p.format.find("audio") != p.format.npos)
return &this->current_graph.software_audio[nid];
else if (p.format.find("midi") != p.format.npos)
return &this->current_graph.software_midi[nid];
}
return nullptr;
};
{
std::lock_guard _{current_graph.mtx};
current_graph.port_cache[p.id] = p;
if (auto node = get_node())
{
if (p.direction == pw_direction::SPA_DIRECTION_OUTPUT)
node->outputs.push_back(p);
else
node->inputs.push_back(p);
}
}
if (on_port_added)
on_port_added(p);
}
int get_fd() const noexcept
{
if (!this->lp)
return -1;
auto spa_callbacks = this->lp->control->iface.cb;
auto spa_loop_methods = (const spa_loop_control_methods*)spa_callbacks.funcs;
if (spa_loop_methods->get_fd)
return spa_loop_methods->get_fd(spa_callbacks.data);
else
return -1;
}
~pipewire_context()
{
if (this->registry)
pw.proxy_destroy((pw_proxy*)this->registry);
for (auto& [id, p, l] : this->port_listener)
if (l)
pw.proxy_destroy((pw_proxy*)p);
if (this->core)
pw.core_disconnect(this->core);
if (this->context)
pw.context_destroy(this->context);
if (owns_main_loop && this->main_loop)
pw.main_loop_destroy(this->main_loop);
}
friend struct pipewire_filter;
const libpipewire& pw = libpipewire::instance();
std::shared_ptr<pipewire_instance> global_instance;
pw_main_loop* main_loop{};
pw_loop* lp{};
pw_context* context{};
pw_core* core{};
pw_registry* registry{};
spa_hook registry_listener{};
std::function<void(const port_info&)> on_port_added;
std::function<void(const port_info&)> on_port_removed;
std::vector<listened_port> port_listener{};
std::atomic<int> pending{};
std::atomic<int> done{};
bool owns_main_loop{true};
int sync{};
};
struct pipewire_filter
{
const libpipewire& pw = libpipewire::instance();
std::shared_ptr<pipewire_context> loop{};
pw_filter* filter{};
std::vector<pw_proxy*> links{};
struct port
{
void* data;
}* port{};
explicit pipewire_filter(std::shared_ptr<pipewire_context> loop)
: loop{loop}
{
}
explicit pipewire_filter(std::shared_ptr<pipewire_context> loop, pw_filter* filter)
: loop{loop}
, filter{filter}
{
}
void create_filter(std::string_view filter_name, const pw_filter_events& events, void* context)
{
assert(!filter);
auto& pw = libpipewire::instance();
// clang-format off
this->filter = pw.filter_new_simple(
loop->lp,
filter_name.data(),
pw.properties_new(
PW_KEY_MEDIA_TYPE, "Midi",
PW_KEY_MEDIA_CATEGORY, "Filter",
PW_KEY_MEDIA_ROLE, "DSP",
PW_KEY_MEDIA_NAME, "libremidi",
#if defined(PW_KEY_NODE_LOCK_RATE)
PW_KEY_NODE_LOCK_RATE, "true",
#endif
PW_KEY_NODE_ALWAYS_PROCESS, "true",
PW_KEY_NODE_PAUSE_ON_IDLE, "false",
#if defined(PW_KEY_NODE_SUSPEND_ON_IDLE)
PW_KEY_NODE_SUSPEND_ON_IDLE, "false",
#endif
nullptr),
&events,
context);
// clang-format on
assert(filter);
}
void destroy()
{
if (this->filter)
pw.filter_destroy(this->filter);
}
void create_local_port(std::string_view port_name, spa_direction direction)
{
// clang-format off
this->port = (struct port*)pw.filter_add_port(
this->filter,
direction,
PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(struct port),
pw.properties_new(
PW_KEY_FORMAT_DSP, "8 bit raw midi",
PW_KEY_PORT_NAME, port_name.data(),
nullptr),
nullptr, 0);
// clang-format on
assert(port);
}
void set_port_buffer(int bytes)
{
uint8_t buffer[1024];
struct spa_pod_builder builder;
spa_pod_builder_init(&builder, buffer, sizeof(buffer));
// clang-format off
const struct spa_pod* params[1] = {
(spa_pod*) spa_pod_builder_add_object(
&builder,
SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
SPA_PARAM_BUFFERS_buffers, SPA_POD_CHOICE_RANGE_Int(1, 1, 32),
SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
SPA_PARAM_BUFFERS_size, SPA_POD_CHOICE_RANGE_Int(bytes, 4096, INT32_MAX),
SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1)
)
};
// clang-format on
pw.filter_update_params(this->filter, this->port, params, 1);
}
void remove_port()
{
assert(this->port);
pw.filter_remove_port(this->port);
this->port = nullptr;
}
void rename_port(std::string_view port_name)
{
assert(this->port);
spa_dict_item items[1] = {
SPA_DICT_ITEM_INIT(PW_KEY_PORT_NAME, port_name.data()),
};
auto properties = SPA_DICT_INIT(items, 1);
pw.filter_update_properties(this->filter, this->port, &properties);
this->port = nullptr;
}
void start_filter()
{
if (pw.filter_connect(this->filter, PW_FILTER_FLAG_RT_PROCESS, NULL, 0) < 0)
{
std::cerr << "can't connect\n";
return;
}
}
uint32_t filter_node_id() { return this->loop->pw.filter_get_node_id(this->filter); }
void synchronize_node()
{
this->loop->synchronize();
int k = 0;
auto node_id = filter_node_id();
while (node_id == 4294967295)
{
this->loop->synchronize();
node_id = filter_node_id();
if (k++; k > 100)
return;
}
}
void synchronize_ports(const pipewire_context::node& this_node)
{
// Leave some time to resolve the ports
int k = 0;
const auto num_local_ins = 1;
const auto num_local_outs = 0;
while (this_node.inputs.size() < num_local_ins || this_node.outputs.size() < num_local_outs)
{
this->loop->synchronize();
if (k++; k > 100)
return;
}
}
};
}
#pragma GCC diagnostic pop
@@ -0,0 +1,464 @@
#pragma once
#include <libremidi/backends/linux/helpers.hpp>
#include <libremidi/backends/linux/pipewire.hpp>
#include <libremidi/backends/pipewire/context.hpp>
#include <libremidi/detail/memory.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/semaphore.hpp>
#include <atomic>
#include <semaphore>
#include <stop_token>
#include <thread>
namespace libremidi
{
struct pipewire_helpers
{
struct port
{
void* data{};
};
// All pipewire operations have to happen in the same thread
// - and pipewire checks that internally.
std::jthread main_loop_thread;
const libpipewire& pw = libpipewire::instance();
std::shared_ptr<pipewire_instance> global_instance;
std::shared_ptr<pipewire_context> global_context;
std::unique_ptr<pipewire_filter> filter;
pw_proxy* link{};
int64_t this_instance{};
eventfd_notifier termination_event{};
pollfd fds[2]{};
semaphore_pair_lock thread_lock;
std::shared_ptr<void> canary = std::make_shared<int>();
enum poll_state
{
start_poll,
in_poll,
not_in_poll
};
std::atomic<poll_state> current_state{not_in_poll};
pipewire_helpers()
{
static std::atomic_int64_t instance{};
this_instance = ++instance;
fds[1] = termination_event;
}
template <typename Self>
void create_filter(Self& self)
{
if (this->filter)
return;
auto& configuration = self.configuration;
if (configuration.context && configuration.filter && configuration.set_process_func)
{
this->filter = std::make_unique<pipewire_filter>(this->global_context, configuration.filter);
pipewire_callback cbs{
.token = this_instance,
.callback = [&self, p = std::weak_ptr{canary}](spa_io_position* nf) -> void {
if (auto pt = p.lock())
self.process(nf);
self.thread_lock.check_client_released();
}};
configuration.set_process_func(cbs);
}
else
{
this->filter = std::make_unique<pipewire_filter>(this->global_context);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
static constexpr struct pw_filter_events filter_events
= {.version = PW_VERSION_FILTER_EVENTS,
.process = +[](void* _data, struct spa_io_position* position) -> void {
// FIXME likely we need the thread_lock check here too
Self& self = *static_cast<Self*>(_data);
self.process(position);
}};
#pragma GCC diagnostic pop
this->filter->create_filter(self.configuration.client_name, filter_events, &self);
this->filter->start_filter();
}
}
template <typename Self>
void destroy_filter(Self& self)
{
assert(global_context);
if (!global_context->owns_main_loop)
{
if (self.configuration.clear_process_func)
{
self.configuration.clear_process_func(this_instance);
}
}
else
{
if (this->filter)
{
this->filter->destroy();
}
}
this->filter.reset();
}
template <typename Self>
int create_context(Self& self)
{
if (this->global_context)
return 0;
// Initialize PipeWire client
auto& configuration = self.configuration;
if (configuration.context)
{
this->global_context = std::make_shared<pipewire_context>(configuration.context);
}
else
{
this->global_instance = std::make_shared<pipewire_instance>();
this->global_context = std::make_shared<pipewire_context>(this->global_instance);
}
return 0;
}
void destroy_context()
{
assert(this->global_context);
this->global_context.reset();
this->global_instance.reset();
}
void run_poll_loop()
try
{
// Note: called from a std::jthread.
assert(this->global_context);
if (int fd = this->global_context->get_fd(); fd != -1)
{
fds[0] = {.fd = fd, .events = POLLIN, .revents = 0};
current_state = poll_state::in_poll;
for (;;)
{
if (int err = poll(fds, 2, -1); err < 0)
{
if (err == -EAGAIN)
continue;
else
break;
}
// Check pipewire fd:
if (fds[0].revents & POLLIN)
{
if (auto lp = this->global_context->lp)
{
int result = pw_loop_iterate(lp, 0);
if (result < 0)
std::cerr << "pw_loop_iterate: " << spa_strerror(result) << "\n";
}
fds[0].revents = 0;
}
// Check exit fd:
if (fds[1].revents & POLLIN)
{
break;
}
}
}
current_state = poll_state::not_in_poll;
}
catch (...)
{
current_state = poll_state::not_in_poll;
}
template <typename Self>
bool create_local_port(Self& self, std::string_view portName, spa_direction direction)
{
assert(this->global_context);
assert(this->filter);
if (portName.empty())
portName = direction == SPA_DIRECTION_INPUT ? "i" : "o";
if (!this->filter->port)
{
this->filter->create_local_port(portName.data(), direction);
}
if (!this->filter->port)
{
self.template error<driver_error>(self.configuration, "PipeWire: error creating port");
return false;
}
return true;
}
void add_callbacks(const observer_configuration& conf)
{
assert(global_context);
global_context->on_port_added = [&conf](const pipewire_context::port_info& port) {
if (port.format.find("midi") == std::string::npos)
return;
bool unfiltered = conf.track_any;
unfiltered |= (port.physical && conf.track_hardware);
unfiltered |= (!port.physical && conf.track_virtual);
if (unfiltered)
{
if (port.direction == SPA_DIRECTION_INPUT)
{
if (conf.output_added)
conf.output_added(to_port_info<SPA_DIRECTION_INPUT>(port));
}
else
{
if (conf.input_added)
conf.input_added(to_port_info<SPA_DIRECTION_OUTPUT>(port));
}
}
};
global_context->on_port_removed = [&conf](const pipewire_context::port_info& port) {
if (port.format.find("midi") == std::string::npos)
return;
bool unfiltered = conf.track_any;
unfiltered |= (port.physical && conf.track_hardware);
unfiltered |= (!port.physical && conf.track_virtual);
if (unfiltered)
{
if (port.direction == SPA_DIRECTION_INPUT)
{
if (conf.output_removed)
conf.output_removed(to_port_info<SPA_DIRECTION_INPUT>(port));
}
else
{
if (conf.input_removed)
conf.input_removed(to_port_info<SPA_DIRECTION_OUTPUT>(port));
}
}
};
}
void start_thread()
{
if (!this->global_context->owns_main_loop)
return;
current_state = poll_state::start_poll;
main_loop_thread = std::jthread{[this]() { run_poll_loop(); }};
}
void stop_thread()
{
assert(this->global_context);
if (!this->global_context->owns_main_loop)
return;
if (main_loop_thread.joinable() || current_state != poll_state::not_in_poll)
{
termination_event.notify();
main_loop_thread.request_stop();
termination_event.notify();
for (int i = 0; i < 100; i++)
{
if (current_state == poll_state::not_in_poll)
break;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
termination_event.notify();
}
if (main_loop_thread.joinable())
main_loop_thread.join();
}
}
void do_close_port()
{
if (!this->filter)
return;
if (!this->filter->port)
return;
if (!this->global_context->owns_main_loop)
{
this->canary.reset();
this->thread_lock.prepare_release_client();
}
unlink_ports();
this->filter->remove_port();
}
void rename_port(std::string_view port_name)
{
if (this->filter)
this->filter->rename_port(port_name);
}
void unlink_ports()
{
if (link)
{
this->global_context->unlink_ports(link);
link = nullptr;
}
}
bool link_ports(auto& self, const input_port& in_port)
{
// Wait for the pipewire server to send us back our node's info
for (int i = 0; i < 1000; i++)
this->filter->synchronize_node();
auto this_node = this->filter->filter_node_id();
auto& midi = this->global_context->current_graph.software_midi;
auto node_it = midi.find(this_node);
if (node_it == midi.end())
{
std::cerr << "Node " << this_node << " not found! \n";
return false;
}
// Wait for the pipewire server to send us back our node's ports
this->filter->synchronize_ports(node_it->second);
if (node_it->second.inputs.empty())
{
std::cerr << "Node " << this_node << " has no ports! \n";
return false;
}
// Link ports
const auto& p = node_it->second.inputs.front();
link = this->global_context->link_ports(in_port.port, p.id);
pw_loop_iterate(this->global_context->lp, 1);
if (!link)
{
self.template error<invalid_parameter_error>(
self.configuration,
"PipeWire: could not connect to port: " + in_port.port_name + " -> " + p.port_name);
return false;
}
return true;
}
bool link_ports(auto& self, const output_port& out_port)
{
// Wait for the pipewire server to send us back our node's info
for (int i = 0; i < 1000; i++)
this->filter->synchronize_node();
auto this_node = this->filter->filter_node_id();
auto& midi = this->global_context->current_graph.software_midi;
auto node_it = midi.find(this_node);
if (node_it == midi.end())
{
std::cerr << "Node " << this_node << " not found! \n";
return false;
}
// Wait for the pipewire server to send us back our node's ports
this->filter->synchronize_ports(node_it->second);
if (node_it->second.outputs.empty())
{
std::cerr << "Node " << this_node << " has no ports! \n";
return false;
}
// Link ports
const auto& p = node_it->second.outputs.front();
link = this->global_context->link_ports(p.id, out_port.port);
pw_loop_iterate(this->global_context->lp, 1);
if (!link)
{
self.template error<invalid_parameter_error>(
self.configuration,
"PipeWire: could not connect to port: " + p.port_name + " -> " + out_port.port_name);
return false;
}
return true;
}
template <spa_direction Direction>
static auto to_port_info(const pipewire_context::port_info& port)
-> std::conditional_t<Direction == SPA_DIRECTION_OUTPUT, input_port, output_port>
{
std::string device_name, port_name;
auto name_colon = port.port_alias.find(':');
if (name_colon != std::string::npos)
{
device_name = port.port_alias.substr(0, name_colon);
port_name = port.port_alias.substr(name_colon + 1);
}
else
{
port_name = port.port_alias;
}
return {{
.client = 0,
.port = port.id,
.manufacturer = "",
.device_name = device_name,
.port_name = port.port_name,
.display_name = port_name,
}};
}
// Note: keep in mind that an "input" port for us (e.g. a keyboard that goes to the computer)
// is an "output" port from the point of view of pipewire as data will come out of it
template <spa_direction Direction>
static auto get_ports(const pipewire_context& ctx) noexcept -> std::vector<
std::conditional_t<Direction == SPA_DIRECTION_OUTPUT, input_port, output_port>>
{
std::vector<std::conditional_t<Direction == SPA_DIRECTION_OUTPUT, input_port, output_port>>
ret;
{
std::lock_guard _{ctx.current_graph.mtx};
for (auto& node : ctx.current_graph.physical_midi)
{
for (auto& p :
(Direction == SPA_DIRECTION_INPUT ? node.second.inputs : node.second.outputs))
{
ret.push_back(to_port_info<Direction>(p));
}
}
for (auto& node : ctx.current_graph.software_midi)
{
for (auto& p :
(Direction == SPA_DIRECTION_INPUT ? node.second.inputs : node.second.outputs))
{
ret.push_back(to_port_info<Direction>(p));
}
}
}
return ret;
}
};
}
@@ -0,0 +1,125 @@
#pragma once
#include <libremidi/backends/pipewire/config.hpp>
#include <libremidi/backends/pipewire/helpers.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
#include <chrono>
namespace libremidi
{
class midi_in_pipewire final
: public midi1::in_api
, public pipewire_helpers
, public error_handler
{
public:
struct
: input_configuration
, pipewire_input_configuration
{
} configuration;
explicit midi_in_pipewire(input_configuration&& conf, pipewire_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
create_context(*this);
create_filter(*this);
}
~midi_in_pipewire() override
{
stop_thread();
do_close_port();
destroy_filter(*this);
destroy_context();
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_pipewire: set_client_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::PIPEWIRE; }
bool open_port(const input_port& in_port, std::string_view name) override
{
if (!create_local_port(*this, name, SPA_DIRECTION_INPUT))
return false;
if (!link_ports(*this, in_port))
return false;
start_thread();
return true;
}
bool open_virtual_port(std::string_view name) override
{
if (!create_local_port(*this, name, SPA_DIRECTION_INPUT))
return false;
start_thread();
return true;
}
void close_port() override
{
stop_thread();
do_close_port();
}
void set_port_name(std::string_view port_name) override { rename_port(port_name); }
timestamp absolute_timestamp() const noexcept override { return system_ns(); }
void process(struct spa_io_position* position)
{
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = true,
.absolute_is_monotonic = true,
.has_samples = true,
};
assert(this->filter);
assert(this->filter->port);
const auto b = pw.filter_dequeue_buffer(this->filter->port);
if (!b)
return;
const auto buf = b->buffer;
const auto d = &buf->datas[0];
if (d->data == nullptr)
return;
const auto pod
= (spa_pod*)spa_pod_from_data(d->data, d->maxsize, d->chunk->offset, d->chunk->size);
if (!pod)
return;
if (!spa_pod_is_sequence(pod))
return;
struct spa_pod_control* c{};
SPA_POD_SEQUENCE_FOREACH((struct spa_pod_sequence*)pod, c)
{
if (c->type != SPA_CONTROL_Midi)
continue;
auto data = (uint8_t*)SPA_POD_BODY(&c->value);
auto size = SPA_POD_BODY_SIZE(&c->value);
const auto to_ns = [=, clk = position->clock] {
return 1e9 * ((clk.position + c->offset) / (double)clk.rate.denom);
};
m_processing.on_bytes(
{data, data + size}, m_processing.timestamp<timestamp_info>(to_ns, c->offset));
}
pw.filter_queue_buffer(this->filter->port, b);
}
midi1::input_state_machine m_processing{this->configuration};
};
}
@@ -0,0 +1,171 @@
#pragma once
#include <libremidi/backends/pipewire/config.hpp>
#include <libremidi/backends/pipewire/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
#include <readerwriterqueue.h>
#include <semaphore>
namespace libremidi
{
class midi_out_pipewire
: public midi1::out_api
, public pipewire_helpers
, public error_handler
{
public:
struct
: output_configuration
, pipewire_output_configuration
{
} configuration;
midi_out_pipewire(output_configuration&& conf, pipewire_output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
create_context(*this);
create_filter(*this);
}
~midi_out_pipewire() override
{
stop_thread();
do_close_port();
destroy_filter(*this);
destroy_context();
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_pipewire: set_client_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::PIPEWIRE; }
bool open_port(const output_port& out_port, std::string_view name) override
{
if (!create_local_port(*this, name, SPA_DIRECTION_OUTPUT))
return false;
this->filter->set_port_buffer(configuration.output_buffer_size);
if (!link_ports(*this, out_port))
return false;
start_thread();
return true;
}
bool open_virtual_port(std::string_view name) override
{
if (!create_local_port(*this, name, SPA_DIRECTION_OUTPUT))
return false;
this->filter->set_port_buffer(configuration.output_buffer_size);
start_thread();
return true;
}
void close_port() override
{
stop_thread();
do_close_port();
}
void set_port_name(std::string_view port_name) override { rename_port(port_name); }
int process(spa_io_position* pos)
{
m_process_clock.store(pos->clock.nsec, std::memory_order_relaxed);
const auto b = pw.filter_dequeue_buffer(this->filter->port);
if (!b)
return 1;
const auto buf = b->buffer;
const auto d = &buf->datas[0];
if (d->data == nullptr)
return 1;
spa_pod_builder build;
spa_zero(build);
spa_pod_builder_init(&build, d->data, d->maxsize);
spa_pod_frame f;
spa_pod_builder_push_sequence(&build, &f, 0);
// for all events
while (auto m_ptr = m_queue.peek())
{
auto& m = *m_ptr;
if (m.empty())
{
m_queue.pop();
continue;
}
// TODO why
if (m.bytes[0] == 0xff)
{
m_queue.pop();
continue;
}
spa_pod_builder_control(&build, m.timestamp, SPA_CONTROL_Midi);
int res = spa_pod_builder_bytes(&build, m.bytes.data(), m.bytes.size());
// Try again next buffer
if (res == -ENOSPC)
break;
m_queue.pop();
}
spa_pod_builder_pop(&build, &f);
int n_fill_frames = build.state.offset;
if (n_fill_frames > 0)
{
d->chunk->offset = 0;
d->chunk->stride = 1;
d->chunk->size = n_fill_frames;
b->size = n_fill_frames;
pw.filter_queue_buffer(this->filter->port, b);
return 0;
}
pw.filter_flush(this->filter->filter, true);
return 0;
}
void send_message(const unsigned char* message, size_t size) override
{
m_queue.enqueue(libremidi::message(midi_bytes{message, message + size}, 0));
}
int convert_timestamp(int64_t user) const noexcept
{
switch (configuration.timestamps)
{
case timestamp_mode::AudioFrame:
return static_cast<int>(user);
default:
// TODO
return 0;
}
}
void schedule_message(int64_t ts, const unsigned char* message, size_t size) override
{
m_queue.enqueue(
libremidi::message(midi_bytes{message, message + size}, convert_timestamp(ts)));
}
moodycamel::ReaderWriterQueue<libremidi::message> m_queue;
std::atomic_int64_t m_process_clock = 0;
};
}
@@ -0,0 +1,74 @@
#pragma once
#include <libremidi/backends/pipewire/config.hpp>
#include <libremidi/backends/pipewire/helpers.hpp>
#include <libremidi/detail/observer.hpp>
#include <unordered_set>
namespace libremidi
{
class observer_pipewire final
: public observer_api
, private pipewire_helpers
, private error_handler
{
public:
struct
: observer_configuration
, pipewire_observer_configuration
{
} configuration;
explicit observer_pipewire(observer_configuration&& conf, pipewire_observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
create_context(*this);
// FIXME notify_in_constructor
// FIXME port rename callback
#if 0
// Initialize PipeWire client
if (configuration.context)
{
this->client = configuration.context;
set_callbacks();
}
else
#endif
{
this->add_callbacks(configuration);
this->start_thread();
}
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::PIPEWIRE; }
std::vector<libremidi::input_port> get_input_ports() const noexcept override
{
return get_ports<SPA_DIRECTION_OUTPUT>(*this->global_context);
}
std::vector<libremidi::output_port> get_output_ports() const noexcept override
{
return get_ports<SPA_DIRECTION_INPUT>(*this->global_context);
}
~observer_pipewire()
{
stop_thread();
destroy_context();
#if 0
if (client && !configuration.context)
{
// If we own the client, deactivate it
pipewire_deactivate(this->client);
pipewire_client_close(this->client);
this->client = nullptr;
}
#endif
}
std::unordered_set<std::string> seen_input_ports;
std::unordered_set<std::string> seen_output_ports;
};
}
@@ -0,0 +1,153 @@
#pragma once
#if __has_include(<boost/lockfree/spsc_queue.hpp>)
#include <libremidi/backends/pipewire/config.hpp>
#include <libremidi/backends/pipewire/helpers.hpp>
#include <libremidi/shared_context.hpp>
#include <boost/lockfree/spsc_queue.hpp>
#include <variant>
namespace libremidi::pipewire
{
// Create a PipeWire client which will be shared across objects
struct shared_handler : public libremidi::shared_context
{
explicit shared_handler(std::string_view v)
{
midiin_callbacks.reserve(64);
midiout_callbacks.reserve(64);
pipewire_status_t status{};
client = pipewire_client_open(v.data(), PipewireNoStartServer, &status);
assert(client);
assert(status == 0);
pipewire_set_process_callback(
client,
+[](pipewire_nframes_t cnt, void* ctx) -> int {
((shared_handler*)ctx)->pipewire_callback(cnt);
return 0;
},
this);
}
virtual void start_processing() override { pipewire_activate(client); }
virtual void stop_processing() override { pipewire_deactivate(client); }
static shared_configurations make(std::string_view client_name)
{
auto clt = std::make_shared<shared_handler>(client_name);
auto add_in_cb = [client = std::weak_ptr{clt}](libremidi::pipewire_callback cb) {
if (auto clt = client.lock())
clt->events.push({shared_handler::event_type::in_callback_added, std::move(cb)});
};
auto clear_in_cb = [client = std::weak_ptr{clt}](int64_t index) {
if (auto clt = client.lock())
clt->events.push({shared_handler::event_type::in_callback_removed, index});
};
auto add_out_cb = [client = std::weak_ptr{clt}](libremidi::pipewire_callback cb) {
if (auto clt = client.lock())
clt->events.push({shared_handler::event_type::out_callback_added, std::move(cb)});
};
auto clear_out_cb = [client = std::weak_ptr{clt}](int64_t index) {
if (auto clt = client.lock())
clt->events.push({shared_handler::event_type::out_callback_removed, index});
};
return {
.context = clt,
.observer = pipewire_observer_configuration{.context = clt->client},
.in
= pipewire_input_configuration{.context = clt->client, .set_process_func = add_in_cb, .clear_process_func = clear_in_cb},
.out
= pipewire_output_configuration{.context = clt->client, .set_process_func = add_out_cb, .clear_process_func = clear_out_cb},
};
}
int pipewire_callback(pipewire_nframes_t cnt)
{
// 1. Process the events that will change the callback list
event ev;
while (events.pop(ev))
{
switch (ev.type)
{
case in_callback_added:
midiin_callbacks.push_back(
std::move(*std::get_if<libremidi::pipewire_callback>(&ev.payload)));
break;
case in_callback_removed: {
auto idx = *std::get_if<int64_t>(&ev.payload);
for (auto it = midiin_callbacks.begin(); it != midiin_callbacks.end();)
{
if (it->token == idx)
{
midiin_callbacks.erase(it);
break;
}
else
{
++it;
}
}
break;
}
case out_callback_added:
midiout_callbacks.push_back(
std::move(*std::get_if<libremidi::pipewire_callback>(&ev.payload)));
break;
case out_callback_removed:
auto idx = *std::get_if<int64_t>(&ev.payload);
for (auto it = midiout_callbacks.begin(); it != midiout_callbacks.end();)
{
if (it->token == idx)
{
midiout_callbacks.erase(it);
break;
}
else
{
++it;
}
}
break;
}
}
for (auto& cb : midiin_callbacks)
cb.callback(cnt);
for (auto& cb : midiout_callbacks)
cb.callback(cnt);
return 0;
}
~shared_handler()
{
pipewire_deactivate(client);
pipewire_client_close(client);
}
pipewire_client_t* client{};
enum event_type
{
in_callback_added,
in_callback_removed,
out_callback_added,
out_callback_removed,
};
struct event
{
event_type type;
std::variant<libremidi::pipewire_callback, int64_t> payload;
};
boost::lockfree::spsc_queue<event> events{16};
std::vector<libremidi::pipewire_callback> midiin_callbacks;
std::vector<libremidi::pipewire_callback> midiout_callbacks;
};
}
#endif
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <libremidi/backends/winmidi/midi_in.hpp>
#include <libremidi/backends/winmidi/midi_out.hpp>
#include <libremidi/backends/winmidi/observer.hpp>
namespace libremidi::winmidi
{
struct backend
{
using midi_in = midi_in_impl;
using midi_out = midi_out_impl;
using midi_observer = observer_impl;
using midi_in_configuration = winmidi::input_configuration;
using midi_out_configuration = winmidi::output_configuration;
using midi_observer_configuration = winmidi::observer_configuration;
static const constexpr auto API = libremidi::API::WINDOWS_MIDI_SERVICES;
static const constexpr auto name = "winmidi";
static const constexpr auto display_name = "Windows MIDI Services";
static constexpr inline bool available() noexcept { return true; }
};
} // namespace libremidi
@@ -0,0 +1,19 @@
#pragma once
#include <libremidi/config.hpp>
namespace libremidi::winmidi
{
struct input_configuration
{
};
struct output_configuration
{
};
struct observer_configuration
{
};
}
@@ -0,0 +1,27 @@
#pragma once
// clang-format off
#define NOMINMAX 1
#define WIN32_LEAN_AND_MEAN 1
#include <libremidi/detail/midi_api.hpp>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <guiddef.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Devices.Enumeration.h>
#include <winrt/Microsoft.Devices.Midi2.h>
// clang-format on
namespace libremidi::winmidi
{
using namespace winrt;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Devices::Enumeration;
using namespace winrt::Windows::Storage::Streams;
using namespace Microsoft::Devices::Midi2;
using namespace Windows::Devices::Enumeration;
}
@@ -0,0 +1,94 @@
#pragma once
#include <libremidi/backends/winmidi/config.hpp>
#include <libremidi/backends/winmidi/helpers.hpp>
#include <libremidi/backends/winmidi/observer.hpp>
#include <libremidi/detail/midi_in.hpp>
namespace libremidi::winmidi
{
class midi_in_impl final
: public midi2::in_api
, public error_handler
{
public:
struct
: libremidi::ump_input_configuration
, winmidi::input_configuration
{
} configuration;
explicit midi_in_impl(
libremidi::ump_input_configuration&& conf, winmidi::input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
}
~midi_in_impl() override { close_port(); }
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_in_winmidi: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_winmidi: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_in_winmidi: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override
{
return libremidi::API::WINDOWS_MIDI_SERVICES;
}
bool open_port(const input_port& port, std::string_view) override
{
#if 0
const auto id = winrt::to_hstring(port.port_name);
if (id.empty())
return false;
port_ = get(MidiInPort::FromIdAsync(id));
if (!port_)
return false;
port_.MessageReceived(
[=](const winrt::Windows::Devices::Midi::IMidiInPort& inputPort,
const winrt::Windows::Devices::Midi::MidiMessageReceivedEventArgs& args) {
this->process_message(args.Message());
});
#endif
return true;
}
#if 0
void process_message(const winrt::Windows::Devices::Midi::IMidiMessage& msg)
{
auto reader = DataReader::FromBuffer(msg.RawData());
auto begin = msg.RawData().data();
auto end = begin + msg.RawData().Length();
auto t = msg.Timestamp().count();
this->configuration.on_message(libremidi::message{{begin, end}, t});
}
#endif
void close_port() override
{
#if 0
if (port_)
{
port_.Close();
port_ = nullptr;
}
#endif
}
private:
// winrt::Microsoft::Devices::Midi2::IMidiInPort port_{nullptr};
};
}
@@ -0,0 +1,91 @@
#pragma once
#include <libremidi/backends/winmidi/config.hpp>
#include <libremidi/backends/winmidi/helpers.hpp>
#include <libremidi/backends/winmidi/observer.hpp>
#include <libremidi/detail/midi_out.hpp>
namespace libremidi::winmidi
{
class midi_out_impl final
: public midi2::out_api
, public error_handler
{
public:
struct
: libremidi::output_configuration
, winmidi::output_configuration
{
} configuration;
midi_out_impl(libremidi::output_configuration&& conf, winmidi::output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
}
~midi_out_impl() override { close_port(); }
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_out_winmidi: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_winmidi: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_out_winmidi: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override
{
return libremidi::API::WINDOWS_MIDI_SERVICES;
}
bool open_port(const output_port& port, std::string_view) override
{
#if 0
const auto id = winrt::to_hstring(port.port_name);
if (id.empty())
return false;
port_ = get(MidiOutPort::FromIdAsync(id));
return bool(port_);
#endif
return true;
}
void close_port() override
{
#if 0
if (port_)
{
port_.Close();
port_ = {};
}
#endif
}
void send_ump(const uint32_t* message, size_t size) override
{
#if 0
if (!port_)
return;
InMemoryRandomAccessStream str;
DataWriter rb(str);
rb.WriteBytes(
winrt::array_view<const uint8_t>{(const uint8_t*)message, (const uint8_t*)message + size});
port_.SendBuffer(rb.DetachBuffer());
#endif
}
private:
#if 0
winrt::Windows::Devices::Midi2::IMidiOutPort port_{nullptr};
#endif
};
}
@@ -0,0 +1,151 @@
#pragma once
#include <libremidi/backends/winmidi/config.hpp>
#include <libremidi/backends/winmidi/helpers.hpp>
#include <libremidi/detail/observer.hpp>
;
namespace libremidi::winmidi
{
struct port_info
{
hstring id;
hstring name;
};
class observer_impl final : public observer_api
{
public:
struct
: libremidi::observer_configuration
, winmidi::observer_configuration
{
} configuration;
MidiSession session;
explicit observer_impl(
libremidi::observer_configuration&& conf, winmidi::observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
, session{
MidiSession::CreateNewSession(L"libremidi session", MidiSessionSettings::Default())}
{
if (!configuration.has_callbacks())
return;
if (configuration.notify_in_constructor)
{
if (configuration.input_added)
for (const auto& p : get_input_ports())
configuration.input_added(p);
if (configuration.output_added)
for (const auto& p : get_output_ports())
configuration.output_added(p);
}
/*
evTokenOnInputAdded_
= internalInPortObserver_.PortAdded([this](const port_info& p) { on_input_added(p); });
evTokenOnInputRemoved_
= internalInPortObserver_.PortRemoved([this](const port_info& p) { on_input_removed(p); });
evTokenOnOutputAdded_
= internalOutPortObserver_.PortAdded([this](const port_info& p) { on_output_added(p); });
evTokenOnOutputRemoved_ = internalOutPortObserver_.PortRemoved(
[this](const port_info& p) { on_output_removed(p); });
*/
}
~observer_impl()
{
if (!configuration.has_callbacks())
return;
// internalInPortObserver_.PortAdded(evTokenOnInputAdded_);
// internalInPortObserver_.PortRemoved(evTokenOnInputRemoved_);
// internalOutPortObserver_.PortAdded(evTokenOnOutputAdded_);
// internalOutPortObserver_.PortRemoved(evTokenOnOutputRemoved_);
}
libremidi::API get_current_api() const noexcept override
{
return libremidi::API::WINDOWS_MIDI_SERVICES;
}
template <bool Input>
auto to_port_info(const DeviceInformation& p) const noexcept
-> std::conditional_t<Input, input_port, output_port>
{
return {
{.client = 0,
.port = 0,
.manufacturer = "",
.device_name = "",
.port_name = to_string(p.Id()),
.display_name = to_string(p.Name())}};
}
std::vector<libremidi::input_port> get_input_ports() const noexcept override
{
std::vector<libremidi::input_port> ret;
auto deviceSelector = MidiEndpointConnection::GetDeviceSelector();
auto endpointDevices = DeviceInformation::FindAllAsync(deviceSelector).get();
for (const auto& selectedEndpointInformation : endpointDevices)
{
// FIXME if(has input...)
ret.emplace_back(to_port_info(selectedEndpointInformation));
}
return ret;
}
std::vector<libremidi::output_port> get_output_ports() const noexcept override
{
std::vector<libremidi::output_port> ret;
auto deviceSelector = MidiEndpointConnection::GetDeviceSelector();
auto endpointDevices = DeviceInformation::FindAllAsync(deviceSelector).get();
for (const auto& selectedEndpointInformation : endpointDevices)
{
// FIXME if(has output...)
ret.emplace_back(to_port_info(selectedEndpointInformation));
}
return ret;
}
void on_input_added(const DeviceInformation& name)
{
if (configuration.input_added)
configuration.input_added(to_port_info(name));
}
void on_input_removed(const DeviceInformation& name)
{
if (configuration.input_removed)
configuration.input_removed(to_port_info(name));
}
void on_output_added(const DeviceInformation& name)
{
if (configuration.output_added)
configuration.output_added(to_port_info(name));
}
void on_output_removed(const DeviceInformation& name)
{
if (configuration.output_removed)
configuration.output_removed(to_port_info(name));
}
private:
#if 0
static inline observer_winmidi_internal internalInPortObserver_{MidiInPort::GetDeviceSelector()};
static inline observer_winmidi_internal internalOutPortObserver_{
MidiOutPort::GetDeviceSelector()};
int evTokenOnInputAdded_{-1};
int evTokenOnInputRemoved_{-1};
int evTokenOnOutputAdded_{-1};
int evTokenOnOutputRemoved_{-1};
#endif
};
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <libremidi/backends/winmm/midi_in.hpp>
#include <libremidi/backends/winmm/midi_out.hpp>
#include <libremidi/backends/winmm/observer.hpp>
// Default for Windows is to add an identifier to the port names; this
// flag can be defined (e.g. in your project file) to disable this behaviour.
// #define LIBREMIDI_DO_NOT_ENSURE_UNIQUE_PORTNAMES
//*********************************************************************//
// API: Windows Multimedia Library (MM)
//*********************************************************************//
// API information deciphered from:
// -
// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/multimed/htm/_win32_midi_reference.asp
// Thanks to Jean-Baptiste Berruchon for the sysex code.
namespace libremidi
{
struct winmm_backend
{
using midi_in = midi_in_winmm;
using midi_out = midi_out_winmm;
using midi_observer = observer_winmm;
using midi_in_configuration = winmm_input_configuration;
using midi_out_configuration = winmm_output_configuration;
using midi_observer_configuration = winmm_observer_configuration;
static const constexpr auto API = libremidi::API::WINDOWS_MM;
static const constexpr auto name = "winmm";
static const constexpr auto display_name = "Windows Multimedia";
static constexpr inline bool available() noexcept { return true; }
};
}
@@ -0,0 +1,31 @@
#pragma once
#include <libremidi/config.hpp>
#include <chrono>
#include <functional>
namespace libremidi
{
struct winmm_input_configuration
{
int sysex_buffer_size = 1024;
int sysex_buffer_count = 4;
};
struct winmm_output_configuration
{
};
struct poll_parameters
{
std::function<void()> callback;
};
struct winmm_observer_configuration
{
std::chrono::milliseconds poll_period{100};
std::function<void(const poll_parameters&)> manual_poll;
};
}
@@ -0,0 +1,81 @@
#pragma once
#define NOMINMAX 1
#define WIN32_LEAN_AND_MEAN 1
#include <libremidi/detail/midi_api.hpp>
#include <algorithm>
// clang-format off
#include <windows.h>
#include <mmsystem.h>
// clang-format on
namespace libremidi
{
// Convert a nullptr-terminated wide string or ANSI-encoded string to UTF-8.
inline std::string ConvertToUTF8(const TCHAR* str)
{
std::string u8str;
const WCHAR* wstr = L"";
#if defined(UNICODE) || defined(_UNICODE)
wstr = str;
#else
// Convert from ANSI encoding to wide string
int wlength = MultiByteToWideChar(CP_ACP, 0, str, -1, nullptr, 0);
std::wstring wstrtemp;
if (wlength)
{
wstrtemp.assign(wlength - 1, 0);
MultiByteToWideChar(CP_ACP, 0, str, -1, &wstrtemp[0], wlength);
wstr = &wstrtemp[0];
}
#endif
// Convert from wide string to UTF-8
int length = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);
if (length)
{
u8str.assign(static_cast<std::string::size_type>(length - 1), 0);
/*length =*/WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &u8str[0], length, nullptr, nullptr);
}
return u8str;
}
// Next functions add the portNumber to the name so that
// the device's names are sure to be listed with individual names
// even when they have the same brand name
inline void MakeUniqueInPortName(std::string& deviceName, unsigned int portNumber)
{
int x = 1;
for (unsigned int i = 0; i < portNumber; i++)
{
MIDIINCAPS deviceCaps;
midiInGetDevCaps(i, &deviceCaps, sizeof(MIDIINCAPS));
auto stringName = ConvertToUTF8(deviceCaps.szPname);
if (deviceName == stringName)
{
x++;
}
}
deviceName += " ";
deviceName += std::to_string(x);
}
inline void MakeUniqueOutPortName(std::string& deviceName, unsigned int portNumber)
{
int x = 1;
for (unsigned int i = 0; i < portNumber; i++)
{
MIDIOUTCAPS deviceCaps;
midiOutGetDevCaps(i, &deviceCaps, sizeof(MIDIOUTCAPS));
auto stringName = ConvertToUTF8(deviceCaps.szPname);
if (deviceName == stringName)
{
x++;
}
}
deviceName += " ";
deviceName += std::to_string(x);
}
}
@@ -0,0 +1,309 @@
#pragma once
#include <libremidi/backends/winmm/config.hpp>
#include <libremidi/backends/winmm/helpers.hpp>
#include <libremidi/backends/winmm/observer.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
namespace libremidi
{
class midi_in_winmm final
: public midi1::in_api
, public error_handler
{
public:
struct
: input_configuration
, winmm_input_configuration
{
} configuration;
explicit midi_in_winmm(input_configuration&& conf, winmm_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
// We'll issue a warning here if no devices are available but not
// throw an error since the user can plugin something later.
if (midiInGetNumDevs() == 0)
{
warning(
configuration, "midi_in_winmm::initialize: no MIDI input devices currently available.");
}
if (!InitializeCriticalSectionAndSpinCount(&(this->_mutex), 0x00000400))
{
warning(
configuration,
"midi_in_winmm::initialize: InitializeCriticalSectionAndSpinCount failed.");
}
}
~midi_in_winmm() override
{
// Close a connection if it exists.
midi_in_winmm::close_port();
DeleteCriticalSection(&(this->_mutex));
}
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_in_winmm: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_winmm: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_in_winmm: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_MM; }
bool do_open(unsigned int portNumber)
{
MMRESULT result = midiInOpen(
&this->inHandle, portNumber, std::bit_cast<DWORD_PTR>(&midiInputCallback),
std::bit_cast<DWORD_PTR>(this), CALLBACK_FUNCTION);
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration, "midi_in_winmm::open_port: error creating Windows MM MIDI input port.");
return false;
}
// Allocate and init the sysex buffers.
const auto bufferCount = static_cast<std::size_t>(configuration.sysex_buffer_count);
this->sysexBuffer.resize(bufferCount);
for (std::size_t i = 0; i < bufferCount; ++i)
{
this->sysexBuffer[i] = new MIDIHDR;
this->sysexBuffer[i]->lpData = new char[configuration.sysex_buffer_size];
this->sysexBuffer[i]->dwBufferLength = static_cast<DWORD>(configuration.sysex_buffer_size);
this->sysexBuffer[i]->dwUser = i; // We use the dwUser parameter as buffer indicator
this->sysexBuffer[i]->dwFlags = 0;
result = midiInPrepareHeader(this->inHandle, this->sysexBuffer[i], sizeof(MIDIHDR));
if (result != MMSYSERR_NOERROR)
{
midiInClose(this->inHandle);
this->inHandle = nullptr;
error<driver_error>(
configuration,
"midi_in_winmm::open_port: error starting Windows MM MIDI input port "
"(PrepareHeader).");
return false;
}
// Register the buffer.
result = midiInAddBuffer(this->inHandle, this->sysexBuffer[i], sizeof(MIDIHDR));
if (result != MMSYSERR_NOERROR)
{
midiInClose(this->inHandle);
this->inHandle = nullptr;
error<driver_error>(
configuration,
"midi_in_winmm::open_port: error starting Windows MM MIDI input port "
"(AddBuffer).");
return false;
}
}
result = midiInStart(this->inHandle);
midi_start_timestamp = std::chrono::steady_clock::now();
if (result != MMSYSERR_NOERROR)
{
midiInClose(this->inHandle);
this->inHandle = nullptr;
error<driver_error>(
configuration, "midi_in_winmm::open_port: error starting Windows MM MIDI input port.");
return false;
}
return true;
}
bool open_port(const input_port& p, std::string_view) override
{
observer_winmm obs{{}, winmm_observer_configuration{}};
auto ports = obs.get_input_ports();
// First check with the display name, e.g. MIDI KEYBOARD 2 will match MIDI KEYBOARD 2
for (auto& port : ports)
{
if (p.display_name == port.display_name)
return do_open(port.port);
}
// If nothing is found, try to check with the raw name
for (auto& port : ports)
{
if (p.port_name == port.port_name)
return do_open(port.port);
}
error<invalid_parameter_error>(
configuration, "midi_in_winmm::open_port: port not found: " + p.port_name);
return false;
}
void close_port() override
{
if (connected_)
{
EnterCriticalSection(&(this->_mutex));
midiInReset(this->inHandle);
midiInStop(this->inHandle);
for (std::size_t i = 0; i < static_cast<std::size_t>(configuration.sysex_buffer_count); ++i)
{
MMRESULT res{};
int wait_count = 5;
while (
((res = midiInUnprepareHeader(this->inHandle, this->sysexBuffer[i], sizeof(MIDIHDR)))
== MIDIERR_STILLPLAYING)
&& wait_count-- >= 0)
{
Sleep(1);
}
if (res != MMSYSERR_NOERROR)
{
warning(
configuration,
"midi_in_winmm::open_port: error closing Windows MM MIDI input "
"port (midiInUnprepareHeader).");
continue;
}
else
{
delete[] this->sysexBuffer[i]->lpData;
delete[] this->sysexBuffer[i];
}
}
midiInClose(this->inHandle);
this->inHandle = nullptr;
LeaveCriticalSection(&(this->_mutex));
}
}
private:
timestamp absolute_timestamp() const noexcept override
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - midi_start_timestamp)
.count();
}
static constexpr int bytes_for_message(uint8_t status)
{
if (status < 0xC0)
return 3;
else if (status < 0xE0)
return 2;
else if (status < 0xF0)
return 3;
else if (status == 0xF1)
return 2;
else if (status == 0xF2)
return 3;
else if (status == 0xF3)
return 2;
else if (status == 0xF8)
return 1;
else if (status == 0xFE)
return 1;
else
return 0;
}
static void CALLBACK midiInputCallback(
HMIDIIN /*hmin*/, UINT inputStatus, DWORD_PTR instancePtr, DWORD_PTR midiMessage,
DWORD_PTR timestamp)
{
if (inputStatus != MIM_DATA && inputStatus != MIM_LONGDATA && inputStatus != MIM_LONGERROR)
return;
auto& self = *reinterpret_cast<midi_in_winmm*>(instancePtr);
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = true,
.absolute_is_monotonic = false,
.has_samples = false,
};
const auto to_ns = [timestamp] { return timestamp * 1'000'000; };
if (inputStatus == MIM_DATA)
{
// Channel or system message
uint8_t message[sizeof(DWORD_PTR)];
memcpy(message, &midiMessage, sizeof(DWORD_PTR));
// Make sure the first byte is a status byte.
if (message[0] & 0x80)
{
self.m_processing.on_bytes(
{message, message + bytes_for_message(message[0])},
self.m_processing.timestamp<timestamp_info>(to_ns, 0));
}
}
else
{
// Sysex message ( MIM_LONGDATA or MIM_LONGERROR )
const auto* sysex = reinterpret_cast<MIDIHDR*>(midiMessage);
if(inputStatus == MIM_LONGERROR)
{
self.m_processing.message.bytes.clear();
self.m_processing.state = self.m_processing.main;
}
else if (!self.configuration.ignore_sysex)
{
if(sysex->dwBytesRecorded > 0)
{
const auto sysex_bytes = reinterpret_cast<uint8_t*>(sysex->lpData);
self.m_processing.on_bytes(
{sysex_bytes, sysex_bytes + sysex->dwBytesRecorded},
self.m_processing.timestamp<timestamp_info>(to_ns, 0));
}
}
// The WinMM API requires that the sysex buffer be requeued after
// input of each sysex message. Even if we are ignoring sysex
// messages, we still need to requeue the buffer in case the user
// decides to not ignore sysex messages in the future. However,
// it seems that WinMM calls this function with an empty sysex
// buffer when an application closes and in this case, we should
// avoid requeueing it, else the computer suddenly reboots after
// one or two minutes.
if (self.sysexBuffer[sysex->dwUser]->dwBytesRecorded > 0)
{
EnterCriticalSection(&(self._mutex));
MMRESULT result
= midiInAddBuffer(self.inHandle, self.sysexBuffer[sysex->dwUser], sizeof(MIDIHDR));
LeaveCriticalSection(&(self._mutex));
if (result != MMSYSERR_NOERROR)
{
#if defined(__LIBREMIDI_DEBUG__)
std::cerr << "\nmidi_in::midiInputCallback: error sending sysex to "
"Midi device!!\n\n";
#endif
}
}
}
}
HMIDIIN inHandle; // Handle to Midi Input Device
std::vector<LPMIDIHDR> sysexBuffer;
// [Patrice] see
// https://groups.google.com/forum/#!topic/mididev/6OUjHutMpEo
CRITICAL_SECTION _mutex;
std::chrono::steady_clock::time_point midi_start_timestamp;
midi1::input_state_machine m_processing{this->configuration};
};
}
@@ -0,0 +1,181 @@
#pragma once
#include <libremidi/backends/winmm/config.hpp>
#include <libremidi/backends/winmm/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
namespace libremidi
{
class midi_out_winmm final
: public midi1::out_api
, public error_handler
{
public:
struct
: output_configuration
, winmm_output_configuration
{
} configuration;
midi_out_winmm(output_configuration&& conf, winmm_output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
// We'll issue a warning here if no devices are available but not
// throw an error since the user can plug something in later.
if (midiOutGetNumDevs() == 0)
{
warning(
configuration,
"midi_out_winmm::initialize: no MIDI output devices currently "
"available.");
}
}
~midi_out_winmm() override
{
// Close a connection if it exists.
midi_out_winmm::close_port();
}
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_out_winmm: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_winmm: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_out_winmm: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_MM; }
[[nodiscard]] bool do_open(unsigned int portNumber)
{
MMRESULT result = midiOutOpen(&this->outHandle, portNumber, 0, 0, CALLBACK_NULL);
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration,
"midi_out_winmm::open_port: error creating Windows MM MIDI output "
"port.");
return false;
}
return true;
}
bool open_port(const output_port& p, std::string_view) override
{
observer_winmm obs{{}, winmm_observer_configuration{}};
auto ports = obs.get_output_ports();
// First check with the display name, e.g. MIDI KEYBOARD 2 will match MIDI KEYBOARD 2
for (auto& port : ports)
{
if (p.display_name == port.display_name)
return do_open(port.port);
}
// If nothing is found, try to check with the raw name
for (auto& port : ports)
{
if (p.port_name == port.port_name)
return do_open(port.port);
}
error<invalid_parameter_error>(
configuration, "midi_out_winmm::open_port: port not found: " + p.port_name);
return false;
}
void close_port() override
{
if (this->outHandle)
midiOutClose(this->outHandle);
this->outHandle = nullptr;
connected_ = false;
}
void send_message(const unsigned char* message, size_t size) override
{
if (!connected_)
return;
if (size == 0)
{
warning(configuration, "midi_out_winmm::send_message: message argument is empty!");
return;
}
if (message[0] == 0xF0)
{ // Sysex message
buffer.assign(message, message + size);
// FIXME this can be made asynchronous... see Chrome source.
// But need to know whe buffers are freed.
// Create and prepare MIDIHDR structure.
MIDIHDR sysex{};
sysex.lpData = (LPSTR)buffer.data();
sysex.dwBufferLength = size;
sysex.dwFlags = 0;
auto result = midiOutPrepareHeader(this->outHandle, &sysex, sizeof(MIDIHDR));
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration, "midi_out_winmm::send_message: error preparing sysex header.");
return;
}
// Send the message.
result = midiOutLongMsg(this->outHandle, &sysex, sizeof(MIDIHDR));
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration, "midi_out_winmm::send_message: error sending sysex message.");
return;
}
// Unprepare the buffer and MIDIHDR.
// FIXME yuck
while (MIDIERR_STILLPLAYING
== midiOutUnprepareHeader(this->outHandle, &sysex, sizeof(MIDIHDR)))
Sleep(1);
}
else
{ // Channel or system message.
// Make sure the message size isn't too big.
if (size > 3)
{
warning(
configuration,
"midi_out_winmm::send_message: message size is greater than 3 bytes "
"(and not sysex)!");
return;
}
// Pack MIDI bytes into double word.
DWORD packet;
std::copy_n(message, size, (unsigned char*)&packet);
// Send the message immediately.
auto result = midiOutShortMsg(this->outHandle, packet);
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration, "midi_out_winmm::send_message: error sending MIDI message.");
}
}
}
private:
HMIDIOUT outHandle; // Handle to Midi Output Device
std::vector<char> buffer;
};
}
@@ -0,0 +1,271 @@
#pragma once
#include <libremidi/backends/winmm/config.hpp>
#include <libremidi/backends/winmm/helpers.hpp>
#include <libremidi/detail/observer.hpp>
#include <condition_variable>
#include <mutex>
#include <ranges>
#include <thread>
namespace libremidi
{
class observer_winmm : public observer_api
{
public:
struct
: observer_configuration
, winmm_observer_configuration
{
} configuration;
explicit observer_winmm(observer_configuration&& conf, winmm_observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (!configuration.has_callbacks())
return;
if (configuration.notify_in_constructor)
check_new_ports<true>();
else
check_new_ports<false>();
}
~observer_winmm() { }
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_MM; }
std::vector<input_port> get_input_ports() const noexcept override
{
return get_port_list<true>();
}
std::vector<output_port> get_output_ports() const noexcept override
{
return get_port_list<false>();
}
protected:
template <bool Notify>
void check_new_ports()
{
auto currInputPortList = get_port_list<true>();
if constexpr (Notify)
{
compare_port_lists_and_notify_clients(
inputPortList, currInputPortList, configuration.input_added,
configuration.input_removed);
}
inputPortList = std::move(currInputPortList);
auto currOutputPortList = get_port_list<false>();
if constexpr (Notify)
{
compare_port_lists_and_notify_clients(
outputPortList, currOutputPortList, configuration.output_added,
configuration.output_removed);
}
outputPortList = std::move(currOutputPortList);
}
void compare_port_lists_and_notify_clients(
const auto& prevList, const auto& currList, const auto& portAddedFunc,
const auto& portRemovedFunc)
{
if (portAddedFunc)
{
for (const auto& port : currList)
{
auto iter
= std::ranges::find(prevList, port.display_name, &port_information::display_name);
if (iter == prevList.end())
portAddedFunc(port);
}
}
if (portRemovedFunc)
{
for (const auto& port : prevList)
{
auto iter
= std::ranges::find(currList, port.display_name, &port_information::display_name);
if (iter == currList.end())
portRemovedFunc(port);
}
}
}
input_port to_in_port_info(std::size_t index) const noexcept
{
MIDIINCAPS deviceCaps;
midiInGetDevCaps(index, &deviceCaps, sizeof(MIDIINCAPS));
auto rawName = ConvertToUTF8(deviceCaps.szPname);
auto portName = rawName;
MakeUniqueInPortName(portName, index);
return {
{.client = 0,
.port = index,
.manufacturer = "",
.device_name = "",
.port_name = rawName,
.display_name = portName}};
}
output_port to_out_port_info(std::size_t index) const noexcept
{
MIDIOUTCAPS deviceCaps;
midiOutGetDevCaps(index, &deviceCaps, sizeof(MIDIOUTCAPS));
auto rawName = ConvertToUTF8(deviceCaps.szPname);
auto portName = rawName;
MakeUniqueOutPortName(portName, index);
return {
{.client = 0,
.port = index,
.manufacturer = "",
.device_name = "",
.port_name = rawName,
.display_name = portName}};
}
template <bool Input>
auto get_port_list() const noexcept
-> std::vector<std::conditional_t<Input, input_port, output_port>>
{
std::vector<std::conditional_t<Input, input_port, output_port>> portList;
if constexpr (Input)
{
std::size_t nDevices = midiInGetNumDevs();
for (std::size_t i = 0; i < nDevices; ++i)
{
portList.push_back(to_in_port_info(i));
}
}
else
{
std::size_t nDevices = midiOutGetNumDevs();
for (std::size_t i = 0; i < nDevices; ++i)
{
portList.push_back(to_out_port_info(i));
}
}
return portList;
}
static constexpr bool INPUT = true;
static constexpr bool OUTPUT = false;
std::vector<input_port> inputPortList;
std::vector<output_port> outputPortList;
};
}
#if __has_include(<stop_token>) && __cpp_lib_jthread >= 201911L
#include <stop_token>
namespace libremidi::winmm
{
class observer_threaded final : public observer_winmm
{
public:
struct
: observer_configuration
, winmm_observer_configuration
{
} configuration;
explicit observer_threaded(observer_configuration&& conf, winmm_observer_configuration&& apiconf)
: observer_winmm{std::move(conf), std::move(apiconf)}
{
thread = std::jthread([this](std::stop_token tk) {
while (!tk.stop_requested())
{
check_new_ports<true>();
std::this_thread::sleep_for(this->configuration.poll_period);
}
});
}
private:
std::jthread thread;
};
}
#else
#include <atomic>
#include <semaphore>
namespace libremidi::winmm
{
class observer_threaded final : public observer_winmm
{
public:
struct
: observer_configuration
, winmm_observer_configuration
{
} configuration;
explicit observer_threaded(observer_configuration&& conf, winmm_observer_configuration&& apiconf)
: observer_winmm{std::move(conf), std::move(apiconf)}
, sema{0}
{
thread = std::thread([this] {
while (!stop_flag.test(std::memory_order_acquire))
{
check_new_ports<true>();
std::this_thread::sleep_for(this->configuration.poll_period);
}
sema.release();
});
}
~observer_threaded()
{
stop_flag.test_and_set();
sema.acquire();
thread.join();
}
private:
std::thread thread;
std::atomic_flag stop_flag = ATOMIC_FLAG_INIT;
std::binary_semaphore sema;
};
}
#endif
namespace libremidi::winmm
{
class observer_manual final : public observer_winmm
{
public:
struct
: observer_configuration
, winmm_observer_configuration
{
} configuration;
explicit observer_manual(observer_configuration&& conf, winmm_observer_configuration&& apiconf)
: observer_winmm{std::move(conf), std::move(apiconf)}
{
this->configuration.manual_poll({.callback = [this] { this->check_new_ports<true>(); }});
}
~observer_manual() { }
};
}
namespace libremidi
{
template <>
inline std::unique_ptr<observer_api> make<observer_winmm>(
libremidi::observer_configuration&& conf, libremidi::winmm_observer_configuration&& api)
{
if (api.manual_poll)
return std::make_unique<winmm::observer_manual>(std::move(conf), std::move(api));
else
return std::make_unique<winmm::observer_threaded>(std::move(conf), std::move(api));
}
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <libremidi/backends/winuwp/midi_in.hpp>
#include <libremidi/backends/winuwp/midi_out.hpp>
#include <libremidi/backends/winuwp/observer.hpp>
namespace libremidi
{
struct winuwp_backend
{
using midi_in = midi_in_winuwp;
using midi_out = midi_out_winuwp;
using midi_observer = observer_winuwp;
using midi_in_configuration = winuwp_input_configuration;
using midi_out_configuration = winuwp_output_configuration;
using midi_observer_configuration = winuwp_observer_configuration;
static const constexpr auto API = libremidi::API::WINDOWS_UWP;
static const constexpr auto name = "winuwp";
static const constexpr auto display_name = "Windows UWP";
static constexpr inline bool available() noexcept { return true; }
};
} // namespace libremidi
@@ -0,0 +1,19 @@
#pragma once
#include <libremidi/config.hpp>
namespace libremidi
{
struct winuwp_input_configuration
{
};
struct winuwp_output_configuration
{
};
struct winuwp_observer_configuration
{
};
}
@@ -0,0 +1,63 @@
#pragma once
#define NOMINMAX 1
#define WIN32_LEAN_AND_MEAN 1
#include <libremidi/detail/midi_api.hpp>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include <winrt/Windows.Devices.Enumeration.h>
#include <winrt/Windows.Devices.Midi.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Storage.Streams.h>
namespace libremidi
{
inline void winrt_init()
{
// init_apartment should only be called on the threads we own.
// Since we're the library we don't own the threads we are called from,
// so we should not perform this initialization ourselves.
// winrt::init_apartment();
}
namespace
{
using namespace winrt;
using namespace winrt::Windows::Foundation;
using namespace winrt::Windows::Devices::Midi;
using namespace winrt::Windows::Devices::Enumeration;
using namespace winrt::Windows::Storage::Streams;
// Helper function to allow waiting for aynchronous operation completion
// from the thread in STA. The only benefit from it compared to the
// get() function from winrt is that we avoid an assertion if waiting
// from the STA thread.
template <typename T>
auto get(T const& async)
{
if (async.Status() != AsyncStatus::Completed)
{
slim_mutex m;
slim_condition_variable cv;
bool completed = false;
async.Completed([&](auto&&, auto&&) {
{
slim_lock_guard const guard(m);
completed = true;
}
cv.notify_one();
});
slim_lock_guard guard(m);
cv.wait(m, [&] { return completed; });
}
return async.GetResults();
}
}
}
@@ -0,0 +1,106 @@
#pragma once
#include <libremidi/backends/winuwp/config.hpp>
#include <libremidi/backends/winuwp/helpers.hpp>
#include <libremidi/backends/winuwp/observer.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
namespace libremidi
{
class midi_in_winuwp final
: public midi1::in_api
, public error_handler
{
public:
struct
: input_configuration
, winuwp_input_configuration
{
} configuration;
explicit midi_in_winuwp(input_configuration&& conf, winuwp_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
winrt_init();
}
~midi_in_winuwp() override { close_port(); }
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_in_winuwp: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_winuwp: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_in_winuwp: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_UWP; }
bool open_port(const input_port& port, std::string_view) override
{
const auto id = winrt::to_hstring(port.port_name);
if (id.empty())
return false;
port_ = get(MidiInPort::FromIdAsync(id));
if (!port_)
return false;
midi_start_timestamp = std::chrono::steady_clock::now();
port_.MessageReceived(
[=](const winrt::Windows::Devices::Midi::IMidiInPort& inputPort,
const winrt::Windows::Devices::Midi::MidiMessageReceivedEventArgs& args) {
this->process_message(args.Message());
});
return true;
}
void process_message(const winrt::Windows::Devices::Midi::IMidiMessage& msg)
{
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = true,
.absolute_is_monotonic = false,
.has_samples = false,
};
auto reader = DataReader::FromBuffer(msg.RawData());
auto begin = msg.RawData().data();
auto end = begin + msg.RawData().Length();
const auto to_ns = [&msg] { return msg.Timestamp().count() * 1'000'000; };
m_processing.on_bytes({begin, end}, m_processing.timestamp<timestamp_info>(to_ns, 0));
}
void close_port() override
{
if (port_)
{
port_.Close();
port_ = nullptr;
}
}
timestamp absolute_timestamp() const noexcept override
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - midi_start_timestamp)
.count();
}
private:
winrt::Windows::Devices::Midi::IMidiInPort port_{nullptr};
std::chrono::steady_clock::time_point midi_start_timestamp;
midi1::input_state_machine m_processing{this->configuration};
};
}
@@ -0,0 +1,80 @@
#pragma once
#include <libremidi/backends/winuwp/config.hpp>
#include <libremidi/backends/winuwp/helpers.hpp>
#include <libremidi/backends/winuwp/observer.hpp>
#include <libremidi/detail/midi_out.hpp>
namespace libremidi
{
class midi_out_winuwp final
: public midi1::out_api
, public error_handler
{
public:
struct
: output_configuration
, winuwp_output_configuration
{
} configuration;
midi_out_winuwp(output_configuration&& conf, winuwp_output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
winrt_init();
}
~midi_out_winuwp() override { close_port(); }
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_out_winuwp: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_winuwp: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_out_winuwp: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_UWP; }
bool open_port(const output_port& port, std::string_view) override
{
const auto id = winrt::to_hstring(port.port_name);
if (id.empty())
return false;
port_ = get(MidiOutPort::FromIdAsync(id));
return bool(port_);
}
void close_port() override
{
if (port_)
{
port_.Close();
port_ = {};
}
}
void send_message(const unsigned char* message, size_t size) override
{
if (!port_)
return;
InMemoryRandomAccessStream str;
DataWriter rb(str);
rb.WriteBytes(
winrt::array_view<const uint8_t>{(const uint8_t*)message, (const uint8_t*)message + size});
port_.SendBuffer(rb.DetachBuffer());
}
private:
winrt::Windows::Devices::Midi::IMidiOutPort port_{nullptr};
};
}
@@ -0,0 +1,296 @@
#pragma once
#include <libremidi/backends/winuwp/config.hpp>
#include <libremidi/detail/observer.hpp>
namespace libremidi
{
class observer_winuwp_internal
{
public:
struct port_info
{
hstring id;
hstring name;
};
struct callback
{
int token{};
std::function<void(const port_info&)> function;
};
struct callbacks
{
std::vector<callback> cbs;
int current_token{};
void operator()(const port_info& p)
{
for (auto& cb : cbs)
cb.function(p);
}
int add(std::function<void(const port_info&)> f)
{
int tk = current_token++;
cbs.emplace_back(tk, f);
return tk;
}
void remove(int tk)
{
auto it = std::remove_if(
cbs.begin(), cbs.end(), [tk](const callback& c) { return c.token == tk; });
auto r = std::distance(it, cbs.end());
cbs.erase(it, cbs.end());
}
};
explicit observer_winuwp_internal(hstring deviceSelector) { initialize(deviceSelector); }
~observer_winuwp_internal() { terminate(); }
std::vector<port_info> get_ports() const
{
std::lock_guard<std::mutex> lock(portListMutex_);
return portList_;
}
unsigned int get_port_count() const
{
std::lock_guard<std::mutex> lock(portListMutex_);
return static_cast<unsigned int>(portList_.size());
}
bool get_port_info(unsigned int portNumber, port_info& portInfo) const
{
std::lock_guard<std::mutex> lock(portListMutex_);
if (portNumber >= portList_.size())
return false;
portInfo = portList_[portNumber];
return true;
}
hstring get_port_id(unsigned int portNumber) const
{
std::lock_guard<std::mutex> lock(portListMutex_);
return portNumber < portList_.size() ? portList_[portNumber].id : hstring{};
}
std::string get_port_name(unsigned int portNumber) const
{
std::lock_guard<std::mutex> lock(portListMutex_);
return portNumber < portList_.size() ? to_string(portList_[portNumber].name) : std::string{};
}
int PortAdded(const std::function<void(port_info)>& handler)
{
return portAddedEvent_.add(handler);
}
void PortAdded(int token) noexcept { portAddedEvent_.remove(token); }
int PortRemoved(const std::function<void(port_info)>& handler)
{
return portRemovedEvent_.add(handler);
}
void PortRemoved(int token) noexcept { portRemovedEvent_.remove(token); }
private:
observer_winuwp_internal(const observer_winuwp_internal&) = delete;
observer_winuwp_internal& operator=(const observer_winuwp_internal&) = delete;
private:
void initialize(hstring deviceSelector)
{
deviceWatcher_ = DeviceInformation::CreateWatcher(deviceSelector);
evTokenOnDeviceAdded_
= deviceWatcher_.Added({this, &observer_winuwp_internal::on_device_added});
evTokenOnDeviceRemoved_
= deviceWatcher_.Removed({this, &observer_winuwp_internal::on_device_removed});
evTokenOnDeviceUpdated_
= deviceWatcher_.Updated({this, &observer_winuwp_internal::on_device_updated});
evTokenOnDeviceEnumerationCompleted_ = deviceWatcher_.EnumerationCompleted(
{this, &observer_winuwp_internal::on_device_enumeration_completed});
deviceWatcher_.Start();
}
void terminate()
{
deviceWatcher_.Stop();
deviceWatcher_.EnumerationCompleted(evTokenOnDeviceEnumerationCompleted_);
deviceWatcher_.Updated(evTokenOnDeviceUpdated_);
deviceWatcher_.Removed(evTokenOnDeviceRemoved_);
deviceWatcher_.Added(evTokenOnDeviceAdded_);
}
void on_device_added(DeviceWatcher sender, DeviceInformation deviceInfo)
{
port_info p;
{
std::lock_guard<std::mutex> lock(portListMutex_);
p = port_info{deviceInfo.Id(), deviceInfo.Name()};
portList_.push_back(p);
}
portAddedEvent_(p);
}
void on_device_removed(DeviceWatcher sender, DeviceInformationUpdate deviceUpdate)
{
const auto id = deviceUpdate.Id();
auto pred = [&id](const port_info& portInfo) { return portInfo.id == id; };
std::optional<port_info> p;
hstring name;
{
std::lock_guard<std::mutex> lock(portListMutex_);
auto iter = std::find_if(portList_.begin(), portList_.end(), pred);
if (iter != portList_.end())
{
p = *iter;
portList_.erase(iter);
}
}
if (p)
portRemovedEvent_(*p);
}
void on_device_updated(DeviceWatcher sender, DeviceInformationUpdate deviceUpdate) { }
void on_device_enumeration_completed(DeviceWatcher sender, IInspectable const&) { }
private:
std::vector<port_info> portList_;
mutable std::mutex portListMutex_;
DeviceWatcher deviceWatcher_{nullptr};
event_token evTokenOnDeviceAdded_;
event_token evTokenOnDeviceRemoved_;
event_token evTokenOnDeviceUpdated_;
event_token evTokenOnDeviceEnumerationCompleted_;
callbacks portAddedEvent_;
callbacks portRemovedEvent_;
};
class observer_winuwp final : public observer_api
{
public:
struct
: observer_configuration
, winuwp_observer_configuration
{
} configuration;
using port_info = observer_winuwp_internal::port_info;
explicit observer_winuwp(observer_configuration&& conf, winuwp_observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (!configuration.has_callbacks())
return;
if (configuration.notify_in_constructor)
{
if (configuration.input_added)
for (const auto& p : get_input_ports())
configuration.input_added(p);
if (configuration.output_added)
for (const auto& p : get_output_ports())
configuration.output_added(p);
}
evTokenOnInputAdded_
= internalInPortObserver_.PortAdded([this](const port_info& p) { on_input_added(p); });
evTokenOnInputRemoved_
= internalInPortObserver_.PortRemoved([this](const port_info& p) { on_input_removed(p); });
evTokenOnOutputAdded_
= internalOutPortObserver_.PortAdded([this](const port_info& p) { on_output_added(p); });
evTokenOnOutputRemoved_ = internalOutPortObserver_.PortRemoved(
[this](const port_info& p) { on_output_removed(p); });
}
~observer_winuwp()
{
if (!configuration.has_callbacks())
return;
internalInPortObserver_.PortAdded(evTokenOnInputAdded_);
internalInPortObserver_.PortRemoved(evTokenOnInputRemoved_);
internalOutPortObserver_.PortAdded(evTokenOnOutputAdded_);
internalOutPortObserver_.PortRemoved(evTokenOnOutputRemoved_);
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_UWP; }
template <bool Input>
auto to_port_info(const observer_winuwp_internal::port_info& p) const noexcept
-> std::conditional_t<Input, input_port, output_port>
{
return {
{.client = 0,
.port = 0,
.manufacturer = "",
.device_name = "",
.port_name = to_string(p.id),
.display_name = to_string(p.name)}};
}
std::vector<libremidi::input_port> get_input_ports() const noexcept override
{
std::vector<libremidi::input_port> ret;
for (auto& port : internalInPortObserver_.get_ports())
ret.push_back(to_port_info<true>(port));
return ret;
}
std::vector<libremidi::output_port> get_output_ports() const noexcept override
{
std::vector<libremidi::output_port> ret;
for (auto& port : internalOutPortObserver_.get_ports())
ret.push_back(to_port_info<false>(port));
return ret;
}
static observer_winuwp_internal& get_internal_in_port_observer()
{
return internalInPortObserver_;
}
static observer_winuwp_internal& get_internal_out_port_observer()
{
return internalOutPortObserver_;
}
void on_input_added(const observer_winuwp_internal::port_info& name)
{
if (configuration.input_added)
configuration.input_added(to_port_info<true>(name));
}
void on_input_removed(const observer_winuwp_internal::port_info& name)
{
if (configuration.input_removed)
configuration.input_removed(to_port_info<true>(name));
}
void on_output_added(const observer_winuwp_internal::port_info& name)
{
if (configuration.output_added)
configuration.output_added(to_port_info<false>(name));
}
void on_output_removed(const observer_winuwp_internal::port_info& name)
{
if (configuration.output_removed)
configuration.output_removed(to_port_info<false>(name));
}
private:
static inline observer_winuwp_internal internalInPortObserver_{MidiInPort::GetDeviceSelector()};
static inline observer_winuwp_internal internalOutPortObserver_{
MidiOutPort::GetDeviceSelector()};
int evTokenOnInputAdded_{-1};
int evTokenOnInputRemoved_{-1};
int evTokenOnOutputAdded_{-1};
int evTokenOnOutputRemoved_{-1};
};
}