very, very simple WP with midi CC out
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
#include <libremidi/config.hpp>
|
||||
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
//! MIDI API specifier arguments.
|
||||
//! To get information on which feature is supported by each back-end, check their backend file
|
||||
//! in e.g. backends/winmm.hpp, etc.
|
||||
enum class API
|
||||
{
|
||||
UNSPECIFIED, /*!< Search for a working compiled API. */
|
||||
|
||||
// MIDI 1.0 APIs
|
||||
COREMIDI, /*!< macOS CoreMidi API. */
|
||||
ALSA_SEQ, /*!< Linux ALSA Sequencer API. */
|
||||
ALSA_RAW, /*!< Linux Raw ALSA API. */
|
||||
JACK_MIDI, /*!< JACK Low-Latency MIDI Server API. */
|
||||
WINDOWS_MM, /*!< Microsoft Multimedia MIDI API. */
|
||||
WINDOWS_UWP, /*!< Microsoft WinRT MIDI API. */
|
||||
WEBMIDI, /*!< Web MIDI API through Emscripten */
|
||||
PIPEWIRE, /*!< PipeWire */
|
||||
|
||||
// MIDI 2.0 APIs
|
||||
ALSA_RAW_UMP, /*!< Raw ALSA API for MIDI 2.0 */
|
||||
ALSA_SEQ_UMP, /*!< Linux ALSA Sequencer API for MIDI 2.0 */
|
||||
COREMIDI_UMP, /*!< macOS CoreMidi API for MIDI 2.0. Requires macOS 11+ */
|
||||
WINDOWS_MIDI_SERVICES, /*!< Windows API for MIDI 2.0. Requires Windows 11 */
|
||||
|
||||
DUMMY /*!< A compilable but non-functional API. */
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief A function to determine the available compiled MIDI 1.0 APIs.
|
||||
|
||||
The values returned in the std::vector can be compared against
|
||||
the enumerated list values. Note that there can be more than one
|
||||
API compiled for certain operating systems.
|
||||
*/
|
||||
LIBREMIDI_EXPORT std::vector<libremidi::API> available_apis() noexcept;
|
||||
|
||||
/**
|
||||
* \brief A function to determine the available compiled MIDI 2.0 APIs.
|
||||
|
||||
The values returned in the std::vector can be compared against
|
||||
the enumerated list values. Note that there can be more than one
|
||||
API compiled for certain operating systems.
|
||||
*/
|
||||
LIBREMIDI_EXPORT std::vector<libremidi::API> available_ump_apis() noexcept;
|
||||
|
||||
//! A static function to determine the current version.
|
||||
LIBREMIDI_EXPORT std::string_view get_version() noexcept;
|
||||
|
||||
//! Map from and to API names
|
||||
LIBREMIDI_EXPORT std::string_view get_api_name(libremidi::API api);
|
||||
//! Map from and to API names
|
||||
LIBREMIDI_EXPORT std::string_view get_api_display_name(libremidi::API api);
|
||||
//! Look-up an API through its name
|
||||
LIBREMIDI_EXPORT libremidi::API get_compiled_api_by_name(std::string_view api);
|
||||
|
||||
namespace midi1
|
||||
{
|
||||
//! Returns the default MIDI 1.0 backend to use for the target OS.
|
||||
inline constexpr libremidi::API default_api() noexcept
|
||||
{
|
||||
#if defined(__APPLE__)
|
||||
return API::COREMIDI;
|
||||
#elif defined(_WIN32)
|
||||
return API::WINDOWS_MM;
|
||||
#elif defined(__linux__)
|
||||
return API::ALSA_SEQ;
|
||||
#elif defined(__emscripten__)
|
||||
return API::EMSCRIPTEN_WEBMIDI;
|
||||
#else
|
||||
return API::DUMMY;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
namespace midi2
|
||||
{
|
||||
//! Returns the default MIDI 2.0 backend to use for the target OS.
|
||||
inline constexpr libremidi::API default_api() noexcept
|
||||
{
|
||||
#if defined(__APPLE__)
|
||||
return API::COREMIDI_UMP;
|
||||
#elif defined(_WIN32)
|
||||
return API::WINDOWS_MIDI_SERVICES;
|
||||
#elif defined(__linux__)
|
||||
return API::ALSA_SEQ_UMP;
|
||||
#elif defined(__emscripten__)
|
||||
return API::DUMMY;
|
||||
#else
|
||||
return API::DUMMY;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
#pragma once
|
||||
#include <tuple>
|
||||
|
||||
#if !__has_include(<weak_libjack.h>) && !__has_include(<jack/jack.h>)
|
||||
#if defined(LIBREMIDI_JACK)
|
||||
#undef LIBREMIDI_JACK
|
||||
#endif
|
||||
#endif
|
||||
#if !defined(LIBREMIDI_ALSA) && !defined(LIBREMIDI_JACK) && !defined(LIBREMIDI_COREMIDI) \
|
||||
&& !defined(LIBREMIDI_WINMM)
|
||||
#define LIBREMIDI_DUMMY
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_ALSA)
|
||||
#include <libremidi/backends/alsa_seq.hpp>
|
||||
#include <libremidi/backends/linux/alsa.hpp>
|
||||
|
||||
#if LIBREMIDI_ALSA_HAS_RAMWIDI
|
||||
#include <libremidi/backends/alsa_raw.hpp>
|
||||
#endif
|
||||
|
||||
#if LIBREMIDI_ALSA_HAS_UMP
|
||||
#include <libremidi/backends/alsa_raw_ump.hpp>
|
||||
#include <libremidi/backends/alsa_seq_ump.hpp>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_JACK)
|
||||
#include <libremidi/backends/jack.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_PIPEWIRE)
|
||||
#include <libremidi/backends/pipewire.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_COREMIDI)
|
||||
#include <libremidi/backends/coremidi.hpp>
|
||||
#include <libremidi/backends/coremidi_ump.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_WINMM)
|
||||
#include <libremidi/backends/winmm.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_WINUWP)
|
||||
#include <libremidi/backends/winuwp.hpp>
|
||||
#if __has_include(<winrt/Microsoft.Devices.Midi2.h>)
|
||||
#include <libremidi/backends/winmidi.hpp>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_EMSCRIPTEN)
|
||||
#include <libremidi/backends/emscripten.hpp>
|
||||
#endif
|
||||
|
||||
#include <libremidi/backends/dummy.hpp>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
// The order here will control the order of the API search in
|
||||
// the constructor.
|
||||
template <typename unused, typename... Args>
|
||||
constexpr auto make_tl(unused, Args...)
|
||||
{
|
||||
return std::tuple<Args...>{};
|
||||
}
|
||||
|
||||
namespace midi1
|
||||
{
|
||||
static constexpr auto available_backends = make_tl(
|
||||
0
|
||||
#if defined(LIBREMIDI_ALSA)
|
||||
,
|
||||
alsa_seq::backend{}
|
||||
#if LIBREMIDI_ALSA_HAS_RAMWIDI
|
||||
,
|
||||
alsa_raw::backend{}
|
||||
#endif
|
||||
#endif
|
||||
#if defined(LIBREMIDI_COREMIDI)
|
||||
,
|
||||
core_backend{}
|
||||
#endif
|
||||
#if defined(LIBREMIDI_WINMM)
|
||||
,
|
||||
winmm_backend{}
|
||||
#endif
|
||||
#if defined(LIBREMIDI_WINUWP)
|
||||
,
|
||||
winuwp_backend{}
|
||||
#endif
|
||||
#if defined(LIBREMIDI_EMSCRIPTEN)
|
||||
,
|
||||
emscripten_backend{}
|
||||
#endif
|
||||
#if defined(LIBREMIDI_JACK)
|
||||
,
|
||||
jack_backend{}
|
||||
#endif
|
||||
#if defined(LIBREMIDI_PIPEWIRE)
|
||||
,
|
||||
pipewire::backend{}
|
||||
#endif
|
||||
,
|
||||
dummy_backend{});
|
||||
|
||||
// There should always be at least one back-end.
|
||||
static_assert(std::tuple_size_v<decltype(available_backends)> >= 1);
|
||||
|
||||
template <typename F>
|
||||
auto for_all_backends(F&& f)
|
||||
{
|
||||
std::apply([&](auto&&... x) { ((x.available() && (f(x), true)), ...); }, available_backends);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
auto for_backend(libremidi::API api, F&& f)
|
||||
{
|
||||
static constexpr auto is_api = [](auto& backend, libremidi::API api) {
|
||||
return backend.available() && backend.API == api;
|
||||
};
|
||||
std::apply([&](auto&&... b) { ((is_api(b, api) && (f(b), true)) || ...); }, available_backends);
|
||||
}
|
||||
}
|
||||
|
||||
namespace midi2
|
||||
{
|
||||
static constexpr auto available_backends = make_tl(
|
||||
0
|
||||
#if defined(LIBREMIDI_ALSA) && LIBREMIDI_ALSA_HAS_UMP
|
||||
,
|
||||
alsa_seq_ump::backend{}, alsa_raw_ump::backend{}
|
||||
#endif
|
||||
#if defined(LIBREMIDI_COREMIDI)
|
||||
,
|
||||
coremidi_ump::backend{}
|
||||
#endif
|
||||
#if defined(LIBREMIDI_JACK)
|
||||
#endif
|
||||
#if defined(LIBREMIDI_WINUWP)
|
||||
#if __has_include(<winrt/Microsoft.Devices.Midi2.h>)
|
||||
,
|
||||
winmidi::backend{}
|
||||
#endif
|
||||
#endif
|
||||
#if defined(LIBREMIDI_EMSCRIPTEN)
|
||||
#endif
|
||||
,
|
||||
dummy_backend{});
|
||||
|
||||
// There should always be at least one back-end.
|
||||
static_assert(std::tuple_size_v<decltype(available_backends)> >= 1);
|
||||
|
||||
template <typename F>
|
||||
auto for_all_backends(F&& f)
|
||||
{
|
||||
std::apply([&](auto&&... x) { (f(x), ...); }, available_backends);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
auto for_backend(libremidi::API api, F&& f)
|
||||
{
|
||||
static constexpr auto is_api
|
||||
= [](auto& backend, libremidi::API api) { return backend.API == api; };
|
||||
std::apply([&](auto&&... b) { ((is_api(b, api) && (f(b), true)) || ...); }, available_backends);
|
||||
}
|
||||
}
|
||||
|
||||
namespace midi_any
|
||||
{
|
||||
|
||||
template <typename F>
|
||||
auto for_all_backends(F&& f)
|
||||
{
|
||||
midi1::for_all_backends(f);
|
||||
midi2::for_all_backends(f);
|
||||
}
|
||||
|
||||
template <typename F>
|
||||
auto for_backend(libremidi::API api, F&& f)
|
||||
{
|
||||
midi1::for_backend(api, f);
|
||||
midi2::for_backend(api, f);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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(¶ms);
|
||||
|
||||
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(¶m);
|
||||
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(¶ms);
|
||||
|
||||
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; }
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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};
|
||||
};
|
||||
}
|
||||
@@ -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; }
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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, ¤t_frames, ¤t_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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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{};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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, ®istry_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
|
||||
@@ -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
|
||||
};
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#if !defined(LIBREMIDI_HEADER_ONLY)
|
||||
#include "client.hpp"
|
||||
#endif
|
||||
#include <libremidi/backends.hpp>
|
||||
#include <libremidi/shared_context.hpp>
|
||||
|
||||
#ifdef LIBREMIDI_ALSA
|
||||
#include <libremidi/backends/alsa_seq/shared_handler.hpp>
|
||||
#endif
|
||||
#ifdef LIBREMIDI_JACK
|
||||
#include <libremidi/backends/jack/shared_handler.hpp>
|
||||
#endif
|
||||
namespace libremidi
|
||||
{
|
||||
LIBREMIDI_INLINE
|
||||
shared_configurations
|
||||
create_shared_context(const libremidi::API api, [[maybe_unused]] std::string_view client_name)
|
||||
{
|
||||
switch (api)
|
||||
{
|
||||
#if __has_include(<boost/lockfree/spsc_queue.hpp>)
|
||||
#if defined(LIBREMIDI_ALSA)
|
||||
case libremidi::API::ALSA_SEQ:
|
||||
return alsa_seq::shared_handler::make(client_name);
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_JACK)
|
||||
case libremidi::API::JACK_MIDI:
|
||||
return jack::shared_handler::make(client_name);
|
||||
#endif
|
||||
#endif
|
||||
case libremidi::API::COREMIDI:
|
||||
case libremidi::API::COREMIDI_UMP:
|
||||
// TODO
|
||||
|
||||
case libremidi::API::WINDOWS_MIDI_SERVICES:
|
||||
// TODO
|
||||
|
||||
default:
|
||||
return {
|
||||
.context = {},
|
||||
.observer = observer_configuration_for(api),
|
||||
.in = midi_in_configuration_for(api),
|
||||
.out = midi_out_configuration_for(api)};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
#pragma once
|
||||
#include <libremidi/configurations.hpp>
|
||||
#include <libremidi/libremidi.hpp>
|
||||
#include <libremidi/shared_context.hpp>
|
||||
|
||||
#include <map>
|
||||
|
||||
namespace libremidi::midi1
|
||||
{
|
||||
struct client_configuration
|
||||
{
|
||||
libremidi::API api = libremidi::midi1::default_api();
|
||||
|
||||
//! A client name, if the backend allows it (e.g. JACK, ALSA SEQ...)
|
||||
std::string_view client_name = "libremidi client";
|
||||
|
||||
//! Set a callback function to be invoked for incoming MIDI messages.
|
||||
//! Mandatory!
|
||||
std::function<void(const libremidi::input_port&, message&&)> on_message
|
||||
= [](const libremidi::input_port& /*port*/, libremidi::message&&) {};
|
||||
|
||||
//! Observation callbacks for when ports are added or removed
|
||||
input_port_callback input_added;
|
||||
input_port_callback input_removed;
|
||||
output_port_callback output_added;
|
||||
output_port_callback output_removed;
|
||||
|
||||
//! Error callback function to be invoked when an error has occured.
|
||||
/*!
|
||||
The callback function will be called whenever an error has occured. It is
|
||||
best to set the error callback function before opening a port.
|
||||
*/
|
||||
midi_error_callback on_error{};
|
||||
midi_error_callback on_warning{};
|
||||
|
||||
//! Poll period for observation polling operations, if relevant to the backend
|
||||
std::chrono::milliseconds poll_period{100};
|
||||
|
||||
//! Specify whether certain MIDI message types should be queued or ignored
|
||||
//! during input.
|
||||
/*!
|
||||
By default, MIDI timing and active sensing messages are ignored
|
||||
during message input because of their relative high data rates.
|
||||
MIDI sysex messages are ignored by default as well. Variable
|
||||
values of "true" imply that the respective message type will be
|
||||
ignored.
|
||||
*/
|
||||
uint32_t ignore_sysex : 1 = true;
|
||||
uint32_t ignore_timing : 1 = true;
|
||||
uint32_t ignore_sensing : 1 = true;
|
||||
uint32_t timestamps : 3 = timestamp_mode::Absolute;
|
||||
|
||||
//! Observe hardware ports
|
||||
uint32_t track_hardware : 1 = true;
|
||||
|
||||
//! Observe software (virtual) ports if the API provides it
|
||||
uint32_t track_virtual : 1 = false;
|
||||
};
|
||||
|
||||
class client
|
||||
{
|
||||
public:
|
||||
explicit client(const client_configuration& conf)
|
||||
: client{conf, create_shared_context(conf.api, conf.client_name)}
|
||||
{
|
||||
}
|
||||
|
||||
explicit client(const client_configuration& conf, shared_configurations ctx)
|
||||
: configuration{conf}
|
||||
, context{ctx}
|
||||
, m_observer{
|
||||
observer_configuration{
|
||||
.on_error = conf.on_error,
|
||||
.on_warning = conf.on_warning,
|
||||
|
||||
.input_added = conf.input_added,
|
||||
.input_removed = conf.input_removed,
|
||||
.output_added = conf.output_added,
|
||||
.output_removed = conf.output_removed,
|
||||
|
||||
.track_hardware = conf.track_hardware,
|
||||
.track_virtual = conf.track_virtual,
|
||||
.notify_in_constructor = false},
|
||||
context.observer}
|
||||
{
|
||||
if (context.context)
|
||||
context.context->start_processing();
|
||||
}
|
||||
|
||||
~client()
|
||||
{
|
||||
m_inputs.clear();
|
||||
m_outputs.clear();
|
||||
|
||||
if (context.context)
|
||||
context.context->stop_processing();
|
||||
}
|
||||
|
||||
std::vector<libremidi::input_port> get_input_ports() const noexcept
|
||||
{
|
||||
return m_observer.get_input_ports();
|
||||
}
|
||||
|
||||
std::vector<libremidi::output_port> get_output_ports() const noexcept
|
||||
{
|
||||
return m_observer.get_output_ports();
|
||||
}
|
||||
|
||||
void add_input(const input_port& port, std::string_view name)
|
||||
{
|
||||
if (m_inputs.find(port) != m_inputs.end())
|
||||
return;
|
||||
|
||||
auto res = m_inputs.try_emplace(
|
||||
port,
|
||||
input_configuration{
|
||||
.on_message
|
||||
= [this, port](libremidi::message&& m) {
|
||||
configuration.on_message(port, std::move(m));
|
||||
},
|
||||
.get_timestamp = {},
|
||||
|
||||
.on_error = configuration.on_error,
|
||||
.on_warning = configuration.on_warning,
|
||||
|
||||
.ignore_sysex = configuration.ignore_sysex,
|
||||
.ignore_timing = configuration.ignore_timing,
|
||||
.ignore_sensing = configuration.ignore_sensing,
|
||||
|
||||
.timestamps = configuration.timestamps},
|
||||
context.in);
|
||||
|
||||
res.first->second.open_port(port, name);
|
||||
}
|
||||
|
||||
void add_output(const output_port& port, std::string_view name)
|
||||
{
|
||||
if (m_outputs.find(port) != m_outputs.end())
|
||||
return;
|
||||
|
||||
auto res = m_outputs.try_emplace(
|
||||
port,
|
||||
output_configuration{
|
||||
.on_error = configuration.on_error,
|
||||
.on_warning = configuration.on_warning,
|
||||
|
||||
.timestamps = configuration.timestamps},
|
||||
context.out);
|
||||
|
||||
res.first->second.open_port(port, name);
|
||||
}
|
||||
|
||||
void remove_input(const input_port& port) { m_inputs.erase(port); }
|
||||
void remove_output(const output_port& port) { m_outputs.erase(port); }
|
||||
|
||||
void send_message(const unsigned char* message, size_t size)
|
||||
{
|
||||
for (auto& [_, out] : m_outputs)
|
||||
{
|
||||
out.send_message(message, size);
|
||||
}
|
||||
}
|
||||
|
||||
void send_ump(const uint32_t* message, size_t size)
|
||||
{
|
||||
for (auto& [_, out] : m_outputs)
|
||||
{
|
||||
out.send_ump(message, size);
|
||||
}
|
||||
}
|
||||
|
||||
void send_message(const output_port& port, const unsigned char* message, size_t size)
|
||||
{
|
||||
if (auto it = m_outputs.find(port); it != m_outputs.end())
|
||||
it->second.send_message(message, size);
|
||||
}
|
||||
|
||||
void send_ump(const output_port& port, const uint32_t* message, size_t size)
|
||||
{
|
||||
if (auto it = m_outputs.find(port); it != m_outputs.end())
|
||||
it->second.send_ump(message, size);
|
||||
}
|
||||
|
||||
private:
|
||||
client_configuration configuration;
|
||||
shared_configurations context;
|
||||
|
||||
std::map<input_port, midi_in> m_inputs;
|
||||
std::map<output_port, midi_out> m_outputs;
|
||||
|
||||
observer m_observer;
|
||||
};
|
||||
}
|
||||
#if defined(LIBREMIDI_HEADER_ONLY)
|
||||
#include "client.cpp"
|
||||
#endif
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(MSC_VER)
|
||||
#define NOMINMAX 1
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
#if defined(LIBREMIDI_EXPORTS)
|
||||
#if defined(_MSC_VER)
|
||||
#define LIBREMIDI_EXPORT __declspec(dllexport)
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
#define LIBREMIDI_EXPORT __attribute__((visibility("default")))
|
||||
#endif
|
||||
#else
|
||||
#define LIBREMIDI_EXPORT
|
||||
#endif
|
||||
|
||||
#define LIBREMIDI_VERSION "4.5.0"
|
||||
|
||||
#if defined(LIBREMIDI_USE_BOOST)
|
||||
#if !__has_include(<boost/container/small_vector.hpp>)
|
||||
#error \
|
||||
"Boost was used for building libremidi but is not found when using it. Add Boost to your include paths."
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_NO_BOOST)
|
||||
#error "Boost was used for building libremidi but LIBREMIDI_NO_BOOST is defined."
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if __has_include(<boost/container/small_vector.hpp>) && !defined(LIBREMIDI_NO_BOOST)
|
||||
|
||||
#if LIBREMIDI_SLIM_MESSAGE > 0
|
||||
#include <boost/container/static_vector.hpp>
|
||||
namespace libremidi
|
||||
{
|
||||
using midi_bytes = boost::container::static_vector<unsigned char, LIBREMIDI_SLIM_MESSAGE>;
|
||||
}
|
||||
#else
|
||||
#include <boost/container/small_vector.hpp>
|
||||
namespace libremidi
|
||||
{
|
||||
static constexpr int small_vector_minimum_size
|
||||
= sizeof(boost::container::small_vector<unsigned char, 1>);
|
||||
using midi_bytes = boost::container::small_vector<unsigned char, small_vector_minimum_size>;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
namespace libremidi
|
||||
{
|
||||
using midi_bytes = std::vector<unsigned char>;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if __has_include(<midi/universal_packet.h>) && defined(LIBREMIDI_USE_NI_MIDI2)
|
||||
#define LIBREMIDI_NI_MIDI2_COMPAT 1
|
||||
#endif
|
||||
|
||||
#if defined(LIBREMIDI_HEADER_ONLY)
|
||||
#define LIBREMIDI_INLINE inline
|
||||
#else
|
||||
#define LIBREMIDI_INLINE
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <libremidi/backends/alsa_raw/config.hpp>
|
||||
#include <libremidi/backends/alsa_raw_ump/config.hpp>
|
||||
#include <libremidi/backends/alsa_seq/config.hpp>
|
||||
#include <libremidi/backends/alsa_seq_ump/config.hpp>
|
||||
#include <libremidi/backends/pipewire/config.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <libremidi/backends/coremidi/config.hpp>
|
||||
#include <libremidi/backends/coremidi_ump/config.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <libremidi/backends/winmidi/config.hpp>
|
||||
#include <libremidi/backends/winmm/config.hpp>
|
||||
#include <libremidi/backends/winuwp/config.hpp>
|
||||
#endif
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
#include <libremidi/backends/emscripten/config.hpp>
|
||||
#endif
|
||||
|
||||
#include <libremidi/backends/jack/config.hpp>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
|
||||
struct dummy_configuration
|
||||
{
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
#include <libremidi/api.hpp>
|
||||
#include <libremidi/config.hpp>
|
||||
#include <libremidi/input_configuration.hpp>
|
||||
#include <libremidi/observer_configuration.hpp>
|
||||
#include <libremidi/output_configuration.hpp>
|
||||
|
||||
#include <any>
|
||||
#include <optional>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
class midi_in;
|
||||
class midi_out;
|
||||
class observer;
|
||||
|
||||
//! Get the default midi in configuration object for a given API
|
||||
LIBREMIDI_EXPORT
|
||||
std::any midi_in_configuration_for(libremidi::API);
|
||||
|
||||
//! Get the default midi out configuration object for a given API
|
||||
LIBREMIDI_EXPORT
|
||||
std::any midi_out_configuration_for(libremidi::API);
|
||||
|
||||
//! Get the default observer configuration object for a given API
|
||||
LIBREMIDI_EXPORT
|
||||
std::any observer_configuration_for(libremidi::API);
|
||||
|
||||
//! Get a matching midi in configuration object for a given observer instance.
|
||||
LIBREMIDI_EXPORT
|
||||
std::any midi_in_configuration_for(const libremidi::observer&);
|
||||
//! Get a matching midi out configuration object for a given observer instance.
|
||||
LIBREMIDI_EXPORT
|
||||
std::any midi_out_configuration_for(const libremidi::observer&);
|
||||
|
||||
//! Get the default port for midi input (if any) in a given API.
|
||||
//! Note that this will only return a hardware / physical port.
|
||||
LIBREMIDI_EXPORT
|
||||
std::optional<input_port> in_default_port(libremidi::API api) noexcept;
|
||||
|
||||
//! Get the default port for midi output (if any) in a given API.
|
||||
//! Note that this will only return a hardware / physical port.
|
||||
LIBREMIDI_EXPORT
|
||||
std::optional<output_port> out_default_port(libremidi::API api) noexcept;
|
||||
|
||||
namespace midi1
|
||||
{
|
||||
//! Get a default configuration for creating a MIDI 1 input
|
||||
LIBREMIDI_EXPORT
|
||||
std::any in_default_configuration();
|
||||
|
||||
//! Get a default configuration for creating a MIDI 1 output
|
||||
LIBREMIDI_EXPORT
|
||||
std::any out_default_configuration();
|
||||
|
||||
//! Get a default configuration for creating a MIDI 1 observer
|
||||
LIBREMIDI_EXPORT
|
||||
std::any observer_default_configuration();
|
||||
|
||||
//! Get a default MIDI 1 input port for the default API of the system
|
||||
inline std::optional<input_port> in_default_port() noexcept
|
||||
{
|
||||
return libremidi::in_default_port(default_api());
|
||||
}
|
||||
|
||||
//! Get a default MIDI 1 output port for the default API of the system
|
||||
inline std::optional<output_port> out_default_port() noexcept
|
||||
{
|
||||
return libremidi::out_default_port(default_api());
|
||||
}
|
||||
}
|
||||
|
||||
namespace midi2
|
||||
{
|
||||
//! Get a default configuration for creating a MIDI 2 input
|
||||
LIBREMIDI_EXPORT
|
||||
std::any in_default_configuration();
|
||||
|
||||
//! Get a default configuration for creating a MIDI 2 output
|
||||
LIBREMIDI_EXPORT
|
||||
std::any out_default_configuration();
|
||||
|
||||
//! Get a default configuration for creating a MIDI 2 observer
|
||||
LIBREMIDI_EXPORT
|
||||
std::any observer_default_configuration();
|
||||
|
||||
//! Get a default MIDI 2 input port for the default API of the system
|
||||
inline std::optional<input_port> in_default_port() noexcept
|
||||
{
|
||||
return libremidi::in_default_port(default_api());
|
||||
}
|
||||
//! Get a default MIDI 2 output port for the default API of the system
|
||||
inline std::optional<output_port> out_default_port() noexcept
|
||||
{
|
||||
return libremidi::out_default_port(default_api());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
template <auto func>
|
||||
struct deleter
|
||||
{
|
||||
template <typename U>
|
||||
void operator()(U* x) noexcept(noexcept(func(x)))
|
||||
{
|
||||
func(x);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T, auto func>
|
||||
using unique_handle = std::unique_ptr<T, deleter<func>>;
|
||||
|
||||
template <typename T>
|
||||
std::shared_ptr<T> instance()
|
||||
{
|
||||
static std::mutex mut;
|
||||
static std::weak_ptr<T> cache;
|
||||
|
||||
std::lock_guard _{mut};
|
||||
|
||||
if (auto ptr = cache.lock())
|
||||
{
|
||||
return ptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto shared = std::make_shared<T>();
|
||||
cache = shared;
|
||||
return shared;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
#include <libremidi/api.hpp>
|
||||
#include <libremidi/config.hpp>
|
||||
#include <libremidi/error.hpp>
|
||||
#include <libremidi/observer_configuration.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
struct error_handler
|
||||
{
|
||||
//! Error reporting function for libremidi classes. Throws.
|
||||
template <typename Error_T>
|
||||
void error(auto& configuration, std::string_view errorString) const
|
||||
{
|
||||
if (configuration.on_error)
|
||||
{
|
||||
if (first_error)
|
||||
return;
|
||||
|
||||
first_error = true;
|
||||
configuration.on_error(Error_T::code, errorString);
|
||||
first_error = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(__LIBREMIDI_DEBUG__)
|
||||
std::cerr << '\n' << errorString << "\n\n";
|
||||
#endif
|
||||
throw Error_T{errorString.data()};
|
||||
}
|
||||
}
|
||||
|
||||
//! Warning reporting function for libremidi classes.
|
||||
void warning(auto& configuration, std::string_view errorString) const
|
||||
{
|
||||
if (configuration.on_warning)
|
||||
{
|
||||
if (first_warning)
|
||||
return;
|
||||
|
||||
first_warning = true;
|
||||
configuration.on_warning(midi_error::WARNING, errorString);
|
||||
first_warning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
#if defined(__LIBREMIDI_DEBUG__)
|
||||
std::cerr << '\n' << errorString << "\n\n";
|
||||
#endif
|
||||
}
|
||||
|
||||
// To prevent infinite error loops
|
||||
mutable bool first_error{};
|
||||
mutable bool first_warning{};
|
||||
};
|
||||
|
||||
class midi_api
|
||||
{
|
||||
public:
|
||||
midi_api() = default;
|
||||
virtual ~midi_api() = default;
|
||||
midi_api(const midi_api&) = delete;
|
||||
midi_api(midi_api&&) = delete;
|
||||
midi_api& operator=(const midi_api&) = delete;
|
||||
midi_api& operator=(midi_api&&) = delete;
|
||||
|
||||
[[nodiscard]] virtual libremidi::API get_current_api() const noexcept = 0;
|
||||
|
||||
[[nodiscard]] virtual bool open_virtual_port(std::string_view) = 0;
|
||||
|
||||
virtual void close_port() = 0;
|
||||
virtual void set_client_name(std::string_view) = 0;
|
||||
virtual void set_port_name(std::string_view) = 0;
|
||||
|
||||
bool is_port_open() const noexcept { return bool(port_open_); }
|
||||
bool is_port_connected() const noexcept { return bool(connected_); }
|
||||
|
||||
protected:
|
||||
friend class midi_in;
|
||||
friend class midi_out;
|
||||
bool port_open_{};
|
||||
bool connected_{};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
#include <libremidi/detail/midi_api.hpp>
|
||||
#include <libremidi/input_configuration.hpp>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
class midi_in_api : public midi_api
|
||||
{
|
||||
public:
|
||||
midi_in_api() = default;
|
||||
~midi_in_api() override = default;
|
||||
midi_in_api(const midi_in_api&) = delete;
|
||||
midi_in_api(midi_in_api&&) = delete;
|
||||
midi_in_api& operator=(const midi_in_api&) = delete;
|
||||
midi_in_api& operator=(midi_in_api&&) = delete;
|
||||
|
||||
[[nodiscard]] virtual bool open_port(const input_port& pt, std::string_view local_port_name) = 0;
|
||||
[[nodiscard]] virtual timestamp absolute_timestamp() const noexcept = 0;
|
||||
};
|
||||
|
||||
namespace midi1
|
||||
{
|
||||
class in_api : public midi_in_api
|
||||
{
|
||||
public:
|
||||
using midi_in_api::midi_in_api;
|
||||
};
|
||||
}
|
||||
|
||||
namespace midi2
|
||||
{
|
||||
class in_api : public midi_in_api
|
||||
{
|
||||
public:
|
||||
using midi_in_api::midi_in_api;
|
||||
|
||||
bool firstMessage{true};
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T, typename Arg>
|
||||
std::unique_ptr<midi_in_api> make(libremidi::input_configuration&& conf, Arg&& arg)
|
||||
{
|
||||
return std::make_unique<T>(std::move(conf), std::move(arg));
|
||||
}
|
||||
|
||||
template <typename T, typename Arg>
|
||||
std::unique_ptr<midi_in_api> make(libremidi::ump_input_configuration&& conf, Arg&& arg)
|
||||
{
|
||||
return std::make_unique<T>(std::move(conf), std::move(arg));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
#include <libremidi/cmidi2.hpp>
|
||||
#include <libremidi/detail/midi_api.hpp>
|
||||
#include <libremidi/output_configuration.hpp>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
|
||||
class midi_out_api : public midi_api
|
||||
{
|
||||
public:
|
||||
midi_out_api() = default;
|
||||
~midi_out_api() override = default;
|
||||
midi_out_api(const midi_out_api&) = delete;
|
||||
midi_out_api(midi_out_api&&) = delete;
|
||||
midi_out_api& operator=(const midi_out_api&) = delete;
|
||||
midi_out_api& operator=(midi_out_api&&) = delete;
|
||||
|
||||
[[nodiscard]] virtual bool open_port(const output_port& pt, std::string_view local_port_name)
|
||||
= 0;
|
||||
|
||||
[[nodiscard]] virtual int64_t current_time() const noexcept { return 0; }
|
||||
|
||||
virtual void send_message(const unsigned char* message, std::size_t size) = 0;
|
||||
virtual void schedule_message(int64_t /*ts*/, const unsigned char* message, std::size_t size)
|
||||
{
|
||||
return send_message(message, size);
|
||||
}
|
||||
|
||||
virtual void send_ump(const uint32_t* message, std::size_t size) = 0;
|
||||
virtual void schedule_ump(int64_t /*ts*/, const uint32_t* ump, std::size_t size)
|
||||
{
|
||||
return send_ump(ump, size);
|
||||
}
|
||||
};
|
||||
|
||||
namespace midi1
|
||||
{
|
||||
class out_api : public midi_out_api
|
||||
{
|
||||
friend struct midi_stream_decoder;
|
||||
|
||||
public:
|
||||
using midi_out_api::midi_out_api;
|
||||
|
||||
void send_ump(const uint32_t* message, std::size_t /*size*/)
|
||||
{
|
||||
uint8_t midi[65536];
|
||||
const auto n
|
||||
= cmidi2_convert_single_ump_to_midi1(midi, sizeof(midi), const_cast<uint32_t*>(message));
|
||||
if (n > 0)
|
||||
send_message(midi, n);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
namespace midi2
|
||||
{
|
||||
class out_api : public midi_out_api
|
||||
{
|
||||
friend struct midi_stream_decoder;
|
||||
|
||||
public:
|
||||
using midi_out_api::midi_out_api;
|
||||
|
||||
void send_message(const unsigned char* message, std::size_t size)
|
||||
{
|
||||
cmidi2_midi_conversion_context context{};
|
||||
cmidi2_midi_conversion_context_initialize(&context);
|
||||
|
||||
uint32_t ump[65536 / 4];
|
||||
|
||||
context.midi1 = const_cast<unsigned char*>(message);
|
||||
context.midi1_num_bytes = size;
|
||||
context.midi1_proceeded_bytes = 0;
|
||||
context.ump = ump;
|
||||
context.ump_num_bytes = sizeof(ump);
|
||||
context.ump_proceeded_bytes = 0;
|
||||
|
||||
if (auto res = cmidi2_convert_midi1_to_ump(&context); res != CMIDI2_CONVERSION_RESULT_OK)
|
||||
return;
|
||||
|
||||
send_ump(context.ump, context.ump_proceeded_bytes / 4);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T, typename Arg>
|
||||
std::unique_ptr<midi_out_api> make(libremidi::output_configuration&& conf, Arg&& arg)
|
||||
{
|
||||
return std::make_unique<T>(std::move(conf), std::forward<Arg>(arg));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
#pragma once
|
||||
|
||||
#include <libremidi/detail/midi_in.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cinttypes>
|
||||
#include <span>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
static inline int64_t system_ns() noexcept
|
||||
{
|
||||
namespace clk = std::chrono;
|
||||
return clk::duration_cast<clk::nanoseconds>(clk::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
|
||||
struct timestamp_backend_info
|
||||
{
|
||||
// The API provides some kind of timestamping
|
||||
bool has_absolute_timestamps{};
|
||||
|
||||
// The provided timestamping is equivalent or more precise than
|
||||
// e.g. clock_gettime(CLOCK_MONOTONIC)
|
||||
bool absolute_is_monotonic{};
|
||||
|
||||
// The API can provide samples in a buffer cycle (only PipeWire and JACK so far)
|
||||
bool has_samples{};
|
||||
};
|
||||
|
||||
namespace midi1
|
||||
{
|
||||
struct input_state_machine
|
||||
{
|
||||
const input_configuration& configuration;
|
||||
explicit input_state_machine(const input_configuration& conf)
|
||||
: configuration{conf}
|
||||
{
|
||||
}
|
||||
|
||||
bool has_finished_sysex(std::span<const uint8_t> bytes) const noexcept
|
||||
{
|
||||
return (((bytes.front() == 0xF0) || (state == in_sysex)) && (bytes.back() == 0xF7));
|
||||
}
|
||||
|
||||
// Function to process a byte stream which may contain multiple successive
|
||||
// MIDI events (CoreMIDI, ALSA Sequencer can work like this)
|
||||
void on_bytes_multi(std::span<const uint8_t> bytes, int64_t timestamp)
|
||||
{
|
||||
int64_t nBytes = bytes.size();
|
||||
int64_t iByte = 0;
|
||||
|
||||
const bool finished_sysex = has_finished_sysex(bytes);
|
||||
switch (state)
|
||||
{
|
||||
case in_sysex: {
|
||||
return on_continue_sysex(bytes, finished_sysex);
|
||||
}
|
||||
case main: {
|
||||
while (iByte < nBytes)
|
||||
{
|
||||
int64_t size = 1;
|
||||
// We are expecting that the next byte in the packet is a status
|
||||
// byte.
|
||||
const auto status = bytes[iByte];
|
||||
if (!(status & 0x80))
|
||||
break;
|
||||
|
||||
// Determine the number of bytes in the MIDI message.
|
||||
if (status < 0xC0)
|
||||
size = 3;
|
||||
else if (status < 0xE0)
|
||||
size = 2;
|
||||
else if (status < 0xF0)
|
||||
size = 3;
|
||||
else if (status == 0xF0)
|
||||
{
|
||||
if (configuration.ignore_sysex)
|
||||
{
|
||||
size = 0;
|
||||
iByte = nBytes;
|
||||
}
|
||||
else
|
||||
{
|
||||
size = nBytes - iByte;
|
||||
}
|
||||
|
||||
if (bytes[nBytes - 1] != 0xF7)
|
||||
{
|
||||
// We know per CoreMIDI API there can't be anything else in this packet
|
||||
state = in_sysex;
|
||||
message.assign(bytes.begin(), bytes.begin() + size);
|
||||
message.timestamp = timestamp;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (status == 0xF1)
|
||||
{
|
||||
// A MIDI time code message
|
||||
if (configuration.ignore_timing)
|
||||
{
|
||||
size = 0;
|
||||
iByte += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
size = 2;
|
||||
}
|
||||
}
|
||||
else if (status == 0xF2)
|
||||
size = 3;
|
||||
else if (status == 0xF3)
|
||||
size = 2;
|
||||
else if (status == 0xF8)
|
||||
{
|
||||
// A MIDI timing tick message
|
||||
if (configuration.ignore_timing)
|
||||
{
|
||||
size = 0;
|
||||
iByte += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
size = 1;
|
||||
}
|
||||
}
|
||||
else if (status == 0xFE)
|
||||
{
|
||||
// A MIDI active sensing message
|
||||
if (configuration.ignore_sensing)
|
||||
{
|
||||
size = 0;
|
||||
iByte += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
size = 1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remaining real-time messages
|
||||
size = 1;
|
||||
}
|
||||
|
||||
// Now process the actual bytes of the message
|
||||
if (size > 0)
|
||||
{
|
||||
auto begin = bytes.begin() + iByte;
|
||||
message.assign(begin, begin + size);
|
||||
message.timestamp = timestamp;
|
||||
|
||||
this->configuration.on_message(std::move(message));
|
||||
message.clear();
|
||||
|
||||
iByte += size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void on_continue_sysex(std::span<const uint8_t> bytes, bool finished_sysex)
|
||||
{
|
||||
if (finished_sysex)
|
||||
state = main;
|
||||
|
||||
if (configuration.ignore_sysex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
message.insert(message.end(), bytes.begin(), bytes.end());
|
||||
if (finished_sysex)
|
||||
{
|
||||
this->configuration.on_message(std::move(message));
|
||||
message.clear();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void on_main(std::span<const uint8_t> bytes, int64_t timestamp, bool finished_sysex)
|
||||
{
|
||||
switch (bytes[0])
|
||||
{
|
||||
// SYSEX start
|
||||
case 0xF0: {
|
||||
if (!finished_sysex)
|
||||
state = in_sysex;
|
||||
|
||||
if (!this->configuration.ignore_sysex)
|
||||
{
|
||||
message.assign(bytes.begin(), bytes.end());
|
||||
message.timestamp = timestamp;
|
||||
if (finished_sysex)
|
||||
{
|
||||
this->configuration.on_message(std::move(message));
|
||||
message.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
case 0xF1:
|
||||
case 0xF8:
|
||||
if (this->configuration.ignore_timing)
|
||||
return;
|
||||
break;
|
||||
|
||||
case 0xFE:
|
||||
if (this->configuration.ignore_sensing)
|
||||
return;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
message.assign(bytes.begin(), bytes.end());
|
||||
message.timestamp = timestamp;
|
||||
|
||||
this->configuration.on_message(std::move(message));
|
||||
message.clear();
|
||||
}
|
||||
|
||||
// Function to process bytes corresponding to at most one midi event
|
||||
// e.g. a midi channel event or a single sysex
|
||||
void on_bytes(std::span<const uint8_t> bytes, int64_t timestamp)
|
||||
{
|
||||
if (bytes.empty())
|
||||
return;
|
||||
|
||||
const bool finished_sysex = has_finished_sysex(bytes);
|
||||
switch (state)
|
||||
{
|
||||
case in_sysex:
|
||||
return on_continue_sysex(bytes, finished_sysex);
|
||||
|
||||
case main:
|
||||
return on_main(bytes, timestamp, finished_sysex);
|
||||
}
|
||||
}
|
||||
|
||||
template <timestamp_backend_info info>
|
||||
int64_t timestamp(auto to_ns, int64_t samples)
|
||||
{
|
||||
switch (configuration.timestamps)
|
||||
{
|
||||
default:
|
||||
case timestamp_mode::NoTimestamp:
|
||||
return 0;
|
||||
|
||||
case timestamp_mode::Relative: {
|
||||
int64_t time_ns;
|
||||
|
||||
if constexpr (info.has_absolute_timestamps)
|
||||
time_ns = to_ns();
|
||||
else
|
||||
time_ns = system_ns();
|
||||
|
||||
int64_t res;
|
||||
if (first_message)
|
||||
{
|
||||
first_message = false;
|
||||
res = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
res = time_ns - last_time_ns;
|
||||
}
|
||||
|
||||
last_time_ns = time_ns;
|
||||
return res;
|
||||
}
|
||||
|
||||
case timestamp_mode::Absolute:
|
||||
if constexpr (info.has_absolute_timestamps)
|
||||
return to_ns();
|
||||
else
|
||||
return system_ns();
|
||||
|
||||
case timestamp_mode::SystemMonotonic:
|
||||
if constexpr (info.absolute_is_monotonic)
|
||||
return to_ns();
|
||||
else
|
||||
return system_ns();
|
||||
|
||||
case timestamp_mode::AudioFrame:
|
||||
if constexpr (info.has_samples)
|
||||
return samples;
|
||||
else
|
||||
return 0;
|
||||
|
||||
case timestamp_mode::Custom:
|
||||
return configuration.get_timestamp(to_ns());
|
||||
}
|
||||
}
|
||||
|
||||
libremidi::message message;
|
||||
|
||||
int64_t last_time_ns = 0;
|
||||
enum
|
||||
{
|
||||
main,
|
||||
in_sysex
|
||||
} state{main};
|
||||
|
||||
bool first_message = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include <libremidi/api.hpp>
|
||||
#include <libremidi/config.hpp>
|
||||
#include <libremidi/observer_configuration.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace libremidi
|
||||
{
|
||||
class observer_api
|
||||
{
|
||||
public:
|
||||
virtual ~observer_api() = default;
|
||||
|
||||
virtual libremidi::API get_current_api() const noexcept = 0;
|
||||
virtual std::vector<libremidi::input_port> get_input_ports() const noexcept = 0;
|
||||
virtual std::vector<libremidi::output_port> get_output_ports() const noexcept = 0;
|
||||
};
|
||||
|
||||
template <typename T, typename Arg>
|
||||
std::unique_ptr<observer_api> make(libremidi::observer_configuration&& conf, Arg&& arg)
|
||||
{
|
||||
return std::make_unique<T>(std::move(conf), std::move(arg));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <semaphore>
|
||||
|
||||
struct semaphore_pair_lock
|
||||
{
|
||||
std::binary_semaphore sem_cleanup{0};
|
||||
std::binary_semaphore sem_needpost{0};
|
||||
void prepare_release_client()
|
||||
{
|
||||
using namespace std::literals;
|
||||
|
||||
// FIXME if jack is not running we can skip this
|
||||
this->sem_needpost.release();
|
||||
this->sem_cleanup.try_acquire_for(1s);
|
||||
}
|
||||
|
||||
void check_client_released()
|
||||
{
|
||||
if (!this->sem_needpost.try_acquire())
|
||||
this->sem_cleanup.release();
|
||||
}
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user