very, very simple WP with midi CC out
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
build/**
|
||||||
|
.cache
|
||||||
|
viseq
|
||||||
|
compile_commands.json
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
CXX = g++-13
|
||||||
|
CXXFLAGS = -std=c++20 -Wall -g -fno-omit-frame-pointer -gdwarf-4 -Og -DLIBREMIDI_ALSA=1
|
||||||
|
CXXINC = -I/usr/lib/gcc/x86_64-pc-linux-gnu/13.3.0/include -Iinclude
|
||||||
|
|
||||||
|
LDLIBS = -lsfml-graphics -lsfml-window -lsfml-system
|
||||||
|
LDFLAGS = -L/usr/lib -L/usr/local/lib
|
||||||
|
|
||||||
|
hdr_files = ${wildcard src/*.h}
|
||||||
|
|
||||||
|
src_files = ${wildcard src/*.cpp}
|
||||||
|
src_files += ${wildcard include/libremidi/*.cpp}
|
||||||
|
|
||||||
|
all: viseq
|
||||||
|
|
||||||
|
viseq: $(src_files) $(hdr_files)
|
||||||
|
mkdir -p build
|
||||||
|
$(CXX) ${wildcard build/*.o} -o viseq $(LDFLAGS) $(LDLIBS)
|
||||||
|
|
||||||
|
$(src_files): %.cpp : %_src
|
||||||
|
$(CXX) -c $@ -o build/$(*F).o $(CXXFLAGS) $(CXXINC)
|
||||||
|
|
||||||
|
$(hdr_files): %.h : %_hdr
|
||||||
|
@echo "G++ >---------- $(*F).h -----------<"
|
||||||
|
|
||||||
|
%_src:
|
||||||
|
@echo "G++ >---------- $(*F).cpp -----------<"
|
||||||
|
|
||||||
|
%_hdr:
|
||||||
|
@echo "G++ >---------- $(*F).h -----------<"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f build/*
|
||||||
|
rm -f viseq
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user