commit 3d6fc151f1643e45f82eda3cd8c202eddea3ab2f Author: Sebastian Date: Wed Oct 9 02:01:23 2024 +0200 very, very simple WP with midi CC out diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d19db64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build/** +.cache +viseq +compile_commands.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..25096c6 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/include/libremidi/api.hpp b/include/libremidi/api.hpp new file mode 100644 index 0000000..7dc45ff --- /dev/null +++ b/include/libremidi/api.hpp @@ -0,0 +1,100 @@ +#pragma once +#include + +#include +#include + +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 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 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 +} +} +} diff --git a/include/libremidi/backends.hpp b/include/libremidi/backends.hpp new file mode 100644 index 0000000..3738d63 --- /dev/null +++ b/include/libremidi/backends.hpp @@ -0,0 +1,187 @@ +#pragma once +#include + +#if !__has_include() && !__has_include() + #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 + #include + + #if LIBREMIDI_ALSA_HAS_RAMWIDI + #include + #endif + + #if LIBREMIDI_ALSA_HAS_UMP + #include + #include + #endif +#endif + +#if defined(LIBREMIDI_JACK) + #include +#endif + +#if defined(LIBREMIDI_PIPEWIRE) + #include +#endif + +#if defined(LIBREMIDI_COREMIDI) + #include + #include +#endif + +#if defined(LIBREMIDI_WINMM) + #include +#endif + +#if defined(LIBREMIDI_WINUWP) + #include + #if __has_include() + #include + #endif +#endif + +#if defined(LIBREMIDI_EMSCRIPTEN) + #include +#endif + +#include + +namespace libremidi +{ +// The order here will control the order of the API search in +// the constructor. +template +constexpr auto make_tl(unused, Args...) +{ + return std::tuple{}; +} + +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 >= 1); + +template +auto for_all_backends(F&& f) +{ + std::apply([&](auto&&... x) { ((x.available() && (f(x), true)), ...); }, available_backends); +} + +template +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() + , + 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 >= 1); + +template +auto for_all_backends(F&& f) +{ + std::apply([&](auto&&... x) { (f(x), ...); }, available_backends); +} + +template +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 +auto for_all_backends(F&& f) +{ + midi1::for_all_backends(f); + midi2::for_all_backends(f); +} + +template +auto for_backend(libremidi::API api, F&& f) +{ + midi1::for_backend(api, f); + midi2::for_backend(api, f); +} + +} +} diff --git a/include/libremidi/backends/alsa_raw.hpp b/include/libremidi/backends/alsa_raw.hpp new file mode 100644 index 0000000..06556b6 --- /dev/null +++ b/include/libremidi/backends/alsa_raw.hpp @@ -0,0 +1,31 @@ +#pragma once +#include +#include +#include + +// 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 + +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; + } +}; +} diff --git a/include/libremidi/backends/alsa_raw/config.hpp b/include/libremidi/backends/alsa_raw/config.hpp new file mode 100644 index 0000000..afe62eb --- /dev/null +++ b/include/libremidi/backends/alsa_raw/config.hpp @@ -0,0 +1,79 @@ +#pragma once +#include + +#include +#include +#include +#include +#include + +#if __has_include() + #include +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 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 fds; + std::function fds)> callback; +}; + +struct alsa_raw_input_configuration +{ + std::function 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; +}; + +struct alsa_raw_observer_configuration +{ + std::chrono::milliseconds poll_period{100}; +}; +} diff --git a/include/libremidi/backends/alsa_raw/helpers.hpp b/include/libremidi/backends/alsa_raw/helpers.hpp new file mode 100644 index 0000000..7aa23b7 --- /dev/null +++ b/include/libremidi/backends/alsa_raw/helpers.hpp @@ -0,0 +1,328 @@ +#pragma once +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 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 + +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 inputs; + std::vector outputs; + + std::function error_callback; + std::function warn_callback; + + template + void warning(Args&&... args) + { + std::string s; + ((s += args), ...); + if (warn_callback) + { + warn_callback(std::move(s)); + } + else + { + std::cerr << s << std::endl; + } + } + template + 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); + } + } + } +}; +} +} diff --git a/include/libremidi/backends/alsa_raw/midi_in.hpp b/include/libremidi/backends/alsa_raw/midi_in.hpp new file mode 100644 index 0000000..ec9018d --- /dev/null +++ b/include/libremidi/backends/alsa_raw/midi_in.hpp @@ -0,0 +1,350 @@ +#pragma once +#include +#include +#include +#include +#include + +#include + +#include +#include + +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(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 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(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(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(ts.tv_sec) * 1'000'000'000 + static_cast(ts.tv_nsec); + }; + decoder_.on_bytes({bytes, bytes + err}, decoder_.timestamp(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 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( + 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( + 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 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 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 make( + libremidi::input_configuration&& conf, libremidi::alsa_raw_input_configuration&& api) +{ + if (api.manual_poll) + return std::make_unique(std::move(conf), std::move(api)); + else + return std::make_unique(std::move(conf), std::move(api)); +} +} diff --git a/include/libremidi/backends/alsa_raw/midi_out.hpp b/include/libremidi/backends/alsa_raw/midi_out.hpp new file mode 100644 index 0000000..0165f21 --- /dev/null +++ b/include/libremidi/backends/alsa_raw/midi_out.hpp @@ -0,0 +1,177 @@ +#pragma once +#include +#include +#include + +#include + +#include +#include + +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( + 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( + 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( + 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(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_{}; +}; +} diff --git a/include/libremidi/backends/alsa_raw/observer.hpp b/include/libremidi/backends/alsa_raw/observer.hpp new file mode 100644 index 0000000..460a767 --- /dev/null +++ b/include/libremidi/backends/alsa_raw/observer.hpp @@ -0,0 +1,240 @@ +#pragma once +#include +#include +#include + +#if LIBREMIDI_HAS_UDEV + #include + #include + #include + + #include + +namespace libremidi::alsa_raw +{ +template +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 get_input_ports() const noexcept override + { + std::vector ret; + Enumerator new_devs; + + new_devs.enumerate_cards(); + for (auto& d : new_devs.inputs) + { + ret.push_back(to_port_info(d)); + } + return ret; + } + + std::vector get_output_ports() const noexcept override + { + std::vector ret; + Enumerator new_devs; + + new_devs.enumerate_cards(); + for (auto& d : new_devs.outputs) + { + ret.push_back(to_port_info(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 + auto to_port_info(alsa_raw::alsa_raw_port_info p) const noexcept + -> std::conditional_t + { + 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(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(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(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(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 current_inputs; + std::vector current_outputs; + + pollfd fds[3]{}; +}; +} +#else + #include +namespace libremidi::alsa_raw +{ +template +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 +{ + using alsa_raw::observer_impl_base::observer_impl_base; + libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW; } +}; +} diff --git a/include/libremidi/backends/alsa_raw_ump.hpp b/include/libremidi/backends/alsa_raw_ump.hpp new file mode 100644 index 0000000..3fc7adf --- /dev/null +++ b/include/libremidi/backends/alsa_raw_ump.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include +#include +#include + +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; + } +}; +} diff --git a/include/libremidi/backends/alsa_raw_ump/config.hpp b/include/libremidi/backends/alsa_raw_ump/config.hpp new file mode 100644 index 0000000..6165eba --- /dev/null +++ b/include/libremidi/backends/alsa_raw_ump/config.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +namespace libremidi::alsa_raw_ump +{ +struct input_configuration +{ + std::function manual_poll; +}; + +struct output_configuration +{ + /** + * For large messages, chunk their content and wait. + * Setting a null optional will disable chunking. + */ + std::optional chunking; +}; + +struct observer_configuration +{ + std::chrono::milliseconds poll_period{100}; +}; +} diff --git a/include/libremidi/backends/alsa_raw_ump/helpers.hpp b/include/libremidi/backends/alsa_raw_ump/helpers.hpp new file mode 100644 index 0000000..c7af4d7 --- /dev/null +++ b/include/libremidi/backends/alsa_raw_ump/helpers.hpp @@ -0,0 +1,66 @@ +#pragma once +#include +#include +#include + +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)); + } +}; + +} diff --git a/include/libremidi/backends/alsa_raw_ump/midi_in.hpp b/include/libremidi/backends/alsa_raw_ump/midi_in.hpp new file mode 100644 index 0000000..9bc8fb5 --- /dev/null +++ b/include/libremidi/backends/alsa_raw_ump/midi_in.hpp @@ -0,0 +1,383 @@ +#pragma once +#include +#include +#include +#include +#include + +#include + +#include +#include + +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( + 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 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(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(ts.tv_sec) * nanos + static_cast(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(ts.tv_sec) * nanos + static_cast(ts.tv_nsec); + break; + case timestamp_mode::Custom: + res = configuration.get_timestamp( + static_cast(ts.tv_sec) * nanos + static_cast(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(this->configuration, text); }; + return device_list; + } + + snd_ump_t* midiport_{}; + std::vector 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( + 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( + 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 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 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 make( + libremidi::ump_input_configuration&& conf, libremidi::alsa_raw_ump::input_configuration&& api) +{ + if (api.manual_poll) + return std::make_unique(std::move(conf), std::move(api)); + else + return std::make_unique(std::move(conf), std::move(api)); +} +} diff --git a/include/libremidi/backends/alsa_raw_ump/midi_out.hpp b/include/libremidi/backends/alsa_raw_ump/midi_out.hpp new file mode 100644 index 0000000..a43d694 --- /dev/null +++ b/include/libremidi/backends/alsa_raw_ump/midi_out.hpp @@ -0,0 +1,107 @@ +#pragma once +#include +#include +#include + +#include + +#include +#include + +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( + 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( + 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( + this->configuration, "midi_out_alsa_raw::send_message: cannot write message."); + return false; + } + + return true; + } + + snd_ump_t* midiport_{}; +}; + +} diff --git a/include/libremidi/backends/alsa_raw_ump/observer.hpp b/include/libremidi/backends/alsa_raw_ump/observer.hpp new file mode 100644 index 0000000..03e3286 --- /dev/null +++ b/include/libremidi/backends/alsa_raw_ump/observer.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include + +namespace libremidi::alsa_raw_ump +{ + +class observer_impl : public alsa_raw::observer_impl_base +{ + using alsa_raw::observer_impl_base::observer_impl_base; + libremidi::API get_current_api() const noexcept override { return libremidi::API::ALSA_RAW_UMP; } +}; + +} diff --git a/include/libremidi/backends/alsa_seq.hpp b/include/libremidi/backends/alsa_seq.hpp new file mode 100644 index 0000000..0d2c43b --- /dev/null +++ b/include/libremidi/backends/alsa_seq.hpp @@ -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 +#include +#include + +namespace libremidi::alsa_seq +{ + +struct backend +{ + using midi_in + = alsa_seq::midi_in_impl; + using midi_out = alsa_seq::midi_out_impl; + using midi_observer = alsa_seq::observer_impl; + 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; + } +}; + +} diff --git a/include/libremidi/backends/alsa_seq/config.hpp b/include/libremidi/backends/alsa_seq/config.hpp new file mode 100644 index 0000000..b0fa758 --- /dev/null +++ b/include/libremidi/backends/alsa_seq/config.hpp @@ -0,0 +1,63 @@ +#pragma once +#include + +#if __has_include() + #include +#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 callback; +}; + +struct input_configuration +{ + using poll_parameters_type = poll_parameters; + + std::string client_name = "libremidi client"; + snd_seq_t* context{}; + std::function manual_poll; + std::function 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 manual_poll; + std::function stop_poll; + + static constexpr int midi_version = 1; +}; + +} diff --git a/include/libremidi/backends/alsa_seq/helpers.hpp b/include/libremidi/backends/alsa_seq/helpers.hpp new file mode 100644 index 0000000..05f8ca4 --- /dev/null +++ b/include/libremidi/backends/alsa_seq/helpers.hpp @@ -0,0 +1,306 @@ +#pragma once +#include +#include +#include + +#include + +#include +#include +#include + +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 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 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() + 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 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( + // 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 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( + 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; + } + } +}; +} diff --git a/include/libremidi/backends/alsa_seq/midi_in.hpp b/include/libremidi/backends/alsa_seq/midi_in.hpp new file mode 100644 index 0000000..1855aec --- /dev/null +++ b/include/libremidi/backends/alsa_seq/midi_in.hpp @@ -0,0 +1,575 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace libremidi::alsa_seq +{ +struct dummy_processing +{ + explicit dummy_processing(auto&&...) { } +}; + +template +using midi_in_base + = std::conditional_t; +template +using midi_in_processing = std::conditional_t< + ConfigurationImpl::midi_version == 1, midi1::input_state_machine, dummy_processing>; + +template +class midi_in_impl + : public midi_in_base + , protected alsa_data + , public error_handler +{ +public: + struct + : ConfigurationBase + , ConfigurationImpl + { + } configuration; + midi_in_processing 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{} + , configuration{std::move(conf), std::move(apiconf)} + { + if (init_client(configuration) < 0) + { + 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( + 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{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 to_address(const port_information& p) + { + return alsa_data::get_port_info(p); + } + + int init_port(std::optional source, std::string_view portName) + { + this->close_port(); + + if (!source) + return -1; + + if (int ret = create_port(portName); ret < 0) + { + error(configuration, "midi_in_alsa::create_port: ALSA error creating port."); + return ret; + } + + if (int ret = connect_port(*source); ret < 0) + { + 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(ev.time.time.tv_sec) * nanos + + static_cast(ev.time.time.tv_nsec); + const auto t0 = static_cast(last_time.tv_sec) * nanos + + static_cast(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::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(ts.tv_sec) * 1'000'000'000 + static_cast(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(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() + 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 decoding_buffer = std::vector(4096); + std::chrono::steady_clock::time_point queue_creation_time; +}; + +template +class midi_in_alsa_threaded : public midi_in_impl +{ +public: + midi_in_alsa_threaded(ConfigurationBase&& conf, ConfigurationImpl&& apiconf) + : midi_in_impl{std::move(conf), std::move(apiconf)} + { + if (this->termination_event < 0) + { + this->template 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::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( + 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() + 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 +class midi_in_alsa_manual : public midi_in_impl +{ +public: + midi_in_alsa_manual(ConfigurationBase&& conf, ConfigurationImpl&& apiconf) + : midi_in_impl{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() + 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::close_port(); + } +}; +} + +namespace libremidi +{ +template <> +inline std::unique_ptr +make>( + libremidi::input_configuration&& conf, libremidi::alsa_seq::input_configuration&& api) +{ + if (api.manual_poll) + return std::make_unique>( + std::move(conf), std::move(api)); + else + return std::make_unique>( + std::move(conf), std::move(api)); +} +} diff --git a/include/libremidi/backends/alsa_seq/midi_out.hpp b/include/libremidi/backends/alsa_seq/midi_out.hpp new file mode 100644 index 0000000..147aaa5 --- /dev/null +++ b/include/libremidi/backends/alsa_seq/midi_out.hpp @@ -0,0 +1,174 @@ +#pragma once +#include +#include +#include + +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( + this->configuration, + "midi_in_alsa::initialize: error creating ALSA sequencer client " + "object."); + return; + } + + if (snd.midi.event_new(this->bufferSize, &this->coder) < 0) + { + 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( + 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(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( + 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( + 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}; +}; +} diff --git a/include/libremidi/backends/alsa_seq/observer.hpp b/include/libremidi/backends/alsa_seq/observer.hpp new file mode 100644 index 0000000..6722a12 --- /dev/null +++ b/include/libremidi/backends/alsa_seq/observer.hpp @@ -0,0 +1,340 @@ +#pragma once +#include +#include +#include +#include + +#include + +#include +#include + +namespace libremidi::alsa_seq +{ + +struct port_info +{ + std::string client_name; + std::string port_name; + int client{}; + int port{}; + bool isInput{}; + bool isOutput{}; +}; + +template +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() + 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 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 + auto to_port_info(port_info p) const noexcept + -> std::conditional_t + { + 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 get_input_ports() const noexcept override + { + std::vector 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(*p)); + }); + return ret; + } + + std::vector get_output_ports() const noexcept override + { + std::vector 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(*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(p)); + } + + if (p.isOutput && configuration.output_added) + { + configuration.output_added(to_port_info(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(p)); + } + + if (p.isOutput && configuration.output_removed) + { + configuration.output_removed(to_port_info(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, port_info> knownClients_; +}; + +template +class observer_threaded : public observer_impl +{ +public: + observer_threaded(libremidi::observer_configuration&& conf, ConfigurationImpl&& apiconf) + : observer_impl{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 descriptors_; +}; + +template +class observer_manual : public observer_impl +{ +public: + observer_manual(libremidi::observer_configuration&& conf, ConfigurationImpl&& apiconf) + : observer_impl{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 +make>( + libremidi::observer_configuration&& conf, libremidi::alsa_seq::observer_configuration&& api) +{ + if (api.manual_poll) + return std::make_unique>( + std::move(conf), std::move(api)); + else + return std::make_unique>( + std::move(conf), std::move(api)); +} +} diff --git a/include/libremidi/backends/alsa_seq/shared_handler.hpp b/include/libremidi/backends/alsa_seq/shared_handler.hpp new file mode 100644 index 0000000..f9434a0 --- /dev/null +++ b/include/libremidi/backends/alsa_seq/shared_handler.hpp @@ -0,0 +1,197 @@ +#pragma once +#if __has_include() + #include + #include + #include + #include + + #include + + #include + +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(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(&ev.payload)); + addresses.push_back(addr); + callbacks.push_back(std::move(cb)); + break; + } + case callback_removed: + auto addr = std::move(*std::get_if(&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 payload; + }; + + snd_seq_t* client{}; + + boost::lockfree::spsc_queue events{16}; + std::vector addresses; + std::vector> callbacks; + std::vector fds; + eventfd_notifier termination_event, queue_event{false}; + std::thread thread; +}; +} +#endif diff --git a/include/libremidi/backends/alsa_seq_ump.hpp b/include/libremidi/backends/alsa_seq_ump.hpp new file mode 100644 index 0000000..ae91f79 --- /dev/null +++ b/include/libremidi/backends/alsa_seq_ump.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include + +namespace libremidi +{ +template <> +inline std::unique_ptr +make>( + libremidi::observer_configuration&& conf, + libremidi::alsa_seq_ump::observer_configuration&& api) +{ + if (api.manual_poll) + return std::make_unique>( + std::move(conf), std::move(api)); + else + return std::make_unique>( + std::move(conf), std::move(api)); +} + +template <> +inline std::unique_ptr make< + alsa_seq::midi_in_impl>( + libremidi::ump_input_configuration&& conf, libremidi::alsa_seq_ump::input_configuration&& api) +{ + if (api.manual_poll) + return std::make_unique>( + std::move(conf), std::move(api)); + else + return std::make_unique>( + 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; + 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; + } +}; +} diff --git a/include/libremidi/backends/alsa_seq_ump/config.hpp b/include/libremidi/backends/alsa_seq_ump/config.hpp new file mode 100644 index 0000000..4ad4242 --- /dev/null +++ b/include/libremidi/backends/alsa_seq_ump/config.hpp @@ -0,0 +1,53 @@ +#pragma once +#include + +#if __has_include() + #include +#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 callback; +}; + +struct input_configuration +{ + using poll_parameters_type = poll_parameters; + + std::string client_name = "libremidi client"; + snd_seq_t* context{}; + std::function manual_poll; + std::function 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 manual_poll; + std::function stop_poll; + + static constexpr int midi_version = 2; +}; + +} diff --git a/include/libremidi/backends/alsa_seq_ump/helpers.hpp b/include/libremidi/backends/alsa_seq_ump/helpers.hpp new file mode 100644 index 0000000..e69de29 diff --git a/include/libremidi/backends/alsa_seq_ump/midi_out.hpp b/include/libremidi/backends/alsa_seq_ump/midi_out.hpp new file mode 100644 index 0000000..87c1b39 --- /dev/null +++ b/include/libremidi/backends/alsa_seq_ump/midi_out.hpp @@ -0,0 +1,151 @@ +#pragma once +#include +#include +#include +#include +#include + +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( + this->configuration, + "midi_in_alsa::initialize: error creating ALSA sequencer client " + "object."); + return; + } + + if (snd.midi.event_new(this->bufferSize, &this->coder) < 0) + { + 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( + 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(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( + 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}; +}; +} diff --git a/include/libremidi/backends/coremidi.hpp b/include/libremidi/backends/coremidi.hpp new file mode 100644 index 0000000..e6f2ee4 --- /dev/null +++ b/include/libremidi/backends/coremidi.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include + +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; } +}; +} diff --git a/include/libremidi/backends/coremidi/config.hpp b/include/libremidi/backends/coremidi/config.hpp new file mode 100644 index 0000000..f126f83 --- /dev/null +++ b/include/libremidi/backends/coremidi/config.hpp @@ -0,0 +1,41 @@ +#pragma once +#include + +#include +#include +#include +#include + +#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 context{}; +}; + +struct coremidi_output_configuration +{ + std::string client_name = "libremidi client"; + std::optional context{}; +}; + +struct coremidi_observer_configuration +{ + std::string client_name = "libremidi client"; + std::function on_create_context{}; +}; + +} diff --git a/include/libremidi/backends/coremidi/helpers.hpp b/include/libremidi/backends/coremidi/helpers.hpp new file mode 100644 index 0000000..b3a53ad --- /dev/null +++ b/include/libremidi/backends/coremidi/helpers.hpp @@ -0,0 +1,324 @@ +#pragma once +#include +#include +#include + +#include +#include + +#include + +#include + +#if TARGET_OS_IPHONE + #include + #include + #define LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME mach_absolute_time +#else + #include + #define LIBREMIDI_AUDIO_GET_CURRENT_HOST_TIME AudioGetCurrentHostTime +#endif + +namespace libremidi +{ +using CFString_handle = unique_handle; +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(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((uint32_t)info.port); + MIDIObjectRef object{}; + MIDIObjectType type{}; + auto ret = MIDIObjectFindByUniqueID(uid, &object, &type); + if (ret != noErr) + { + self.template error( + self.configuration, "coremidi::locate_object: cannot find port: " + info.port_name); + return 0; + } + + if (type != requested_type || object == 0) + { + self.template 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; + } + } + } +}; + +} diff --git a/include/libremidi/backends/coremidi/midi_in.hpp b/include/libremidi/backends/coremidi/midi_in.hpp new file mode 100644 index 0000000..2207cbd --- /dev/null +++ b/include/libremidi/backends/coremidi/midi_in.hpp @@ -0,0 +1,178 @@ +#pragma once +#include +#include +#include +#include + +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( + 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( + 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( + 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( + 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(to_ns, 0)); + + packet = MIDIPacketNext(packet); + } + } + + midi1::input_state_machine m_processing{this->configuration}; +}; +} diff --git a/include/libremidi/backends/coremidi/midi_out.hpp b/include/libremidi/backends/coremidi/midi_out.hpp new file mode 100644 index 0000000..a9f139a --- /dev/null +++ b/include/libremidi/backends/coremidi/midi_out.hpp @@ -0,0 +1,203 @@ +#pragma once +#include +#include +#include + +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( + 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( + 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( + 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(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( + 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{}; +}; +} diff --git a/include/libremidi/backends/coremidi/observer.hpp b/include/libremidi/backends/coremidi/observer.hpp new file mode 100644 index 0000000..defe623 --- /dev/null +++ b/include/libremidi/backends/coremidi/observer.hpp @@ -0,0 +1,175 @@ +#pragma once +#include +#include +#include + +#include + +#include + +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( + 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 + auto to_port_info(MIDIObjectRef obj) const noexcept + -> std::optional> + { + 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{ + {.client = (std::uintptr_t)this->client, + .port = std::bit_cast(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 get_input_ports() const noexcept override + { + std::vector ret; + + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false); + for (ItemCount i = 0; i < MIDIGetNumberOfSources(); i++) + { + if (auto p = to_port_info(MIDIGetSource(i))) + ret.push_back(std::move(*p)); + } + + return ret; + } + + std::vector get_output_ports() const noexcept override + { + std::vector ret; + + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false); + for (ItemCount i = 0; i < MIDIGetNumberOfDestinations(); i++) + { + if (auto p = to_port_info(MIDIGetDestination(i))) + ret.push_back(std::move(*p)); + } + return ret; + } + + void notify(const MIDINotification* message) + { + switch (message->messageID) + { + case kMIDIMsgObjectAdded: { + auto obj = reinterpret_cast(message); + + switch (obj->childType) + { + case kMIDIObjectType_Source: + if (auto& cb = configuration.input_added) + if (auto p = to_port_info(obj->child)) + cb(std::move(*p)); + break; + case kMIDIObjectType_Destination: + if (auto& cb = configuration.output_added) + if (auto p = to_port_info(obj->child)) + cb(std::move(*p)); + break; + default: + break; + } + + break; + } + + case kMIDIMsgObjectRemoved: { + auto obj = reinterpret_cast(message); + + switch (obj->childType) + { + case kMIDIObjectType_Source: + if (auto& cb = configuration.input_removed) + if (auto p = to_port_info(obj->child)) + cb(std::move(*p)); + break; + case kMIDIObjectType_Destination: + if (auto& cb = configuration.output_removed) + if (auto p = to_port_info(obj->child)) + cb(std::move(*p)); + break; + default: + break; + } + + break; + } + + default: + break; + } + } + + ~observer_core() { MIDIClientDispose(this->client); } + +private: + MIDIClientRef client{}; +}; +} diff --git a/include/libremidi/backends/coremidi_ump.hpp b/include/libremidi/backends/coremidi_ump.hpp new file mode 100644 index 0000000..f6a6af2 --- /dev/null +++ b/include/libremidi/backends/coremidi_ump.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include + +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? */ } +}; +} diff --git a/include/libremidi/backends/coremidi_ump/config.hpp b/include/libremidi/backends/coremidi_ump/config.hpp new file mode 100644 index 0000000..ff49822 --- /dev/null +++ b/include/libremidi/backends/coremidi_ump/config.hpp @@ -0,0 +1,9 @@ +#pragma once +#include + +namespace libremidi::coremidi_ump +{ +using input_configuration = coremidi_input_configuration; +using output_configuration = coremidi_output_configuration; +using observer_configuration = coremidi_observer_configuration; +} diff --git a/include/libremidi/backends/coremidi_ump/helpers.hpp b/include/libremidi/backends/coremidi_ump/helpers.hpp new file mode 100644 index 0000000..46e1325 --- /dev/null +++ b/include/libremidi/backends/coremidi_ump/helpers.hpp @@ -0,0 +1,2 @@ +#pragma once +#include diff --git a/include/libremidi/backends/coremidi_ump/midi_in.hpp b/include/libremidi/backends/coremidi_ump/midi_in.hpp new file mode 100644 index 0000000..a90a58c --- /dev/null +++ b/include/libremidi/backends/coremidi_ump/midi_in.hpp @@ -0,0 +1,183 @@ +#pragma once +#include +#include +#include + +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( + 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( + 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( + 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( + 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{}; +}; + +} diff --git a/include/libremidi/backends/coremidi_ump/midi_out.hpp b/include/libremidi/backends/coremidi_ump/midi_out.hpp new file mode 100644 index 0000000..0431584 --- /dev/null +++ b/include/libremidi/backends/coremidi_ump/midi_out.hpp @@ -0,0 +1,185 @@ +#pragma once +#include +#include +#include +#include +#include + +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( + 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( + 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( + 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(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]; +}; +} diff --git a/include/libremidi/backends/coremidi_ump/observer.hpp b/include/libremidi/backends/coremidi_ump/observer.hpp new file mode 100644 index 0000000..5a28ce0 --- /dev/null +++ b/include/libremidi/backends/coremidi_ump/observer.hpp @@ -0,0 +1,13 @@ +#pragma once +#include + +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; } +}; + +} diff --git a/include/libremidi/backends/dummy.hpp b/include/libremidi/backends/dummy.hpp new file mode 100644 index 0000000..7527a89 --- /dev/null +++ b/include/libremidi/backends/dummy.hpp @@ -0,0 +1,80 @@ +#pragma once +#include +#include +#include +#include +#include + +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 get_input_ports() const noexcept override { return {}; } + std::vector 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; } +}; +} diff --git a/include/libremidi/backends/emscripten.hpp b/include/libremidi/backends/emscripten.hpp new file mode 100644 index 0000000..867e943 --- /dev/null +++ b/include/libremidi/backends/emscripten.hpp @@ -0,0 +1,30 @@ +#pragma once + +#if defined(__EMSCRIPTEN__) + + #include + #include + #include + #include + +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 diff --git a/include/libremidi/backends/emscripten/config.hpp b/include/libremidi/backends/emscripten/config.hpp new file mode 100644 index 0000000..6a1d735 --- /dev/null +++ b/include/libremidi/backends/emscripten/config.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +namespace libremidi +{ + +struct emscripten_input_configuration +{ +}; +struct emscripten_output_configuration +{ +}; +struct emscripten_observer_configuration +{ +}; + +} diff --git a/include/libremidi/backends/emscripten/helpers.hpp b/include/libremidi/backends/emscripten/helpers.hpp new file mode 100644 index 0000000..3064ae1 --- /dev/null +++ b/include/libremidi/backends/emscripten/helpers.hpp @@ -0,0 +1,17 @@ +#pragma once +#include + +#include + +namespace libremidi +{ +namespace webmidi_helpers +{ +struct device_information +{ + std::string id; + std::string name; + bool connected{}; +}; +} +} diff --git a/include/libremidi/backends/emscripten/midi_access.cpp b/include/libremidi/backends/emscripten/midi_access.cpp new file mode 100644 index 0000000..3c20bd4 --- /dev/null +++ b/include/libremidi/backends/emscripten/midi_access.cpp @@ -0,0 +1,22 @@ +#if defined(__EMSCRIPTEN__) + #include + +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 diff --git a/include/libremidi/backends/emscripten/midi_access.hpp b/include/libremidi/backends/emscripten/midi_access.hpp new file mode 100644 index 0000000..90de5ef --- /dev/null +++ b/include/libremidi/backends/emscripten/midi_access.hpp @@ -0,0 +1,303 @@ +#pragma once +#include +#include +#include +#include + +#include + +#include + +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(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& inputs() const noexcept { return m_current_inputs; } + const std::vector& 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 m_observers; + std::vector m_current_inputs; + std::vector m_current_outputs; + + std::map> m_opened_inputs; + + std::map m_input_indices; + std::map m_output_indices; +}; + +} +} diff --git a/include/libremidi/backends/emscripten/midi_in.cpp b/include/libremidi/backends/emscripten/midi_in.cpp new file mode 100644 index 0000000..0cb6746 --- /dev/null +++ b/include/libremidi/backends/emscripten/midi_in.cpp @@ -0,0 +1,91 @@ +#if defined(__EMSCRIPTEN__) + #include + #include + #include + + #include + +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( + 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(to_ns, 0)); +} + +} +#endif diff --git a/include/libremidi/backends/emscripten/midi_in.hpp b/include/libremidi/backends/emscripten/midi_in.hpp new file mode 100644 index 0000000..32e7cd7 --- /dev/null +++ b/include/libremidi/backends/emscripten/midi_in.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include + +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}; +}; +} diff --git a/include/libremidi/backends/emscripten/midi_out.cpp b/include/libremidi/backends/emscripten/midi_out.cpp new file mode 100644 index 0000000..feaadd8 --- /dev/null +++ b/include/libremidi/backends/emscripten/midi_out.cpp @@ -0,0 +1,74 @@ +#if defined(__EMSCRIPTEN__) + #include + #include + +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( + 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( + 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(message), size); +} +} +#endif diff --git a/include/libremidi/backends/emscripten/midi_out.hpp b/include/libremidi/backends/emscripten/midi_out.hpp new file mode 100644 index 0000000..584e1cb --- /dev/null +++ b/include/libremidi/backends/emscripten/midi_out.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include + +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}; +}; +} diff --git a/include/libremidi/backends/emscripten/observer.cpp b/include/libremidi/backends/emscripten/observer.cpp new file mode 100644 index 0000000..9cdda2d --- /dev/null +++ b/include/libremidi/backends/emscripten/observer.cpp @@ -0,0 +1,105 @@ +#if defined(__EMSCRIPTEN__) + #include + #include + + #include + +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 +static auto to_port_info(int index, const webmidi_helpers::device_information& dev) + -> std::conditional_t +{ + return { + {.client = 0, + .port = (uint64_t)index, + .manufacturer = "", + .device_name = "", + .port_name = dev.name, + .display_name = dev.name}}; +} + +LIBREMIDI_INLINE +std::vector observer_emscripten::get_input_ports() const noexcept +{ + std::vector 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(i, webmidi.inputs()[i])); + } + + return ret; +} + +LIBREMIDI_INLINE +std::vector observer_emscripten::get_output_ports() const noexcept +{ + std::vector 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(i, webmidi.outputs()[i])); + } + + return ret; +} + +LIBREMIDI_INLINE void observer_emscripten::update( + const std::vector& current_inputs, + const std::vector& 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(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(i, m_known_outputs[i])); + } +} +} +#endif diff --git a/include/libremidi/backends/emscripten/observer.hpp b/include/libremidi/backends/emscripten/observer.hpp new file mode 100644 index 0000000..017785d --- /dev/null +++ b/include/libremidi/backends/emscripten/observer.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include + +#include + +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& current_inputs, const std::vector& current_outputs); + + libremidi::API get_current_api() const noexcept override; + + std::vector get_input_ports() const noexcept override; + std::vector get_output_ports() const noexcept override; + +private: + std::vector m_known_inputs; + std::vector m_known_outputs; +}; +} diff --git a/include/libremidi/backends/jack.hpp b/include/libremidi/backends/jack.hpp new file mode 100644 index 0000000..8367676 --- /dev/null +++ b/include/libremidi/backends/jack.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include + +//*********************************************************************// +// 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 + } +}; +} diff --git a/include/libremidi/backends/jack/config.hpp b/include/libremidi/backends/jack/config.hpp new file mode 100644 index 0000000..1e406e7 --- /dev/null +++ b/include/libremidi/backends/jack/config.hpp @@ -0,0 +1,50 @@ +#pragma once +#include + +#include +#include +#include +#include + +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; +struct jack_callback +{ + int64_t token; + std::function callback; +}; +struct jack_input_configuration +{ + std::string client_name = "libremidi client"; + + jack_client_t* context{}; + std::function set_process_func; + std::function clear_process_func; +}; + +struct jack_output_configuration +{ + std::string client_name = "libremidi client"; + + jack_client_t* context{}; + std::function set_process_func; + std::function 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{}; +}; + +} diff --git a/include/libremidi/backends/jack/helpers.hpp b/include/libremidi/backends/jack/helpers.hpp new file mode 100644 index 0000000..0fcfeaf --- /dev/null +++ b/include/libremidi/backends/jack/helpers.hpp @@ -0,0 +1,241 @@ +#pragma once + +#if __has_include() + #include +#elif __has_include() + #include +#elif __has_include( ) + #include + #include + #include +#endif +#include +#include + +#include +#include + +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(jack_port_name_size()); + auto p2 = std::make_unique(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 + static auto to_port_info(jack_client_t* client, jack_port_t* port) + -> std::conditional_t + { + return {{ + .client = reinterpret_cast(client), + .port = 0, + .manufacturer = "", + .device_name = "", + .port_name = jack_port_name(port), + .display_name = get_port_display_name(port), + }}; + } + + template + static auto + get_ports(jack_client_t* client, const char* pattern, const JackPortFlags flags) noexcept + -> std::vector> + { + std::vector> 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(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> impl + = std::make_shared>(nullptr); + } port; + + int64_t this_instance{}; + + semaphore_pair_lock thread_lock; + + jack_helpers() + { + static std::atomic_int64_t instance{}; + this_instance = ++instance; + } + + template + 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(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 + 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(jack_port_name_size())) + { + self.template 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(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); + } +}; +} diff --git a/include/libremidi/backends/jack/midi_in.hpp b/include/libremidi/backends/jack/midi_in.hpp new file mode 100644 index 0000000..97e2e39 --- /dev/null +++ b/include/libremidi/backends/jack/midi_in.hpp @@ -0,0 +1,114 @@ +#pragma once +#include +#include +#include +#include + +#include + +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( + 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(to_ns, event.time)); + } + + return 0; + } + + midi1::input_state_machine m_processing{this->configuration}; +}; +} diff --git a/include/libremidi/backends/jack/midi_out.hpp b/include/libremidi/backends/jack/midi_out.hpp new file mode 100644 index 0000000..2f77cc4 --- /dev/null +++ b/include/libremidi/backends/jack/midi_out.hpp @@ -0,0 +1,231 @@ +#pragma once +#include +#include +#include + +#include + +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(sz + size_sz) > ringbuffer_space) + return; + + while (jack_ringbuffer_write_space(ringbuffer) < sz + size_sz) + sched_yield(); + + jack_ringbuffer_write(ringbuffer, reinterpret_cast(&sz), size_sz); + jack_ringbuffer_write(ringbuffer, reinterpret_cast(data), sz); + } + + void read(void* jack_events) const noexcept + { + int32_t sz; + while (jack_ringbuffer_peek(ringbuffer, reinterpret_cast(&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(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( + 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(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 make( + libremidi::output_configuration&& conf, libremidi::jack_output_configuration&& api) +{ + if (api.direct) + return std::make_unique(std::move(conf), std::move(api)); + else + return std::make_unique(std::move(conf), std::move(api)); +} +} diff --git a/include/libremidi/backends/jack/observer.hpp b/include/libremidi/backends/jack/observer.hpp new file mode 100644 index 0000000..f3c6a73 --- /dev/null +++ b/include/libremidi/backends/jack/observer.hpp @@ -0,0 +1,224 @@ +#pragma once +#include +#include +#include + +#include + +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(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(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(client, port)); + } + else if (flags & JackPortIsInput) + { + seen_output_ports.insert(name); + if (this->configuration.output_added) + this->configuration.output_added(to_port_info(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(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(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(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 get_input_ports() const noexcept override + { + return get_ports(this->client, nullptr, JackPortIsOutput); + } + + std::vector get_output_ports() const noexcept override + { + return get_ports(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 seen_input_ports; + std::unordered_set seen_output_ports; +}; +} diff --git a/include/libremidi/backends/jack/shared_handler.hpp b/include/libremidi/backends/jack/shared_handler.hpp new file mode 100644 index 0000000..0ab18d0 --- /dev/null +++ b/include/libremidi/backends/jack/shared_handler.hpp @@ -0,0 +1,153 @@ +#pragma once +#if __has_include() + #include + #include + #include + + #include + + #include + +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(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(&ev.payload))); + break; + case in_callback_removed: { + auto idx = *std::get_if(&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(&ev.payload))); + break; + case out_callback_removed: + auto idx = *std::get_if(&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 payload; + }; + + boost::lockfree::spsc_queue events{16}; + + std::vector midiin_callbacks; + std::vector midiout_callbacks; +}; +} +#endif diff --git a/include/libremidi/backends/linux/alsa.hpp b/include/libremidi/backends/linux/alsa.hpp new file mode 100644 index 0000000..6741f39 --- /dev/null +++ b/include/libremidi/backends/linux/alsa.hpp @@ -0,0 +1,459 @@ +#pragma once + +#include + +#include + +#if defined(SND_LIB_VERSION) + #if __has_include() && 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() && 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("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 +} diff --git a/include/libremidi/backends/linux/dylib_loader.hpp b/include/libremidi/backends/linux/dylib_loader.hpp new file mode 100644 index 0000000..808e197 --- /dev/null +++ b/include/libremidi/backends/linux/dylib_loader.hpp @@ -0,0 +1,85 @@ +#pragma once +#if __has_include() + #include + + #include + +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 + T symbol(const char* const sym) const noexcept + { + assert(impl); + return reinterpret_cast(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( \ + 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( \ + LIBREMIDI_SYMBOL_NAME2_S(prefix, name, varname)); \ + if (!varname) \ + { \ + available = false; \ + return; \ + } \ + } +#endif diff --git a/include/libremidi/backends/linux/helpers.hpp b/include/libremidi/backends/linux/helpers.hpp new file mode 100644 index 0000000..b86b461 --- /dev/null +++ b/include/libremidi/backends/linux/helpers.hpp @@ -0,0 +1,77 @@ +#pragma once +#include +#include + +#include +#include + +#include + +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}; +}; +} diff --git a/include/libremidi/backends/linux/pipewire.hpp b/include/libremidi/backends/linux/pipewire.hpp new file mode 100644 index 0000000..c14bd39 --- /dev/null +++ b/include/libremidi/backends/linux/pipewire.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include + +#include + +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("\1"); + init = library.symbol("pw_init"); + deinit = library.symbol("pw_deinit"); + + context_new = library.symbol("pw_context_new"); + context_connect = library.symbol("pw_context_connect"); + context_destroy = library.symbol("pw_context_destroy"); + + core_disconnect = library.symbol("pw_core_disconnect"); + + proxy_add_listener + = library.symbol("pw_proxy_add_listener"); + proxy_destroy = library.symbol("pw_proxy_destroy"); + + main_loop_new = library.symbol("pw_main_loop_new"); + main_loop_destroy = library.symbol("pw_main_loop_destroy"); + main_loop_quit = library.symbol("pw_main_loop_quit"); + main_loop_run = library.symbol("pw_main_loop_run"); + main_loop_get_loop + = library.symbol("pw_main_loop_get_loop"); + + properties_new = library.symbol("pw_properties_new"); + properties_free = library.symbol("pw_properties_free"); + properties_get = library.symbol("pw_properties_get"); + + filter_new_simple = library.symbol("pw_filter_new_simple"); + filter_get_node_id + = library.symbol("pw_filter_get_node_id"); + filter_get_properties + = library.symbol("pw_filter_get_properties"); + filter_add_port = library.symbol("pw_filter_add_port"); + filter_remove_port + = library.symbol("pw_filter_remove_port"); + filter_update_properties + = library.symbol("pw_filter_update_properties"); + filter_update_params + = library.symbol("pw_filter_update_params"); + filter_get_time = library.symbol("pw_filter_get_time"); + filter_destroy = library.symbol("pw_filter_destroy"); + filter_connect = library.symbol("pw_filter_connect"); + filter_get_dsp_buffer + = library.symbol("pw_filter_get_dsp_buffer"); + filter_dequeue_buffer + = library.symbol("pw_filter_dequeue_buffer"); + filter_queue_buffer + = library.symbol("pw_filter_queue_buffer"); + filter_flush = library.symbol("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 +} diff --git a/include/libremidi/backends/linux/udev.hpp b/include/libremidi/backends/linux/udev.hpp new file mode 100644 index 0000000..2d03767 --- /dev/null +++ b/include/libremidi/backends/linux/udev.hpp @@ -0,0 +1,92 @@ +#pragma once +#include +#include + +#include + +#include + +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{}; +}; + +} diff --git a/include/libremidi/backends/pipewire.hpp b/include/libremidi/backends/pipewire.hpp new file mode 100644 index 0000000..f3a9a96 --- /dev/null +++ b/include/libremidi/backends/pipewire.hpp @@ -0,0 +1,30 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +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; + } +}; +} diff --git a/include/libremidi/backends/pipewire/config.hpp b/include/libremidi/backends/pipewire/config.hpp new file mode 100644 index 0000000..3881324 --- /dev/null +++ b/include/libremidi/backends/pipewire/config.hpp @@ -0,0 +1,53 @@ +#pragma once +#include + +#include +#include +#include +#include + +extern "C" { +struct pw_main_loop; +struct pw_filter; +struct spa_io_position; +} + +namespace libremidi +{ +using pipewire_callback_function = std::function; +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 set_process_func; + std::function clear_process_func; +}; + +struct pipewire_output_configuration +{ + std::string client_name = "libremidi client"; + + pw_main_loop* context{}; + pw_filter* filter{}; + std::function set_process_func; + std::function clear_process_func; + + int64_t output_buffer_size{65536}; +}; + +struct pipewire_observer_configuration +{ + std::string client_name = "libremidi client"; + + pw_main_loop* context{}; +}; + +} diff --git a/include/libremidi/backends/pipewire/context.hpp b/include/libremidi/backends/pipewire/context.hpp new file mode 100644 index 0000000..849a94f --- /dev/null +++ b/include/libremidi/backends/pipewire/context.hpp @@ -0,0 +1,600 @@ +#pragma once +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +namespace libremidi +{ +template +using hash_map = std::unordered_map; + +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 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 inputs; + std::vector outputs; + }; + + struct graph + { + mutable std::mutex mtx; + libremidi::hash_map physical_audio; + libremidi::hash_map physical_midi; + libremidi::hash_map software_audio; + libremidi::hash_map software_midi; + libremidi::hash_map 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 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()}); + 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 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 on_port_added; + std::function on_port_removed; + + std::vector port_listener{}; + + std::atomic pending{}; + std::atomic done{}; + bool owns_main_loop{true}; + int sync{}; +}; + +struct pipewire_filter +{ + const libpipewire& pw = libpipewire::instance(); + std::shared_ptr loop{}; + pw_filter* filter{}; + std::vector links{}; + + struct port + { + void* data; + }* port{}; + + explicit pipewire_filter(std::shared_ptr loop) + : loop{loop} + { + } + + explicit pipewire_filter(std::shared_ptr 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 diff --git a/include/libremidi/backends/pipewire/helpers.hpp b/include/libremidi/backends/pipewire/helpers.hpp new file mode 100644 index 0000000..5f43f4c --- /dev/null +++ b/include/libremidi/backends/pipewire/helpers.hpp @@ -0,0 +1,464 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +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 global_instance; + std::shared_ptr global_context; + std::unique_ptr filter; + pw_proxy* link{}; + + int64_t this_instance{}; + + eventfd_notifier termination_event{}; + pollfd fds[2]{}; + + semaphore_pair_lock thread_lock; + std::shared_ptr canary = std::make_shared(); + + enum poll_state + { + start_poll, + in_poll, + not_in_poll + }; + std::atomic current_state{not_in_poll}; + + pipewire_helpers() + { + static std::atomic_int64_t instance{}; + this_instance = ++instance; + + fds[1] = termination_event; + } + + template + 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(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(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(_data); + self.process(position); + }}; +#pragma GCC diagnostic pop + + this->filter->create_filter(self.configuration.client_name, filter_events, &self); + this->filter->start_filter(); + } + } + + template + 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 + 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(configuration.context); + } + else + { + this->global_instance = std::make_shared(); + this->global_context = std::make_shared(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 + 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(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(port)); + } + else + { + if (conf.input_added) + conf.input_added(to_port_info(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(port)); + } + else + { + if (conf.input_removed) + conf.input_removed(to_port_info(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( + 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( + self.configuration, + "PipeWire: could not connect to port: " + p.port_name + " -> " + out_port.port_name); + return false; + } + + return true; + } + + template + static auto to_port_info(const pipewire_context::port_info& port) + -> std::conditional_t + { + 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 + static auto get_ports(const pipewire_context& ctx) noexcept -> std::vector< + std::conditional_t> + { + std::vector> + 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(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(p)); + } + } + } + + return ret; + } +}; +} diff --git a/include/libremidi/backends/pipewire/midi_in.hpp b/include/libremidi/backends/pipewire/midi_in.hpp new file mode 100644 index 0000000..05680e5 --- /dev/null +++ b/include/libremidi/backends/pipewire/midi_in.hpp @@ -0,0 +1,125 @@ +#pragma once +#include +#include +#include +#include + +#include + +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(to_ns, c->offset)); + } + + pw.filter_queue_buffer(this->filter->port, b); + } + + midi1::input_state_machine m_processing{this->configuration}; +}; +} diff --git a/include/libremidi/backends/pipewire/midi_out.hpp b/include/libremidi/backends/pipewire/midi_out.hpp new file mode 100644 index 0000000..00e83ff --- /dev/null +++ b/include/libremidi/backends/pipewire/midi_out.hpp @@ -0,0 +1,171 @@ +#pragma once +#include +#include +#include + +#include + +#include + +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(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 m_queue; + std::atomic_int64_t m_process_clock = 0; +}; +} diff --git a/include/libremidi/backends/pipewire/observer.hpp b/include/libremidi/backends/pipewire/observer.hpp new file mode 100644 index 0000000..7a1e0a0 --- /dev/null +++ b/include/libremidi/backends/pipewire/observer.hpp @@ -0,0 +1,74 @@ +#pragma once +#include +#include +#include + +#include + +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 get_input_ports() const noexcept override + { + return get_ports(*this->global_context); + } + + std::vector get_output_ports() const noexcept override + { + return get_ports(*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 seen_input_ports; + std::unordered_set seen_output_ports; +}; +} diff --git a/include/libremidi/backends/pipewire/shared_handler.hpp b/include/libremidi/backends/pipewire/shared_handler.hpp new file mode 100644 index 0000000..5dab246 --- /dev/null +++ b/include/libremidi/backends/pipewire/shared_handler.hpp @@ -0,0 +1,153 @@ +#pragma once +#if __has_include() + #include + #include + #include + + #include + + #include + +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(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(&ev.payload))); + break; + case in_callback_removed: { + auto idx = *std::get_if(&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(&ev.payload))); + break; + case out_callback_removed: + auto idx = *std::get_if(&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 payload; + }; + + boost::lockfree::spsc_queue events{16}; + + std::vector midiin_callbacks; + std::vector midiout_callbacks; +}; +} +#endif diff --git a/include/libremidi/backends/winmidi.hpp b/include/libremidi/backends/winmidi.hpp new file mode 100644 index 0000000..be7788c --- /dev/null +++ b/include/libremidi/backends/winmidi.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include + +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 diff --git a/include/libremidi/backends/winmidi/config.hpp b/include/libremidi/backends/winmidi/config.hpp new file mode 100644 index 0000000..276b616 --- /dev/null +++ b/include/libremidi/backends/winmidi/config.hpp @@ -0,0 +1,19 @@ +#pragma once +#include + +namespace libremidi::winmidi +{ + +struct input_configuration +{ +}; + +struct output_configuration +{ +}; + +struct observer_configuration +{ +}; + +} diff --git a/include/libremidi/backends/winmidi/helpers.hpp b/include/libremidi/backends/winmidi/helpers.hpp new file mode 100644 index 0000000..6a0046a --- /dev/null +++ b/include/libremidi/backends/winmidi/helpers.hpp @@ -0,0 +1,27 @@ +#pragma once +// clang-format off +#define NOMINMAX 1 +#define WIN32_LEAN_AND_MEAN 1 +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +// 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; +} diff --git a/include/libremidi/backends/winmidi/midi_in.hpp b/include/libremidi/backends/winmidi/midi_in.hpp new file mode 100644 index 0000000..84bb44f --- /dev/null +++ b/include/libremidi/backends/winmidi/midi_in.hpp @@ -0,0 +1,94 @@ +#pragma once +#include +#include +#include +#include + +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}; +}; +} diff --git a/include/libremidi/backends/winmidi/midi_out.hpp b/include/libremidi/backends/winmidi/midi_out.hpp new file mode 100644 index 0000000..d10f560 --- /dev/null +++ b/include/libremidi/backends/winmidi/midi_out.hpp @@ -0,0 +1,91 @@ +#pragma once +#include +#include +#include +#include + +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*)message, (const uint8_t*)message + size}); + port_.SendBuffer(rb.DetachBuffer()); +#endif + } + +private: +#if 0 +winrt::Windows::Devices::Midi2::IMidiOutPort port_{nullptr}; +#endif +}; + +} diff --git a/include/libremidi/backends/winmidi/observer.hpp b/include/libremidi/backends/winmidi/observer.hpp new file mode 100644 index 0000000..bc81f68 --- /dev/null +++ b/include/libremidi/backends/winmidi/observer.hpp @@ -0,0 +1,151 @@ +#pragma once +#include +#include +#include +; +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 + auto to_port_info(const DeviceInformation& p) const noexcept + -> std::conditional_t + { + return { + {.client = 0, + .port = 0, + .manufacturer = "", + .device_name = "", + .port_name = to_string(p.Id()), + .display_name = to_string(p.Name())}}; + } + + std::vector get_input_ports() const noexcept override + { + std::vector 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 get_output_ports() const noexcept override + { + std::vector 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 +}; + +} diff --git a/include/libremidi/backends/winmm.hpp b/include/libremidi/backends/winmm.hpp new file mode 100644 index 0000000..a45692f --- /dev/null +++ b/include/libremidi/backends/winmm.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include +#include + +// 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; } +}; +} diff --git a/include/libremidi/backends/winmm/config.hpp b/include/libremidi/backends/winmm/config.hpp new file mode 100644 index 0000000..61863e0 --- /dev/null +++ b/include/libremidi/backends/winmm/config.hpp @@ -0,0 +1,31 @@ +#pragma once +#include + +#include +#include + +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 callback; +}; + +struct winmm_observer_configuration +{ + std::chrono::milliseconds poll_period{100}; + std::function manual_poll; +}; + +} diff --git a/include/libremidi/backends/winmm/helpers.hpp b/include/libremidi/backends/winmm/helpers.hpp new file mode 100644 index 0000000..63584d2 --- /dev/null +++ b/include/libremidi/backends/winmm/helpers.hpp @@ -0,0 +1,81 @@ +#pragma once +#define NOMINMAX 1 +#define WIN32_LEAN_AND_MEAN 1 +#include + +#include + +// clang-format off +#include +#include +// 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(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); +} + +} diff --git a/include/libremidi/backends/winmm/midi_in.hpp b/include/libremidi/backends/winmm/midi_in.hpp new file mode 100644 index 0000000..02a67d5 --- /dev/null +++ b/include/libremidi/backends/winmm/midi_in.hpp @@ -0,0 +1,309 @@ +#pragma once +#include +#include +#include +#include +#include + +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(&midiInputCallback), + std::bit_cast(this), CALLBACK_FUNCTION); + if (result != MMSYSERR_NOERROR) + { + 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(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(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( + 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( + 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( + 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( + 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(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::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(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(to_ns, 0)); + } + } + else + { + // Sysex message ( MIM_LONGDATA or MIM_LONGERROR ) + const auto* sysex = reinterpret_cast(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(sysex->lpData); + + self.m_processing.on_bytes( + {sysex_bytes, sysex_bytes + sysex->dwBytesRecorded}, + self.m_processing.timestamp(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 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}; +}; + +} diff --git a/include/libremidi/backends/winmm/midi_out.hpp b/include/libremidi/backends/winmm/midi_out.hpp new file mode 100644 index 0000000..8735c91 --- /dev/null +++ b/include/libremidi/backends/winmm/midi_out.hpp @@ -0,0 +1,181 @@ +#pragma once +#include +#include +#include + +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( + 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( + 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( + 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( + 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( + configuration, "midi_out_winmm::send_message: error sending MIDI message."); + } + } + } + +private: + HMIDIOUT outHandle; // Handle to Midi Output Device + std::vector buffer; +}; + +} diff --git a/include/libremidi/backends/winmm/observer.hpp b/include/libremidi/backends/winmm/observer.hpp new file mode 100644 index 0000000..c243bd3 --- /dev/null +++ b/include/libremidi/backends/winmm/observer.hpp @@ -0,0 +1,271 @@ +#pragma once +#include +#include +#include + +#include +#include +#include +#include + +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(); + else + check_new_ports(); + } + + ~observer_winmm() { } + + libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_MM; } + + std::vector get_input_ports() const noexcept override + { + return get_port_list(); + } + + std::vector get_output_ports() const noexcept override + { + return get_port_list(); + } + +protected: + template + void check_new_ports() + { + auto currInputPortList = get_port_list(); + + 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(); + + 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 + auto get_port_list() const noexcept + -> std::vector> + { + std::vector> 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 inputPortList; + std::vector outputPortList; +}; +} + +#if __has_include() && __cpp_lib_jthread >= 201911L + #include +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(); + std::this_thread::sleep_for(this->configuration.poll_period); + } + }); + } + +private: + std::jthread thread; +}; +} +#else + #include + #include +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(); + 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(); }}); + } + + ~observer_manual() { } +}; +} +namespace libremidi +{ +template <> +inline std::unique_ptr make( + libremidi::observer_configuration&& conf, libremidi::winmm_observer_configuration&& api) +{ + if (api.manual_poll) + return std::make_unique(std::move(conf), std::move(api)); + else + return std::make_unique(std::move(conf), std::move(api)); +} +} diff --git a/include/libremidi/backends/winuwp.hpp b/include/libremidi/backends/winuwp.hpp new file mode 100644 index 0000000..48e91b3 --- /dev/null +++ b/include/libremidi/backends/winuwp.hpp @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#include + +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 diff --git a/include/libremidi/backends/winuwp/config.hpp b/include/libremidi/backends/winuwp/config.hpp new file mode 100644 index 0000000..27bb1a1 --- /dev/null +++ b/include/libremidi/backends/winuwp/config.hpp @@ -0,0 +1,19 @@ +#pragma once +#include + +namespace libremidi +{ + +struct winuwp_input_configuration +{ +}; + +struct winuwp_output_configuration +{ +}; + +struct winuwp_observer_configuration +{ +}; + +} diff --git a/include/libremidi/backends/winuwp/helpers.hpp b/include/libremidi/backends/winuwp/helpers.hpp new file mode 100644 index 0000000..380e458 --- /dev/null +++ b/include/libremidi/backends/winuwp/helpers.hpp @@ -0,0 +1,63 @@ +#pragma once +#define NOMINMAX 1 +#define WIN32_LEAN_AND_MEAN 1 +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +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 +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(); +} +} +} diff --git a/include/libremidi/backends/winuwp/midi_in.hpp b/include/libremidi/backends/winuwp/midi_in.hpp new file mode 100644 index 0000000..fa198db --- /dev/null +++ b/include/libremidi/backends/winuwp/midi_in.hpp @@ -0,0 +1,106 @@ +#pragma once +#include +#include +#include +#include +#include + +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(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::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}; +}; +} diff --git a/include/libremidi/backends/winuwp/midi_out.hpp b/include/libremidi/backends/winuwp/midi_out.hpp new file mode 100644 index 0000000..6a74619 --- /dev/null +++ b/include/libremidi/backends/winuwp/midi_out.hpp @@ -0,0 +1,80 @@ +#pragma once +#include +#include +#include +#include + +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*)message, (const uint8_t*)message + size}); + port_.SendBuffer(rb.DetachBuffer()); + } + +private: + winrt::Windows::Devices::Midi::IMidiOutPort port_{nullptr}; +}; + +} diff --git a/include/libremidi/backends/winuwp/observer.hpp b/include/libremidi/backends/winuwp/observer.hpp new file mode 100644 index 0000000..86eeb59 --- /dev/null +++ b/include/libremidi/backends/winuwp/observer.hpp @@ -0,0 +1,296 @@ +#pragma once +#include +#include + +namespace libremidi +{ +class observer_winuwp_internal +{ +public: + struct port_info + { + hstring id; + hstring name; + }; + struct callback + { + int token{}; + std::function function; + }; + struct callbacks + { + std::vector cbs; + int current_token{}; + void operator()(const port_info& p) + { + for (auto& cb : cbs) + cb.function(p); + } + + int add(std::function 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 get_ports() const + { + std::lock_guard lock(portListMutex_); + return portList_; + } + + unsigned int get_port_count() const + { + std::lock_guard lock(portListMutex_); + return static_cast(portList_.size()); + } + + bool get_port_info(unsigned int portNumber, port_info& portInfo) const + { + std::lock_guard lock(portListMutex_); + if (portNumber >= portList_.size()) + return false; + portInfo = portList_[portNumber]; + return true; + } + + hstring get_port_id(unsigned int portNumber) const + { + std::lock_guard lock(portListMutex_); + return portNumber < portList_.size() ? portList_[portNumber].id : hstring{}; + } + + std::string get_port_name(unsigned int portNumber) const + { + std::lock_guard lock(portListMutex_); + return portNumber < portList_.size() ? to_string(portList_[portNumber].name) : std::string{}; + } + + int PortAdded(const std::function& handler) + { + return portAddedEvent_.add(handler); + } + + void PortAdded(int token) noexcept { portAddedEvent_.remove(token); } + + int PortRemoved(const std::function& 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 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 p; + hstring name; + { + std::lock_guard 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 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 + auto to_port_info(const observer_winuwp_internal::port_info& p) const noexcept + -> std::conditional_t + { + return { + {.client = 0, + .port = 0, + .manufacturer = "", + .device_name = "", + .port_name = to_string(p.id), + .display_name = to_string(p.name)}}; + } + + std::vector get_input_ports() const noexcept override + { + std::vector ret; + for (auto& port : internalInPortObserver_.get_ports()) + ret.push_back(to_port_info(port)); + return ret; + } + + std::vector get_output_ports() const noexcept override + { + std::vector ret; + for (auto& port : internalOutPortObserver_.get_ports()) + ret.push_back(to_port_info(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(name)); + } + + void on_input_removed(const observer_winuwp_internal::port_info& name) + { + if (configuration.input_removed) + configuration.input_removed(to_port_info(name)); + } + + void on_output_added(const observer_winuwp_internal::port_info& name) + { + if (configuration.output_added) + configuration.output_added(to_port_info(name)); + } + + void on_output_removed(const observer_winuwp_internal::port_info& name) + { + if (configuration.output_removed) + configuration.output_removed(to_port_info(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}; +}; + +} diff --git a/include/libremidi/client.cpp b/include/libremidi/client.cpp new file mode 100644 index 0000000..4d2c2d2 --- /dev/null +++ b/include/libremidi/client.cpp @@ -0,0 +1,47 @@ +#if !defined(LIBREMIDI_HEADER_ONLY) + #include "client.hpp" +#endif +#include +#include + +#ifdef LIBREMIDI_ALSA + #include +#endif +#ifdef LIBREMIDI_JACK + #include +#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() + #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)}; + } +} +} diff --git a/include/libremidi/client.hpp b/include/libremidi/client.hpp new file mode 100644 index 0000000..8acb082 --- /dev/null +++ b/include/libremidi/client.hpp @@ -0,0 +1,196 @@ +#pragma once +#include +#include +#include + +#include + +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 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 get_input_ports() const noexcept + { + return m_observer.get_input_ports(); + } + + std::vector 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 m_inputs; + std::map m_outputs; + + observer m_observer; +}; +} +#if defined(LIBREMIDI_HEADER_ONLY) + #include "client.cpp" +#endif diff --git a/include/libremidi/cmidi2.hpp b/include/libremidi/cmidi2.hpp new file mode 100644 index 0000000..39a347a --- /dev/null +++ b/include/libremidi/cmidi2.hpp @@ -0,0 +1,3261 @@ +#ifndef CMIDI2_H_INCLUDED +#define CMIDI2_H_INCLUDED + +#include +#include +#include + +#define MIDI_2_0_RESERVED 0 +#define JR_TIMESTAMP_TICKS_PER_SECOND 31250 + +#pragma GCC system_header +#pragma clang system_header + +#ifdef __cplusplus +extern "C" { +#endif + +enum cmidi2_status_code +{ + CMIDI2_STATUS_NOTE_OFF = 0x80, + CMIDI2_STATUS_NOTE_ON = 0x90, + CMIDI2_STATUS_PAF = 0xA0, + CMIDI2_STATUS_CC = 0xB0, + CMIDI2_STATUS_PROGRAM = 0xC0, + CMIDI2_STATUS_CAF = 0xD0, + CMIDI2_STATUS_PITCH_BEND = 0xE0, + CMIDI2_STATUS_PER_NOTE_RCC = 0x00, + CMIDI2_STATUS_PER_NOTE_ACC = 0x10, + CMIDI2_STATUS_RPN = 0x20, + CMIDI2_STATUS_NRPN = 0x30, + CMIDI2_STATUS_RELATIVE_RPN = 0x40, + CMIDI2_STATUS_RELATIVE_NRPN = 0x50, + CMIDI2_STATUS_PER_NOTE_PITCH_BEND = 0x60, + CMIDI2_STATUS_PER_NOTE_MANAGEMENT = 0xF0, +}; + +enum cmidi2_message_type +{ + CMIDI2_MESSAGE_TYPE_UTILITY = 0, + CMIDI2_MESSAGE_TYPE_SYSTEM = 1, + CMIDI2_MESSAGE_TYPE_MIDI_1_CHANNEL = 2, + CMIDI2_MESSAGE_TYPE_SYSEX7 = 3, + CMIDI2_MESSAGE_TYPE_MIDI_2_CHANNEL = 4, + CMIDI2_MESSAGE_TYPE_SYSEX8_MDS = 5, + CMIDI2_MESSAGE_TYPE_FLEX_DATA = 0xD, + CMIDI2_MESSAGE_TYPE_UMP_STREAM = 0xF, +}; + +enum cmidi2_ci_protocol_bytes +{ + CMIDI2_PROTOCOL_BYTES_TYPE = 1, + CMIDI2_PROTOCOL_BYTES_VERSION = 2, + CMIDI2_PROTOCOL_BYTES_EXTENSIONS = 3, +}; + +enum cmidi2_ci_protocol_values +{ + CMIDI2_PROTOCOL_TYPE_MIDI1 = 1, + CMIDI2_PROTOCOL_TYPE_MIDI2 = 2, + CMIDI2_PROTOCOL_VERSION_MIDI1 = 0, + CMIDI2_PROTOCOL_VERSION_MIDI2_V1 = 0, +}; + +enum cmidi2_ci_protocol_extensions +{ + CMIDI2_PROTOCOL_EXTENSIONS_JITTER = 1, + CMIDI2_PROTOCOL_EXTENSIONS_LARGER = 2, // only for MIDI 1.0 compat UMP +}; + +typedef struct cmidi2_ci_protocol_tag +{ + uint8_t protocol_type; // cmidi2_ci_protocol_bytes + uint8_t version; // cmidi2_ci_protocol_values + uint8_t extensions; // cmidi2_ci_protocol_extensions (flags) + uint8_t reserved1; + uint8_t reserved2; +} cmidi2_ci_protocol; + +enum cmidi2_cc +{ + CMIDI2_CC_BANK_SELECT = 0x00, + CMIDI2_CC_MODULATION = 0x01, + CMIDI2_CC_BREATH = 0x02, + CMIDI2_CC_FOOT = 0x04, + CMIDI2_CC_PORTAMENTO_TIME = 0x05, + CMIDI2_CC_DTE_MSB = 0x06, + CMIDI2_CC_VOLUME = 0x07, + CMIDI2_CC_BALANCE = 0x08, + CMIDI2_CC_PAN = 0x0A, + CMIDI2_CC_EXPRESSION = 0x0B, + CMIDI2_CC_EFFECT_CONTROL_1 = 0x0C, + CMIDI2_CC_EFFECT_CONTROL_2 = 0x0D, + CMIDI2_CC_GENERAL_1 = 0x10, + CMIDI2_CC_GENERAL_2 = 0x11, + CMIDI2_CC_GENERAL_3 = 0x12, + CMIDI2_CC_GENERAL_4 = 0x13, + CMIDI2_CC_BANK_SELECT_LSB = 0x20, + CMIDI2_CC_MODULATION_LSB = 0x21, + CMIDI2_CC_BREATH_LSB = 0x22, + CMIDI2_CC_FOOT_LSB = 0x24, + CMIDI2_CC_PORTAMENTO_TIME_LSB = 0x25, + CMIDI2_CC_DTE_LSB = 0x26, + CMIDI2_CC_VOLUME_LSB = 0x27, + CMIDI2_CC_BALANCE_LSB = 0x28, + CMIDI2_CC_PAN_LSB = 0x2A, + CMIDI2_CC_EXPRESSION_LSB = 0x2B, + CMIDI2_CC_EFFECT1_LSB = 0x2C, + CMIDI2_CC_EFFECT2_LSB = 0x2D, + CMIDI2_CC_GENERAL_1_LSB = 0x30, + CMIDI2_CC_GENERAL_2_LSB = 0x31, + CMIDI2_CC_GENERAL_3_LSB = 0x32, + CMIDI2_CC_GENERAL_4_LSB = 0x33, + CMIDI2_CC_HOLD = 0x40, + CMIDI2_CC_PORTAMENTO_SWITCH = 0x41, + CMIDI2_CC_SOSTENUTO = 0x42, + CMIDI2_CC_SOFT_PEDAL = 0x43, + CMIDI2_CC_LEGATO = 0x44, + CMIDI2_CC_HOLD_2 = 0x45, + CMIDI2_CC_SOUND_CONTROLLER_1 = 0x46, + CMIDI2_CC_SOUND_CONTROLLER_2 = 0x47, + CMIDI2_CC_SOUND_CONTROLLER_3 = 0x48, + CMIDI2_CC_SOUND_CONTROLLER_4 = 0x49, + CMIDI2_CC_SOUND_CONTROLLER_5 = 0x4A, + CMIDI2_CC_SOUND_CONTROLLER_6 = 0x4B, + CMIDI2_CC_SOUND_CONTROLLER_7 = 0x4C, + CMIDI2_CC_SOUND_CONTROLLER_8 = 0x4D, + CMIDI2_CC_SOUND_CONTROLLER_9 = 0x4E, + CMIDI2_CC_SOUND_CONTROLLER_10 = 0x4F, + CMIDI2_CC_GENERAL_5 = 0x50, + CMIDI2_CC_GENERAL_6 = 0x51, + CMIDI2_CC_GENERAL_7 = 0x52, + CMIDI2_CC_GENERAL_8 = 0x53, + CMIDI2_CC_PORTAMENTO_CONTROL = 0x54, + CMIDI2_CC_RSD = 0x5B, + CMIDI2_CC_EFFECT_1 = 0x5B, + CMIDI2_CC_TREMOLO = 0x5C, + CMIDI2_CC_EFFECT_2 = 0x5C, + CMIDI2_CC_CSD = 0x5D, + CMIDI2_CC_EFFECT_3 = 0x5D, + CMIDI2_CC_CELESTE = 0x5E, + CMIDI2_CC_EFFECT_4 = 0x5E, + CMIDI2_CC_PHASER = 0x5F, + CMIDI2_CC_EFFECT_5 = 0x5F, + CMIDI2_CC_DTE_INCREMENT = 0x60, + CMIDI2_CC_DTE_DECREMENT = 0x61, + CMIDI2_CC_NRPN_LSB = 0x62, + CMIDI2_CC_NRPN_MSB = 0x63, + CMIDI2_CC_RPN_LSB = 0x64, + CMIDI2_CC_RPN_MSB = 0x65, + // CHANNEL MODE MESSAGES + CMIDI2_CC_ALL_SOUND_OFF = 0x78, + CMIDI2_CC_RESET_ALL_CONTROLLERS = 0x79, + CMIDI2_CC_LOCAL_CONTROL = 0x7A, + CMIDI2_CC_ALL_NOTES_OFF = 0x7B, + CMIDI2_CC_OMNI_MODE_OFF = 0x7C, + CMIDI2_CC_OMNI_MODE_ON = 0x7D, + CMIDI2_CC_POLY_MODE_ON_OFF = 0x7E, + CMIDI2_CC_POLY_MODE_ON = 0x7F, +}; + +enum cmidi2_rpn +{ + CMIDI2_RPN_PITCH_BEND_SENSITIVITY = 0, + CMIDI2_RPN_FINE_TUNING = 1, + CMIDI2_RPN_COARSE_TUNING = 2, + CMIDI2_RPN_TUNING_PROGRAM = 3, + CMIDI2_RPN_TUNING_BANK_SELECT = 4, + CMIDI2_RPN_MODULATION_DEPTH = 5, +}; + +enum cmidi2_meta_event_type +{ + CMIDI2_META_SEQUENCE_NUMBER = 0X00, + CMIDI2_META_TEXT = 0X01, + CMIDI2_META_COPYRIGHT = 0X02, + CMIDI2_META_TRACK_NAME = 0X03, + CMIDI2_META_INSTRUMENT_NAME = 0X04, + CMIDI2_META_LYRIC = 0X05, + CMIDI2_META_MARKER = 0X06, + CMIDI2_META_CUE = 0X07, + CMIDI2_META_CHANNEL_PREFIX = 0X20, + CMIDI2_META_END_OF_TRACK = 0X2F, + CMIDI2_META_TEMPO = 0X51, + CMIDI2_META_SMPTE_OFFSET = 0X54, + CMIDI2_META_TIME_SIGNATURE = 0X58, + CMIDI2_META_KEY_SIGNATURE = 0X59, + CMIDI2_META_SEQUENCER_SPECIFIC = 0X7F, +}; + +enum cmidi2_per_note_management_flags +{ + CMIDI2_PER_NOTE_MANAGEMENT_RESET = 1, + CMIDI2_PER_NOTE_MANAGEMENT_DETACH = 2, +}; + +enum cmidi2_note_attribute_type +{ + CMIDI2_ATTRIBUTE_TYPE_NONE = 0, + CMIDI2_ATTRIBUTE_TYPE_MANUFACTURER = 1, + CMIDI2_ATTRIBUTE_TYPE_PROFILE = 2, + CMIDI2_ATTRIBUTE_TYPE_PITCH7_9 = 3, +}; + +enum cmidi2_program_change_option_flags +{ + CMIDI2_PROGRAM_CHANGE_OPTION_NONE = 0, + CMIDI2_PROGRAM_CHANGE_OPTION_BANK_VALID = 1, +}; + +enum cmidi2_sysex_status +{ + CMIDI2_SYSEX_IN_ONE_UMP = 0, + CMIDI2_SYSEX_START = 0x10, + CMIDI2_SYSEX_CONTINUE = 0x20, + CMIDI2_SYSEX_END = 0x30, +}; + +enum cmidi2_mixed_data_set_status +{ + CMIDI2_MIXED_DATA_STATUS_HEADER = 0x80, + CMIDI2_MIXED_DATA_STATUS_PAYLOAD = 0x90, +}; + +enum cmidi2_system_message_status +{ + CMIDI2_SYSTEM_STATUS_MIDI_TIME_CODE = 0xF1, + CMIDI2_SYSTEM_STATUS_SONG_POSITION = 0xF2, + CMIDI2_SYSTEM_STATUS_SONG_SELECT = 0xF3, + CMIDI2_SYSTEM_STATUS_TUNE_REQUEST = 0xF6, + CMIDI2_SYSTEM_STATUS_TIMING_CLOCK = 0xF8, + CMIDI2_SYSTEM_STATUS_START = 0xFA, + CMIDI2_SYSTEM_STATUS_CONTINUE = 0xFB, + CMIDI2_SYSTEM_STATUS_STOP = 0xFC, + CMIDI2_SYSTEM_STATUS_ACTIVE_SENSING = 0xFE, + CMIDI2_SYSTEM_STATUS_RESET = 0xFF, +}; + +// TODO: remove this. Use cmidi2_utility_message_status instead +enum cmidi2_jr_timestamp_status +{ + CMIDI2_JR_CLOCK = 0x10, + CMIDI2_JR_TIMESTAMP = 0x20, +}; + +enum cmidi2_utility_message_status +{ + CMIDI2_UTILITY_STATUS_NOOP = 0, + CMIDI2_UTILITY_STATUS_JR_CLOCK = 0x10, + CMIDI2_UTILITY_STATUS_JR_TIMESTAMP = 0x20, + CMIDI2_UTILITY_STATUS_DCTPQ = 0x30, + CMIDI2_UTILITY_STATUS_DELTA_CLOCKSTAMP = 0x40, +}; + +enum cmidi2_flex_data_status_bank +{ + CMIDI2_FLEX_DATA_BANK_SETUP_AND_PERFORMANCE = 0, + CMIDI2_FLEX_DATA_BANK_METADATA_TEXT = 1, + CMIDI2_FLEX_DATA_BANK_PERFORMANCE_TEXT = 2 +}; +enum cmidi2_flex_data_setup_status +{ + CMIDI2_FLEX_DATA_STATUS_SET_TEMPO = 0, + CMIDI2_FLEX_DATA_STATUS_SET_TIME_SIGNATURE = 1, + CMIDI2_FLEX_DATA_STATUS_SET_METRONOME = 2, + CMIDI2_FLEX_DATA_STATUS_SET_KEY_SIGNATURE = 5, + CMIDI2_FLEX_DATA_STATUS_SET_CHORD_NAME = 6 +}; +enum cmidi2_flex_data_metadata_text_status +{ + CMIDI2_FLEX_DATA_STATUS_UNKNOWN_METADATA_TEXT = 0, + CMIDI2_FLEX_DATA_STATUS_PROJECT_NAME = 1, + CMIDI2_FLEX_DATA_STATUS_SONG_NAME = 2, + CMIDI2_FLEX_DATA_STATUS_CLIP_NAME = 3, + CMIDI2_FLEX_DATA_STATUS_COPYRIGHT_NAME = 4, + CMIDI2_FLEX_DATA_STATUS_COMPOSER_NAME = 5, + CMIDI2_FLEX_DATA_STATUS_LYRICIST_NAME = 6, + CMIDI2_FLEX_DATA_STATUS_ARRANGER_NAME = 7, + CMIDI2_FLEX_DATA_STATUS_PUBLISHER_NAME = 8, + CMIDI2_FLEX_DATA_STATUS_PRIMARY_PERFORMER_NAME = 9, + CMIDI2_FLEX_DATA_STATUS_ACCOMPANYING_PERFORMAER_NAME = 10, + CMIDI2_FLEX_DATA_STATUS_RECORDING_DATE = 11, + CMIDI2_FLEX_DATA_STATUS_RECORDING_LOCATION = 12 +}; +enum cmidi2_flex_data_performance_text_status +{ + CMIDI2_FLEX_DATA_STATUS_UNKNOWN_PERFORMANCE_TEXT = 0, + CMIDI2_FLEX_DATA_STATUS_LYRICS = 1, + CMIDI2_FLEX_DATA_STATUS_LYRICS_LANGUAGE = 2, + CMIDI2_FLEX_DATA_STATUS_RUBY = 3, + CMIDI2_FLEX_DATA_STATUS_RUBY_LANGUAGE = 4 +}; + +enum cmidi2_ump_chord_name_sharps_flats +{ + CMIDI2_UMP_CHORD_NAME_DOUBLE_SHARP = 2, + CMIDI2_UMP_CHORD_NAME_SHARP = 1, + CMIDI2_UMP_CHORD_NAME_NATURAL = 0, + CMIDI2_UMP_CHORD_NAME_FLAT = 0xF, + CMIDI2_UMP_CHORD_NAME_DOUBLE_FLAT = 0xE, +}; + +enum cmidi2_ump_chord_name_tonic_note +{ + CMIDI2_UMP_CHORD_NAME_UNKNOWN = 0, + CMIDI2_UMP_CHORD_NAME_A = 1, + CMIDI2_UMP_CHORD_NAME_B = 2, + CMIDI2_UMP_CHORD_NAME_C = 3, + CMIDI2_UMP_CHORD_NAME_D = 4, + CMIDI2_UMP_CHORD_NAME_E = 5, + CMIDI2_UMP_CHORD_NAME_F = 6, + CMIDI2_UMP_CHORD_NAME_G = 7, +}; + +enum cmidi2_ump_chor_name_chord_type +{ + CMIDI2_UMP_CHORD_TYPE_UNKNOWN = 0, + CMIDI2_UMP_CHORD_TYPE_MAJOR = 1, + CMIDI2_UMP_CHORD_TYPE_MAJOR_6TH = 2, + CMIDI2_UMP_CHORD_TYPE_MAJOR_7TH = 3, + CMIDI2_UMP_CHORD_TYPE_MAJOR_9TH = 4, + CMIDI2_UMP_CHORD_TYPE_MAJOR_11TH = 5, + CMIDI2_UMP_CHORD_TYPE_MAJOR_13TH = 6, + CMIDI2_UMP_CHORD_TYPE_MINOR = 7, + CMIDI2_UMP_CHORD_TYPE_MINOR_6TH = 8, + CMIDI2_UMP_CHORD_TYPE_MINOR_7TH = 9, + CMIDI2_UMP_CHORD_TYPE_MINOR_9TH = 10, + CMIDI2_UMP_CHORD_TYPE_MINOR_11TH = 11, + CMIDI2_UMP_CHORD_TYPE_MINOR_13TH = 12, + CMIDI2_UMP_CHORD_TYPE_DOMINANT = 13, + CMIDI2_UMP_CHORD_TYPE_DOMINANT_9TH = 14, + CMIDI2_UMP_CHORD_TYPE_DOMINANT_11TH = 15, + CMIDI2_UMP_CHORD_TYPE_DOMINANT_13TH = 16, + CMIDI2_UMP_CHORD_TYPE_AUGMENTED = 17, + CMIDI2_UMP_CHORD_TYPE_AUGMENTED_7TH = 18, + CMIDI2_UMP_CHORD_TYPE_DIMINISHED = 19, + CMIDI2_UMP_CHORD_TYPE_DIMINISHED_7TH = 20, + CMIDI2_UMP_CHORD_TYPE_HALF_DIMINISHED = 21, + CMIDI2_UMP_CHORD_TYPE_MAJOR_MINOR = 22, + CMIDI2_UMP_CHORD_TYPE_MINOR_MAJOR = 22, // same + CMIDI2_UMP_CHORD_TYPE_PEDAL = 23, + CMIDI2_UMP_CHORD_TYPE_POWER = 24, + CMIDI2_UMP_CHORD_TYPE_SUSPENDED_2ND = 25, + CMIDI2_UMP_CHORD_TYPE_SUSPENDED_4TH = 26, + CMIDI2_UMP_CHORD_TYPE_7_SUSPENDED_4TH = 27, +}; + +static inline uint8_t cmidi2_ump_get_num_bytes(uint32_t data) +{ + switch (((data & 0xF0000000) >> 28) & 0xF) + { + case CMIDI2_MESSAGE_TYPE_UTILITY: + case CMIDI2_MESSAGE_TYPE_SYSTEM: + case CMIDI2_MESSAGE_TYPE_MIDI_1_CHANNEL: + return 4; + case CMIDI2_MESSAGE_TYPE_MIDI_2_CHANNEL: + case CMIDI2_MESSAGE_TYPE_SYSEX7: + return 8; + case CMIDI2_MESSAGE_TYPE_SYSEX8_MDS: + return 16; + } + return 0xFF; /* wrong */ +} + +typedef struct cmidi2_ump128 +{ + uint32_t p1; + uint32_t p2; + uint32_t p3; + uint32_t p4; +} cmidi2_ump128_t; + +typedef struct cmidi2_ump_version +{ + uint8_t major; + uint8_t minor; +} cmidi2_ump_version_t; + +enum cmidi2_ump_stream_status +{ + CMIDI2_UMP_STREAM_STATUS_ENDPOINT_DISCOVERY = 0, + CMIDI2_UMP_STREAM_STATUS_ENDPOINT_INFO = 1, + CMIDI2_UMP_STREAM_STATUS_DEVICE_IDENTITY = 2, + CMIDI2_UMP_STREAM_STATUS_ENDPOINT_NAME = 3, + CMIDI2_UMP_STREAM_STATUS_PRODUCT_INSTANCE_ID = 4, + CMIDI2_UMP_STREAM_STATUS_STREAM_CONFIGURATION_REQUEST = 5, + CMIDI2_UMP_STREAM_STATUS_STREAM_CONFIGURATION_NOTIFICATION = 6, + CMIDI2_UMP_STREAM_STATUS_FUNCTION_BLOCK_DISCOVERY = 0x10, + CMIDI2_UMP_STREAM_STATUS_FUNCTION_BLOCK_INFO = 0x11, + CMIDI2_UMP_STREAM_STATUS_FUNCTION_BLOCK_NAME = 0x12, + CMIDI2_UMP_STREAM_STATUS_START_OF_CLIP = 0x20, + CMIDI2_UMP_STREAM_STATUS_END_OF_CLIP = 0x21, +}; + +enum cmidi2_ump_endpoint_filter_flags +{ + CMIDI2_UMP_ENDPOINT_FILTER_ENDPOINT_INFO = 1, + CMIDI2_UMP_ENDPOINT_FILTER_DEVICE_IDENTITY = 2, + CMIDI2_UMP_ENDPOINT_FILTER_ENDPOINT_NAME = 4, + CMIDI2_UMP_ENDPOINT_FILTER_PRODUCT_INSTANCE_ID = 8, + CMIDI2_UMP_ENDPOINT_FILTER_STREAM_CONFIGURATION = 0x10, +}; + +enum cmidi2_ump_function_block_discovery_flags +{ + CMIDI2_UMP_FUNCTION_BLOCK_FILTER_INFO = 1, + CMIDI2_UMP_FUNCTION_BLOCK_FILTER_NAME = 2, +}; + +// -------- +// UMP generators + +// 7.1 UMP Stream Messages + +static inline cmidi2_ump128_t +cmidi2_ump_endpoint_discovery(cmidi2_ump_version_t version, uint8_t filterBitmap) +{ + cmidi2_ump128_t ret + = {(uint32_t)((CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_ENDPOINT_DISCOVERY << 16) + (version.major << 8) + + version.minor), + (uint32_t)(filterBitmap & 0x1F), 0, 0}; + return ret; +} + +static inline cmidi2_ump128_t cmidi2_ump_endpoint_info_notification( + cmidi2_ump_version_t version, bool isStaticFunctionBlock, uint8_t numFunctionBlocks, + bool midi2Capable, bool midi1Capable, bool rxJR, bool txJR) +{ + cmidi2_ump128_t ret = { + (uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_ENDPOINT_INFO << 16) + (version.major << 8) + version.minor, + (uint32_t)(isStaticFunctionBlock ? 1 << 31 : 0) + (numFunctionBlocks << 24) + + (midi2Capable ? 0x1000 : 0) + (midi1Capable ? 0x100 : 0) + (rxJR ? 2 : 0) + + (txJR ? 1 : 0), + 0, 0}; + return ret; +} + +static inline cmidi2_ump128_t cmidi2_ump_device_identity_notification( + uint32_t manufacturerIdIn7bitArray, uint8_t deviceFamilyLSB, uint8_t deviceFamilyMSB, + uint8_t deviceFamilyModelLSB, uint8_t deviceFamilyModelMSB, + uint32_t softwareRevisionIn7bitArray) +{ + (void)deviceFamilyModelMSB; + cmidi2_ump128_t ret + = {(uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_DEVICE_IDENTITY << 16), + manufacturerIdIn7bitArray, + (uint32_t)(deviceFamilyLSB << 24) + (deviceFamilyMSB << 16) + (deviceFamilyModelLSB << 8) + + deviceFamilyMSB, + softwareRevisionIn7bitArray}; + return ret; +} + +static inline cmidi2_ump128_t +cmidi2_ump_internal_name_notification(uint8_t statusCode, const char name[14]) +{ + cmidi2_ump128_t ret + = {(uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + (statusCode << 16) + (name[0] << 8) + + name[1], + (uint32_t)(name[2] << 24) + (name[3] << 16) + (name[4] << 8) + name[5], + (uint32_t)(name[6] << 24) + (name[7] << 16) + (name[8] << 8) + name[9], + (uint32_t)(name[10] << 24) + (name[11] << 16) + (name[12] << 8) + name[13]}; + return ret; +} + +static inline cmidi2_ump128_t cmidi2_ump_endpoint_name_notification(const char name[14]) +{ + return cmidi2_ump_internal_name_notification(CMIDI2_UMP_STREAM_STATUS_ENDPOINT_NAME, name); +} + +static inline cmidi2_ump128_t cmidi2_ump_product_instance_id_notification(const char id[14]) +{ + return cmidi2_ump_internal_name_notification(CMIDI2_UMP_STREAM_STATUS_PRODUCT_INSTANCE_ID, id); +} + +static inline cmidi2_ump128_t +cmidi2_ump_stream_configuration_request(uint8_t protocol, bool rxJR, bool txJR) +{ + cmidi2_ump128_t ret + = {(uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_STREAM_CONFIGURATION_REQUEST << 16) + (protocol << 8) + + (rxJR ? 2 : 0) + (txJR ? 1 : 0), + 0, 0, 0}; + return ret; +} + +static inline cmidi2_ump128_t +cmidi2_ump_stream_configuration_notification(uint8_t protocol, bool rxJR, bool txJR) +{ + cmidi2_ump128_t ret + = {(uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_STREAM_CONFIGURATION_NOTIFICATION << 16) + (protocol << 8) + + (rxJR ? 2 : 0) + (txJR ? 1 : 0), + 0, 0, 0}; + return ret; +} + +static inline cmidi2_ump128_t +cmidi2_ump_function_block_discovery(uint8_t numFunctionBlocks, uint8_t filter) +{ + cmidi2_ump128_t ret + = {(uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_FUNCTION_BLOCK_DISCOVERY << 16) + (numFunctionBlocks << 8) + + filter, + 0, 0, 0}; + return ret; +} + +static inline cmidi2_ump128_t cmidi2_ump_function_block_info_notification( + bool active, uint8_t numFunctionBlocks, uint8_t uiHint, uint8_t midi1, uint8_t direction, + uint8_t firstGroup, uint8_t numSpannedGroup, uint8_t midiCIVersionFormat, + uint8_t maxNumSysEx8Streams) +{ + cmidi2_ump128_t ret + = {(uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_FUNCTION_BLOCK_INFO << 16) + (active ? 0x8000 : 0) + + (numFunctionBlocks << 8) + (uiHint << 4) + (midi1 << 2) + direction, + (uint32_t)(firstGroup << 24) + (numSpannedGroup << 16) + (midiCIVersionFormat << 8) + + maxNumSysEx8Streams, + 0, 0}; + return ret; +} + +static inline cmidi2_ump128_t cmidi2_ump_function_block_name_notification(const char name[14]) +{ + return cmidi2_ump_internal_name_notification(CMIDI2_UMP_STREAM_STATUS_FUNCTION_BLOCK_NAME, name); +} + +static inline cmidi2_ump128_t cmidi2_ump_start_of_clip() +{ + cmidi2_ump128_t ret + = {(uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_START_OF_CLIP << 16), + 0, 0, 0}; + return ret; +} + +static inline cmidi2_ump128_t cmidi2_ump_end_of_clip() +{ + cmidi2_ump128_t ret + = {(uint32_t)(CMIDI2_MESSAGE_TYPE_UMP_STREAM << 28) + + (CMIDI2_UMP_STREAM_STATUS_END_OF_CLIP << 16), + 0, 0, 0}; + return ret; +} + +// 7.2 Utility Messages +static inline uint32_t cmidi2_ump_noop(uint8_t group) +{ + return (group & 0xF) << 24; +} + +static inline uint32_t cmidi2_ump_jr_clock_direct(uint8_t group, uint32_t senderClockTime) +{ + return cmidi2_ump_noop(group) + (CMIDI2_UTILITY_STATUS_JR_CLOCK << 16) + senderClockTime; +} + +static inline uint32_t cmidi2_ump_jr_clock(uint8_t group, double senderClockTime) +{ + uint16_t value = (uint16_t)(senderClockTime * JR_TIMESTAMP_TICKS_PER_SECOND); + return cmidi2_ump_noop(group) + (CMIDI2_UTILITY_STATUS_JR_CLOCK << 16) + value; +} + +static inline uint32_t cmidi2_ump_jr_timestamp_direct(uint8_t group, uint16_t senderClockTimestamp) +{ + return cmidi2_ump_noop(group) + (CMIDI2_UTILITY_STATUS_JR_TIMESTAMP << 16) + + senderClockTimestamp; +} + +static inline uint32_t cmidi2_ump_jr_timestamp(uint8_t group, double senderClockTimestamp) +{ + uint16_t value = (uint16_t)(senderClockTimestamp * JR_TIMESTAMP_TICKS_PER_SECOND); + return cmidi2_ump_noop(group) + (CMIDI2_UTILITY_STATUS_JR_TIMESTAMP << 16) + value; +} + +static inline uint32_t cmidi2_ump_dctpq(uint8_t group, uint32_t dctpq) +{ + return cmidi2_ump_noop(group) + (CMIDI2_UTILITY_STATUS_DCTPQ << 16) + dctpq; +} + +static inline uint32_t cmidi2_ump_dcs(uint8_t group, uint32_t ticks) +{ + // Note that unlike JR timestamp delta clockstamps accepts ticks up to 20 bits. + return cmidi2_ump_noop(group) + (CMIDI2_UTILITY_STATUS_DELTA_CLOCKSTAMP << 16) + + (ticks & 0xFFFFF); +} + +// 7.6 System Common and System Real Time Messages +static inline int32_t +cmidi2_ump_system_message(uint8_t group, uint8_t status, uint8_t midi1Byte2, uint8_t midi1Byte3) +{ + return (CMIDI2_MESSAGE_TYPE_SYSTEM << 28) + ((group & 0xF) << 24) + (status << 16) + + ((midi1Byte2 & 0x7F) << 8) + (midi1Byte3 & 0x7F); +} + +// 7.3 MIDI 1.0 Channel Voice Messages +static inline int32_t cmidi2_ump_midi1_message( + uint8_t group, uint8_t code, uint8_t channel, uint8_t byte3, uint8_t byte4) +{ + return (CMIDI2_MESSAGE_TYPE_MIDI_1_CHANNEL << 28) + ((group & 0xF) << 24) + + (((code & 0xF0) + (channel & 0xF)) << 16) + ((byte3 & 0x7F) << 8) + (byte4 & 0x7F); +} + +static inline int32_t +cmidi2_ump_midi1_note_off(uint8_t group, uint8_t channel, uint8_t note, uint8_t velocity) +{ + return cmidi2_ump_midi1_message( + group, CMIDI2_STATUS_NOTE_OFF, channel, note & 0x7F, velocity & 0x7F); +} + +static inline int32_t +cmidi2_ump_midi1_note_on(uint8_t group, uint8_t channel, uint8_t note, uint8_t velocity) +{ + return cmidi2_ump_midi1_message( + group, CMIDI2_STATUS_NOTE_ON, channel, note & 0x7F, velocity & 0x7F); +} + +static inline int32_t +cmidi2_ump_midi1_paf(uint8_t group, uint8_t channel, uint8_t note, uint8_t data) +{ + return cmidi2_ump_midi1_message(group, CMIDI2_STATUS_PAF, channel, note & 0x7F, data & 0x7F); +} + +static inline int32_t +cmidi2_ump_midi1_cc(uint8_t group, uint8_t channel, uint8_t index, uint8_t data) +{ + return cmidi2_ump_midi1_message(group, CMIDI2_STATUS_CC, channel, index & 0x7F, data & 0x7F); +} + +static inline int32_t cmidi2_ump_midi1_program(uint8_t group, uint8_t channel, uint8_t program) +{ + return cmidi2_ump_midi1_message( + group, CMIDI2_STATUS_PROGRAM, channel, program & 0x7F, MIDI_2_0_RESERVED); +} + +static inline int32_t cmidi2_ump_midi1_caf(uint8_t group, uint8_t channel, uint8_t data) +{ + return cmidi2_ump_midi1_message( + group, CMIDI2_STATUS_CAF, channel, data & 0x7F, MIDI_2_0_RESERVED); +} + +static inline int32_t +cmidi2_ump_midi1_pitch_bend_direct(uint8_t group, uint8_t channel, uint16_t data) +{ + return cmidi2_ump_midi1_message( + group, CMIDI2_STATUS_PITCH_BEND, channel, data & 0x7F, (data >> 7) & 0x7F); +} + +static inline int32_t +cmidi2_ump_midi1_pitch_bend_split(uint8_t group, uint8_t channel, uint8_t dataLSB, uint8_t dataMSB) +{ + return cmidi2_ump_midi1_message( + group, CMIDI2_STATUS_PITCH_BEND, channel, dataLSB & 0x7F, dataMSB & 0x7F); +} + +static inline int32_t cmidi2_ump_midi1_pitch_bend(uint8_t group, uint8_t channel, int16_t data) +{ + data += 8192; + return cmidi2_ump_midi1_message( + group, CMIDI2_STATUS_PITCH_BEND, channel, data & 0x7F, (data >> 7) & 0x7F); +} + +// 7.4 MIDI 2.0 Channel Voice Messages +static inline int64_t cmidi2_ump_midi2_channel_message_8_8_16_16( + uint8_t group, uint8_t code, uint8_t channel, uint8_t byte3, uint8_t byte4, uint16_t short1, + uint16_t short2) +{ + return (((uint64_t)(CMIDI2_MESSAGE_TYPE_MIDI_2_CHANNEL << 28) + ((group & 0xF) << 24) + + (((code & 0xF0) + (channel & 0xF)) << 16) + (byte3 << 8) + byte4) + << 32) + + ((uint64_t)short1 << 16) + short2; +} + +static inline int64_t cmidi2_ump_midi2_channel_message_8_8_32( + uint8_t group, uint8_t code, uint8_t channel, uint8_t byte3, uint8_t byte4, uint32_t rest) +{ + return (((uint64_t)(CMIDI2_MESSAGE_TYPE_MIDI_2_CHANNEL << 28) + ((group & 0xF) << 24) + + (((code & 0xF0) + (channel & 0xF)) << 16) + (byte3 << 8) + byte4) + << 32) + + rest; +} + +static inline uint16_t cmidi2_ump_pitch_7_9(double semitone) +{ + double actual = semitone < 0.0 ? 0.0 : semitone >= 128.0 ? 128.0 : semitone; + uint16_t dec = (uint16_t)actual; + double microtone = actual - dec; + return (dec << 9) + (int)(microtone * 512.0); +} + +static inline uint16_t cmidi2_ump_pitch_7_9_split(uint8_t semitone, double microtone) +{ + uint16_t ret = (uint16_t)(semitone & 0x7F) << 9; + double actual = microtone < 0.0 ? 0.0 : microtone > 1.0 ? 1.0 : microtone; + ret += (int)(actual * 512.0); + return ret; +} + +static inline int64_t cmidi2_ump_midi2_note_off( + uint8_t group, uint8_t channel, uint8_t note, uint8_t attributeType, uint16_t velocity, + uint16_t attributeData) +{ + return cmidi2_ump_midi2_channel_message_8_8_16_16( + group, CMIDI2_STATUS_NOTE_OFF, channel, note & 0x7F, attributeType, velocity, attributeData); +} + +static inline int64_t cmidi2_ump_midi2_note_on( + uint8_t group, uint8_t channel, uint8_t note, uint8_t attributeType, uint16_t velocity, + uint16_t attributeData) +{ + return cmidi2_ump_midi2_channel_message_8_8_16_16( + group, CMIDI2_STATUS_NOTE_ON, channel, note & 0x7F, attributeType, velocity, attributeData); +} + +static inline int64_t +cmidi2_ump_midi2_paf(uint8_t group, uint8_t channel, uint8_t note, uint32_t data) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_PAF, channel, note & 0x7F, MIDI_2_0_RESERVED, data); +} + +static inline int64_t cmidi2_ump_midi2_per_note_rcc( + uint8_t group, uint8_t channel, uint8_t note, uint8_t index, uint32_t data) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_PER_NOTE_RCC, channel, note & 0x7F, index, data); +} + +static inline int64_t cmidi2_ump_midi2_per_note_acc( + uint8_t group, uint8_t channel, uint8_t note, uint8_t index, uint32_t data) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_PER_NOTE_ACC, channel, note & 0x7F, index, data); +} + +static inline int64_t cmidi2_ump_midi2_per_note_management( + uint8_t group, uint8_t channel, uint8_t note, uint8_t optionFlags) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_PER_NOTE_MANAGEMENT, channel, note & 0x7F, optionFlags & 3, 0); +} + +static inline int64_t +cmidi2_ump_midi2_cc(uint8_t group, uint8_t channel, uint8_t index, uint32_t data) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_CC, channel, index & 0x7F, MIDI_2_0_RESERVED, data); +} + +static inline int64_t cmidi2_ump_midi2_rpn( + uint8_t group, uint8_t channel, uint8_t bankAkaMSB, uint8_t indexAkaLSB, uint32_t dataAkaDTE) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_RPN, channel, bankAkaMSB & 0x7F, indexAkaLSB & 0x7F, dataAkaDTE); +} + +static inline int64_t cmidi2_ump_midi2_nrpn( + uint8_t group, uint8_t channel, uint8_t bankAkaMSB, uint8_t indexAkaLSB, uint32_t dataAkaDTE) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_NRPN, channel, bankAkaMSB & 0x7F, indexAkaLSB & 0x7F, dataAkaDTE); +} + +static inline int64_t cmidi2_ump_midi2_relative_rpn( + uint8_t group, uint8_t channel, uint8_t bankAkaMSB, uint8_t indexAkaLSB, uint32_t dataAkaDTE) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_RELATIVE_RPN, channel, bankAkaMSB & 0x7F, indexAkaLSB & 0x7F, + dataAkaDTE); +} + +static inline int64_t cmidi2_ump_midi2_relative_nrpn( + uint8_t group, uint8_t channel, uint8_t bankAkaMSB, uint8_t indexAkaLSB, uint32_t dataAkaDTE) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_RELATIVE_NRPN, channel, bankAkaMSB & 0x7F, indexAkaLSB & 0x7F, + dataAkaDTE); +} + +static inline int64_t cmidi2_ump_midi2_program( + uint8_t group, uint8_t channel, uint8_t optionFlags, uint8_t program, uint8_t bankMSB, + uint8_t bankLSB) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_PROGRAM, channel, MIDI_2_0_RESERVED, optionFlags & 1, + ((program & 0x7F) << 24) + (bankMSB << 8) + bankLSB); +} + +static inline int64_t cmidi2_ump_midi2_caf(uint8_t group, uint8_t channel, uint32_t data) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_CAF, channel, MIDI_2_0_RESERVED, MIDI_2_0_RESERVED, data); +} + +static inline int64_t +cmidi2_ump_midi2_pitch_bend_direct(uint8_t group, uint8_t channel, uint32_t unsignedData) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_PITCH_BEND, channel, MIDI_2_0_RESERVED, MIDI_2_0_RESERVED, + unsignedData); +} + +static inline int64_t cmidi2_ump_midi2_pitch_bend(uint8_t group, uint8_t channel, int32_t data) +{ + return cmidi2_ump_midi2_pitch_bend_direct(group, channel, 0x80000000 + data); +} + +static inline int64_t cmidi2_ump_midi2_per_note_pitch_bend_direct( + uint8_t group, uint8_t channel, uint8_t note, uint32_t data) +{ + return cmidi2_ump_midi2_channel_message_8_8_32( + group, CMIDI2_STATUS_PER_NOTE_PITCH_BEND, channel, note & 0x7F, MIDI_2_0_RESERVED, data); +} + +static inline int64_t +cmidi2_ump_midi2_per_note_pitch_bend(uint8_t group, uint8_t channel, uint8_t note, uint32_t data) +{ + return cmidi2_ump_midi2_per_note_pitch_bend_direct(group, channel, note, 0x80000000 + data); +} + +// Common utility functions for sysex support + +static inline uint8_t cmidi2_ump_get_byte_from_uint32(uint32_t src, uint8_t index) +{ + return (uint8_t)(src >> ((7 - index) * 8) & 0xFF); +} + +static inline uint8_t cmidi2_ump_get_byte_from_uint64(uint64_t src, uint8_t index) +{ + return (uint8_t)(src >> ((7 - index) * 8) & 0xFF); +} + +static inline uint8_t cmidi2_ump_sysex_get_num_packets(uint8_t numBytes, uint8_t radix) +{ + return numBytes <= radix ? 1 : numBytes / radix + (numBytes % radix ? 1 : 0); +} + +static inline uint32_t cmidi2_ump_read_uint32_bytes_le(const void* sequence) +{ + const uint8_t* bytes = (const uint8_t*)sequence; + uint32_t ret = 0; + for (int i = 0; i < 4; i++) + ret += ((uint32_t)bytes[i]) << (i * 8); + return ret; +} + +static inline uint32_t cmidi2_ump_read_uint32_bytes_be(const void* sequence) +{ + const uint8_t* bytes = (const uint8_t*)sequence; + uint32_t ret = 0; + for (int i = 0; i < 4; i++) + ret += ((uint32_t)bytes[i]) << ((3 - i) * 8); + return ret; +} + +static inline bool cmidi2_util_is_platform_little_endian() +{ + int i = 1; + return *(char*)&i; +} + +static inline uint32_t cmidi2_ump_read_uint32_bytes(const void* sequence) +{ + return cmidi2_util_is_platform_little_endian() ? cmidi2_ump_read_uint32_bytes_le(sequence) + : cmidi2_ump_read_uint32_bytes_be(sequence); +} + +static inline uint64_t cmidi2_ump_read_uint64_bytes_le(const void* sequence) +{ + return ((uint64_t)cmidi2_ump_read_uint32_bytes_le(sequence) << 32) + + cmidi2_ump_read_uint32_bytes_le((const uint8_t*)sequence + 4); +} + +static inline uint64_t cmidi2_ump_read_uint64_bytes_be(const void* sequence) +{ + return ((uint64_t)cmidi2_ump_read_uint32_bytes_be(sequence) << 32) + + cmidi2_ump_read_uint32_bytes_be((const uint8_t*)sequence + 4); +} + +static inline uint64_t cmidi2_ump_read_uint64_bytes(const void* sequence) +{ + return cmidi2_util_is_platform_little_endian() ? cmidi2_ump_read_uint64_bytes_le(sequence) + : cmidi2_ump_read_uint64_bytes_be(sequence); +} + +static inline void cmidi2_ump_sysex_get_packet_of( + uint64_t* result1, uint64_t* result2, uint8_t group, uint8_t numBytes, const void* srcData, + int32_t index, enum cmidi2_message_type messageType, int radix, bool hasStreamId, + uint8_t streamId) +{ + uint8_t dst8[16]; + memset(dst8, 0, 16); + const uint8_t* src8 = (const uint8_t*)srcData; + + dst8[0] = (messageType << 4) + (group & 0xF); + + enum cmidi2_sysex_status status; + uint8_t size; + if (numBytes <= radix) + { + status = CMIDI2_SYSEX_IN_ONE_UMP; + size = numBytes; // single packet message + } + else if (index == 0) + { + status = CMIDI2_SYSEX_START; + size = radix; + } + else + { + uint8_t isEnd = index == cmidi2_ump_sysex_get_num_packets(numBytes, radix) - 1; + if (isEnd) + { + size = numBytes % radix ? numBytes % radix : radix; + status = CMIDI2_SYSEX_END; + } + else + { + size = radix; + status = CMIDI2_SYSEX_CONTINUE; + } + } + dst8[1] = status + size + (hasStreamId ? 1 : 0); + + if (hasStreamId) + dst8[2] = streamId; + + uint8_t dstOffset = hasStreamId ? 3 : 2; + for (uint8_t i = 0, j = index * radix; i < size; i++, j++) + dst8[i + dstOffset] = src8[j]; + + *result1 = cmidi2_ump_read_uint64_bytes_be(dst8); + if (result2) + *result2 = cmidi2_ump_read_uint64_bytes_be(dst8 + 8); +} + +// 7.7 System Exclusive 7-Bit Messages +static inline uint64_t cmidi2_ump_sysex7_direct( + uint8_t group, uint8_t status, uint8_t numBytes, uint8_t data1, uint8_t data2, uint8_t data3, + uint8_t data4, uint8_t data5, uint8_t data6) +{ + return (((uint64_t)((CMIDI2_MESSAGE_TYPE_SYSEX7 << 28) + ((group & 0xF) << 24) + + ((status + numBytes) << 16))) + << 32) + + ((uint64_t)data1 << 40) + ((uint64_t)data2 << 32) + (data3 << 24) + (data4 << 16) + + (data5 << 8) + data6; +} + +static inline uint32_t cmidi2_ump_sysex7_get_sysex_length(const void* srcData) +{ + int i = 0; + const uint8_t* csrc = (const uint8_t*)srcData; + while (csrc[i] != 0xF7) + i++; + /* This function automatically detects if 0xF0 is prepended and reduce length if it is. */ + return i - (csrc[0] == 0xF0 ? 1 : 0); +} + +static inline uint8_t cmidi2_ump_sysex7_get_num_packets(uint8_t numSysex7Bytes) +{ + return cmidi2_ump_sysex_get_num_packets(numSysex7Bytes, 6); +} + +static inline uint64_t cmidi2_ump_sysex7_get_packet_of( + uint8_t group, uint8_t numBytes, const void* srcData, int32_t index) +{ + uint64_t result; + int srcOffset = numBytes > 0 && ((const uint8_t*)srcData)[0] == 0xF0 ? 1 : 0; + cmidi2_ump_sysex_get_packet_of( + &result, NULL, group, numBytes, (const uint8_t*)srcData + srcOffset, index, + CMIDI2_MESSAGE_TYPE_SYSEX7, 6, false, 0); + return result; +} + +/* process() - more complicated function */ + +// This returns NULL for success, or anything else for failure. +typedef void* (*cmidi2_ump_handler_u64)(uint64_t data, void* context); + +// This returns NULL for success, or anything else that `sendUMP` returns for failure. +static inline void* cmidi2_ump_sysex7_process( + uint8_t group, void* sysex, cmidi2_ump_handler_u64 sendUMP, void* context) +{ + int32_t length = cmidi2_ump_sysex7_get_sysex_length(sysex); + int32_t numPackets = cmidi2_ump_sysex7_get_num_packets(length); + for (int p = 0; p < numPackets; p++) + { + int64_t ump = cmidi2_ump_sysex7_get_packet_of(group, length, sysex, p); + void* retCode = sendUMP(ump, context); + if (retCode != 0) + return retCode; + } + return NULL; +} + +// 7.8 System Exclusive 8-Bit Messages + +static inline int8_t cmidi2_ump_sysex8_get_num_packets(uint8_t numBytes) +{ + return cmidi2_ump_sysex_get_num_packets(numBytes, 13); +} + +static inline void cmidi2_ump_sysex8_get_packet_of( + uint8_t group, uint8_t streamId, uint8_t numBytes, const void* srcData, int32_t index, + uint64_t* result1, uint64_t* result2) +{ + cmidi2_ump_sysex_get_packet_of( + result1, result2, group, numBytes, srcData, index, CMIDI2_MESSAGE_TYPE_SYSEX8_MDS, 13, true, + streamId); +} + +/* process() - more complicated function */ + +// This returns NULL for success, or anything else for failure. +typedef void* (*cmidi2_ump_handler_u128)( + uint64_t data1, uint64_t data2, size_t index, void* context); + +// This returns NULL for success, or anything else that `sendUMP` returns for failure. +static inline void* cmidi2_ump_sysex8_process( + uint8_t group, void* sysex, uint32_t length, uint8_t streamId, cmidi2_ump_handler_u128 sendUMP, + void* context) +{ + uint32_t numPackets = cmidi2_ump_sysex8_get_num_packets(length); + for (size_t p = 0; p < numPackets; p++) + { + uint64_t result1, result2; + cmidi2_ump_sysex8_get_packet_of(group, streamId, length, sysex, p, &result1, &result2); + void* retCode = sendUMP(result1, result2, p, context); + if (retCode != 0) + return retCode; + } + return NULL; +} + +// 7.9 Mixed Data Set Message + +static inline uint16_t cmidi2_ump_mds_get_num_chunks(uint32_t numTotalBytesInMDS) +{ + uint32_t radix = 14 * 0x10000; + return numTotalBytesInMDS / radix + (numTotalBytesInMDS % radix ? 1 : 0); +} + +// Returns -1 if input is out of range +static inline int32_t cmidi2_ump_mds_get_num_payloads(uint32_t numTotalBytesinChunk) +{ + if (numTotalBytesinChunk > 14 * 65535) + return -1; + return numTotalBytesinChunk / 14 + (numTotalBytesinChunk % 14 ? 1 : 0); +} + +static inline void cmidi2_ump_mds_get_header( + uint8_t group, uint8_t mdsId, uint16_t numBytesInChunk, uint16_t numChunks, + uint16_t chunkIndex, uint16_t manufacturerId, uint16_t deviceId, uint16_t subId, + uint16_t subId2, uint64_t* result1, uint64_t* result2) +{ + uint8_t dst8[16]; + memset(dst8, 0, 16); + + dst8[0] = (CMIDI2_MESSAGE_TYPE_SYSEX8_MDS << 4) + (group & 0xF); + dst8[1] = CMIDI2_MIXED_DATA_STATUS_HEADER + mdsId; + *((uint16_t*)(void*)(dst8 + 2)) = numBytesInChunk; + *((uint16_t*)(void*)(dst8 + 4)) = numChunks; + *((uint16_t*)(void*)(dst8 + 6)) = chunkIndex; + *((uint16_t*)(void*)(dst8 + 8)) = manufacturerId; + *((uint16_t*)(void*)(dst8 + 10)) = deviceId; + *((uint16_t*)(void*)(dst8 + 12)) = subId; + *((uint16_t*)(void*)(dst8 + 14)) = subId2; + + *result1 = cmidi2_ump_read_uint64_bytes_be(dst8); + if (result2) + *result2 = cmidi2_ump_read_uint64_bytes_be(dst8 + 8); +} + +// srcData points to exact start of the source data. +static inline void cmidi2_ump_mds_get_payload_of( + uint8_t group, uint8_t mdsId, uint16_t numBytes, const void* srcData, uint64_t* result1, + uint64_t* result2) +{ + uint8_t dst8[16]; + memset(dst8, 0, 16); + const uint8_t* src8 = (const uint8_t*)srcData; + + dst8[0] = (CMIDI2_MESSAGE_TYPE_SYSEX8_MDS << 4) + (group & 0xF); + dst8[1] = CMIDI2_MIXED_DATA_STATUS_PAYLOAD + mdsId; + + uint8_t radix = 14; + uint8_t size = numBytes < radix ? numBytes % radix : radix; + + for (uint8_t i = 0; i < size; i++) + dst8[i + 2] = src8[i]; + + *result1 = cmidi2_ump_read_uint64_bytes_be(dst8); + if (result2) + *result2 = cmidi2_ump_read_uint64_bytes_be(dst8 + 8); +} + +/* process() - more complicated function */ +// This returns NULL for success, or anything else for failure. +typedef void* (*cmidi2_mds_handler)( + uint64_t data1, uint64_t data2, size_t chunkId, size_t payloadId, void* context); + +// This returns NULL for success, or anything else that `sendUMP` returns for failure. +static inline void* cmidi2_ump_mds_process( + uint8_t group, uint8_t mdsId, void* data, uint32_t length, cmidi2_mds_handler sendUMP, + void* context) +{ + int32_t numChunks = cmidi2_ump_mds_get_num_chunks(length); + for (int c = 0; c < numChunks; c++) + { + int32_t maxChunkSize = 14 * 65535; + int32_t chunkSize = c + 1 == numChunks ? (int32_t)(length % maxChunkSize) : maxChunkSize; + int32_t numPayloads = cmidi2_ump_mds_get_num_payloads(chunkSize); + for (int p = 0; p < numPayloads; p++) + { + uint64_t result1, result2; + size_t offset = 14 * (65536 * c + p); + cmidi2_ump_mds_get_payload_of( + group, mdsId, chunkSize, (uint8_t*)data + offset, &result1, &result2); + void* retCode = sendUMP(result1, result2, c, p, context); + if (retCode) + return retCode; + } + } + return NULL; +} + +// 7.5 Flex Data Message + +static inline uint16_t cmidi2_ump_flex_data_get_num_packets(uint32_t numTotalBytesInFlexData) +{ + return numTotalBytesInFlexData / 12 + (numTotalBytesInFlexData % 12 ? 1 : 0); +} + +static inline void cmidi2_ump_flex_data_complete_packet( + uint8_t group, uint8_t addressing, uint8_t channel, uint8_t statusBank, uint8_t statusCode, + uint32_t data1, uint32_t data2, uint32_t data3, uint64_t* result1, uint64_t* result2) +{ + uint8_t dst8[4]; + memset(dst8, 0, 4); + dst8[0] = (CMIDI2_MESSAGE_TYPE_FLEX_DATA << 4) + (group & 0xF); + dst8[1] = (CMIDI2_SYSEX_IN_ONE_UMP << 2) + ((addressing & 0x3) << 4) + (channel & 0xF); + dst8[2] = statusBank; + dst8[3] = statusCode; + + *result1 = ((uint64_t)cmidi2_ump_read_uint32_bytes_be(dst8) << 32) + data1; + *result2 = ((uint64_t)data2 << 32) + data3; +} + +static inline void cmidi2_ump_flex_data_get_packet_of( + uint8_t group, uint8_t addressing, uint8_t channel, uint8_t statusBank, uint8_t statusCode, + uint16_t numBytes, const void* srcData, int32_t currentPacket, uint64_t* result1, + uint64_t* result2) +{ + uint8_t dst8[16]; + memset(dst8, 0, 16); + const uint8_t* src8 = (const uint8_t*)srcData; + + dst8[0] = (CMIDI2_MESSAGE_TYPE_FLEX_DATA << 4) + (group & 0xF); + + const size_t radix = 12; + enum cmidi2_sysex_status format; + uint8_t size; + if (numBytes <= radix) + { + format = CMIDI2_SYSEX_IN_ONE_UMP; + size = numBytes; // single packet message + } + else if (currentPacket == 0) + { + format = CMIDI2_SYSEX_START; + size = radix; + } + else + { + uint8_t isEnd = currentPacket == cmidi2_ump_sysex_get_num_packets(numBytes, radix) - 1; + if (isEnd) + { + size = numBytes % radix ? numBytes % radix : radix; + format = CMIDI2_SYSEX_END; + } + else + { + size = radix; + format = CMIDI2_SYSEX_CONTINUE; + } + } + dst8[1] = (format << 2) + ((addressing & 0x3) << 4) + (channel & 0xF); + dst8[2] = statusBank; + dst8[3] = statusCode; + + for (uint8_t i = 0; i < size; i++) + dst8[i + 4] = src8[i]; + + *result1 = cmidi2_ump_read_uint64_bytes_be(dst8); + *result2 = cmidi2_ump_read_uint64_bytes_be(dst8 + 8); +} + +/* process() - more complicated function */ +// This returns NULL for success, or anything else for failure. +typedef void* (*cmidi2_flex_data_handler)(uint64_t data1, uint64_t data2, void* context); + +// This returns NULL for success, or anything else that `sendUMP` returns for failure. +// `text` is usually a null-terminated text string, but it may contain `\0` as Melisma in lyricText. Hence we still need `length`. +static inline void* cmidi2_ump_flex_data_process( + uint8_t group, uint8_t addressing, uint8_t channel, uint8_t statusBank, uint8_t statusCode, + const char* text, uint32_t length, cmidi2_flex_data_handler sendUMP, void* context) +{ + int32_t numPackets = cmidi2_ump_flex_data_get_num_packets(length); + for (int p = 0; p < numPackets; p++) + { + uint64_t result1, result2; + cmidi2_ump_flex_data_get_packet_of( + group, addressing, channel, statusBank, statusCode, length, text, p, &result1, &result2); + void* retCode = sendUMP(result1, result2, context); + if (retCode != 0) + return retCode; + } + return NULL; +} + +// individual flex data message generators + +static inline void cmidi2_ump_flex_data_set_tempo_direct( + uint8_t group, uint8_t channel, uint32_t tempoIn10NanosecondsPerQN, uint64_t* result1, + uint64_t* result2) +{ + cmidi2_ump_flex_data_complete_packet( + group, 1, channel, CMIDI2_FLEX_DATA_BANK_SETUP_AND_PERFORMANCE, + CMIDI2_FLEX_DATA_STATUS_SET_TEMPO, tempoIn10NanosecondsPerQN, 0, 0, result1, result2); +} + +static inline void cmidi2_ump_flex_data_set_time_signature( + uint8_t group, uint8_t channel, uint8_t numerator, uint8_t denominator, + uint8_t numberOf32thNotes, uint64_t* result1, uint64_t* result2) +{ + cmidi2_ump_flex_data_complete_packet( + group, 1, channel, CMIDI2_FLEX_DATA_BANK_SETUP_AND_PERFORMANCE, + CMIDI2_FLEX_DATA_STATUS_SET_TIME_SIGNATURE, + (numerator << 24) + (denominator << 16) + (numberOf32thNotes << 8), 0, 0, result1, result2); +} + +static inline void cmidi2_ump_flex_data_set_metronome( + uint8_t group, uint8_t channel, uint8_t clocksPerPrimaryClick, uint8_t barAccent1, + uint8_t barAccent2, uint8_t barAccent3, uint8_t subDivisionClicks1, uint8_t subDivisionClicks2, + uint64_t* result1, uint64_t* result2) +{ + cmidi2_ump_flex_data_complete_packet( + group, 1, channel, CMIDI2_FLEX_DATA_BANK_SETUP_AND_PERFORMANCE, + CMIDI2_FLEX_DATA_STATUS_SET_METRONOME, + (clocksPerPrimaryClick << 24) + (barAccent1 << 16) + (barAccent2 << 8) + barAccent3, + (subDivisionClicks1 << 24) + (subDivisionClicks2 << 16), 0, result1, result2); +} + +static inline void cmidi2_ump_flex_data_set_key_signature( + uint8_t group, uint8_t addressing, uint8_t channel, uint8_t sharpsFlats, uint8_t tonicNote, + uint64_t* result1, uint64_t* result2) +{ + cmidi2_ump_flex_data_complete_packet( + group, addressing, channel, CMIDI2_FLEX_DATA_BANK_SETUP_AND_PERFORMANCE, + CMIDI2_FLEX_DATA_STATUS_SET_KEY_SIGNATURE, (sharpsFlats << 24) + (tonicNote << 16), 0, 0, + result1, result2); +} + +static inline void cmidi2_ump_flex_data_set_chord_name( + uint8_t group, uint8_t addressing, uint8_t channel, uint8_t sharpsFlats, uint8_t chordTonic, + uint8_t chordType, uint8_t alter1Type, uint8_t alter1Degree, uint8_t alter2Type, + uint8_t alter2Degree, uint8_t alter3Type, uint8_t alter3Degree, uint8_t alter4Type, + uint8_t alter4Degree, uint8_t bassSharpsFlats, uint8_t bassNote, uint8_t bassChordType, + uint8_t bassAlter1Type, uint8_t bassAlter1Degree, uint8_t bassAlter2Type, + uint8_t bassAlter2Degree, uint64_t* result1, uint64_t* result2) +{ + cmidi2_ump_flex_data_complete_packet( + group, addressing, channel, CMIDI2_FLEX_DATA_BANK_SETUP_AND_PERFORMANCE, + CMIDI2_FLEX_DATA_STATUS_SET_CHORD_NAME, + (sharpsFlats << 28) + (chordTonic << 24) + (chordType << 16) + (alter1Type << 12) + + (alter1Degree << 8) + (alter2Type << 4) + alter2Degree, + (alter3Type << 28) + (alter3Degree << 24) + (alter4Type << 20) + (alter4Degree << 16), + (bassSharpsFlats << 28) + (bassNote << 24) + (bassChordType << 16) + (bassAlter1Type << 12) + + (bassAlter1Degree << 8) + (bassAlter2Type << 4) + bassAlter2Degree, + result1, result2); +} + +// -------- +// Strongly-typed(?) UMP. +// I kind of think those getters are overkill, so I would collect almost use `cmidi2_ump_get_xxx()` +// as those strongly typed functions, so that those who don't want them can safely ignore them. + +typedef uint32_t cmidi2_ump; + +static inline void cmidi2_ump_write32(cmidi2_ump* dst, uint32_t value) +{ + dst[0] = value; +} + +static inline void cmidi2_ump_write64(cmidi2_ump* dst, uint64_t value) +{ + dst[0] = value >> 32; + dst[1] = value & 0xFFFFFFFF; +} + +static inline void cmidi2_ump_write128(cmidi2_ump* dst, uint64_t value1, uint64_t value2) +{ + dst[0] = value1 >> 32; + dst[1] = value1 & 0xFFFFFFFF; + dst[2] = value2 >> 32; + dst[3] = value2 & 0xFFFFFFFF; +} + +static inline uint8_t cmidi2_ump_get_byte_at(const cmidi2_ump* ump, uint8_t at) +{ + ump += at / 4; + switch (at % 4) + { + case 0: + return (*ump & 0xFF000000) >> 24; + case 1: + return (*ump & 0xFF0000) >> 16; + case 2: + return (*ump & 0xFF00) >> 8; + case 3: + return *ump & 0xFF; + } + return 0; // This is unexpected. +} + +static inline uint8_t cmidi2_ump_get_message_type(const cmidi2_ump* ump) +{ + return *ump >> 28; +} + +static inline uint8_t cmidi2_ump_get_message_size_bytes(const cmidi2_ump* ump) +{ + switch (cmidi2_ump_get_message_type(ump)) + { + case CMIDI2_MESSAGE_TYPE_UTILITY: + case CMIDI2_MESSAGE_TYPE_SYSTEM: + case CMIDI2_MESSAGE_TYPE_MIDI_1_CHANNEL: + return 4; + case CMIDI2_MESSAGE_TYPE_SYSEX7: + case CMIDI2_MESSAGE_TYPE_MIDI_2_CHANNEL: + return 8; + case CMIDI2_MESSAGE_TYPE_SYSEX8_MDS: + return 16; + } + return 0; // invalid +} + +static inline uint8_t cmidi2_ump_get_group(const cmidi2_ump* ump) +{ + return (*ump >> 24) & 0xF; +} + +static inline uint8_t cmidi2_ump_get_status_code(const cmidi2_ump* ump) +{ + return (*ump >> 16) & 0xF0; +} + +static inline uint8_t cmidi2_ump_get_channel(const cmidi2_ump* ump) +{ + return (*ump >> 16) & 0xF; +} + +static inline uint32_t cmidi2_ump_get_32_to_64(const cmidi2_ump* ump) +{ + return *(ump + 1); +} + +static inline uint16_t cmidi2_ump_get_jr_clock_time(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 2) << 8) + cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint16_t cmidi2_ump_get_jr_timestamp_timestamp(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 2) << 8) + cmidi2_ump_get_byte_at(ump, 3); +} + +static inline uint16_t cmidi2_ump_get_dctpq(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 2) << 8) + cmidi2_ump_get_byte_at(ump, 3); +} + +static inline uint16_t cmidi2_ump_get_dcs(const cmidi2_ump* ump) +{ + return ((cmidi2_ump_get_byte_at(ump, 1) & 0xF) << 16) + (cmidi2_ump_get_byte_at(ump, 2) << 8) + + cmidi2_ump_get_byte_at(ump, 3); +} + +static inline uint8_t cmidi2_ump_get_system_message_byte2(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_system_message_byte3(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} + +static inline uint8_t cmidi2_ump_get_midi1_byte2(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_midi1_byte3(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} + +static inline uint8_t cmidi2_ump_get_midi1_note_note(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_midi1_note_velocity(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint8_t cmidi2_ump_get_midi1_paf_note(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_midi1_paf_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint8_t cmidi2_ump_get_midi1_cc_index(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_midi1_cc_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint8_t cmidi2_ump_get_midi1_program_program(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_midi1_caf_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint16_t cmidi2_ump_get_midi1_pitch_bend_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2) + cmidi2_ump_get_byte_at(ump, 3) * 0x80; +} + +static inline uint8_t cmidi2_ump_get_sysex7_num_bytes(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_channel(ump); // same bits +} + +static inline uint8_t cmidi2_ump_get_midi2_note_note(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_midi2_note_attribute_type(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint16_t cmidi2_ump_get_midi2_note_velocity(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 4) << 8) + (cmidi2_ump_get_byte_at(ump, 5)); +} +static inline uint16_t cmidi2_ump_get_midi2_note_attribute_data(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 6) << 8) + (cmidi2_ump_get_byte_at(ump, 7)); +} +static inline uint8_t cmidi2_ump_get_midi2_paf_note(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint32_t cmidi2_ump_get_midi2_paf_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_32_to_64(ump); +} +static inline uint8_t cmidi2_ump_get_midi2_pnrcc_note(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_midi2_pnrcc_index(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint32_t cmidi2_ump_get_midi2_pnrcc_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_32_to_64(ump); +} +static inline uint8_t cmidi2_ump_get_midi2_pnacc_note(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_midi2_pnacc_index(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint32_t cmidi2_ump_get_midi2_pnacc_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_32_to_64(ump); +} +static inline uint32_t cmidi2_ump_get_midi2_pn_management_note(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint32_t cmidi2_ump_get_midi2_pn_management_options(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint8_t cmidi2_ump_get_midi2_cc_index(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint32_t cmidi2_ump_get_midi2_cc_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_32_to_64(ump); +} +// absolute RPN or relative RPN +static inline uint8_t cmidi2_ump_get_midi2_rpn_msb(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +// absolute RPN or relative RPN +static inline uint8_t cmidi2_ump_get_midi2_rpn_lsb(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +// absolute RPN or relative RPN +static inline uint32_t cmidi2_ump_get_midi2_rpn_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_32_to_64(ump); +} +// absolute NRPN or relative NRPN +static inline uint8_t cmidi2_ump_get_midi2_nrpn_msb(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +// absolute NRPN or relative NRPN +static inline uint8_t cmidi2_ump_get_midi2_nrpn_lsb(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +// absolute NRPN or relative NRPN +static inline uint32_t cmidi2_ump_get_midi2_nrpn_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_32_to_64(ump); +} +static inline uint8_t cmidi2_ump_get_midi2_program_options(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint8_t cmidi2_ump_get_midi2_program_program(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 4); +} +static inline uint8_t cmidi2_ump_get_midi2_program_bank_msb(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 6)); +} +static inline uint8_t cmidi2_ump_get_midi2_program_bank_lsb(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 7)); +} +static inline uint32_t cmidi2_ump_get_midi2_caf_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_32_to_64(ump); +} +// either per-note or channel +static inline uint32_t cmidi2_ump_get_midi2_pitch_bend_data(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_32_to_64(ump); +} +static inline uint8_t cmidi2_ump_get_midi2_pn_pitch_bend_note(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} + +static inline uint8_t cmidi2_ump_get_sysex8_num_bytes(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_channel(ump); // same bits +} +static inline uint8_t cmidi2_ump_get_sysex8_stream_id(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_byte_at(ump, 2); +} +static inline uint8_t cmidi2_ump_get_mds_mds_id(const cmidi2_ump* ump) +{ + return cmidi2_ump_get_channel(ump); // same bits +} +static inline uint16_t cmidi2_ump_get_mds_num_chunk_bytes(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 2) << 8) + cmidi2_ump_get_byte_at(ump, 3); +} +static inline uint16_t cmidi2_ump_get_mds_num_chunks(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 4) << 8) + cmidi2_ump_get_byte_at(ump, 5); +} +static inline uint16_t cmidi2_ump_get_mds_chunk_index(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 6) << 8) + cmidi2_ump_get_byte_at(ump, 7); +} +static inline uint16_t cmidi2_ump_get_mds_manufacturer_id(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 8) << 8) + cmidi2_ump_get_byte_at(ump, 9); +} +static inline uint16_t cmidi2_ump_get_mds_device_id(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 10) << 8) + cmidi2_ump_get_byte_at(ump, 11); +} +static inline uint16_t cmidi2_ump_get_mds_sub_id_1(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 12) << 8) + cmidi2_ump_get_byte_at(ump, 13); +} +static inline uint16_t cmidi2_ump_get_mds_sub_id_2(const cmidi2_ump* ump) +{ + return (cmidi2_ump_get_byte_at(ump, 14) << 8) + cmidi2_ump_get_byte_at(ump, 15); +} + +// -------- +// Realtime-safe SysEx8 binary reader +// +// The primary end-user facing function is `cmidi2_ump_get_sysex8_data()`, but the +// function signature needs some explanation as it requires some complicated processing. +// +// There can be multiple SysEx8 "streams" that simultaneously run within a UMP stream +// (e.g. sysex8 start packet for stream #1, sysex8 continue packet for stream #1, +// sysex8 start packet for stream #2, sysex8 continue packet for stream #1, +// sysex8 continue packet for stream #2, sysex8 complete packet for stream #3, +// sysex8 end packet for stream #2, sysex8 end packet for strean #1 ...). +// +// Those stream state must be preserved during UMP parsing. It is represented as +// `cmidi2_ump_binary_read_state` struct. +// +// There are handful of usage scenarios where we... +// - want to handle them all +// - want to handle only relevant ones +// - can control what we generate and determine that only one stream can appear so +// the context stream is always obvious +// +// To handle them all, we need "stream selector". There is a function type +// `cmidi2_ump_stream_selector_func`, and a function pointer is passed to the entry +// point `cmidi2_ump_get_sysex8_data()` function. +// +// Another control point is where we want to finish parsing sysex8. Whenever a relevant +// stream is parsed, `cmidi2_ump_get_syex8_data()` calls "continuity checker" function +// which is typed as `cmidi2_ump_binary_read_continuity_checker_func`. +// If this function returns true, then it completes parsing, returning the number of +// the parsed UMPs (in uint32_t length). +// +// See `testType5Messages_sysex8_reader_writer()` testcase for the actual usage example. +// + +enum cmidi2_ump_binary_reader_result_code +{ + CMIDI2_BINARY_READER_RESULT_INCOMPLETE = 0, + CMIDI2_BINARY_READER_RESULT_COMPLETE = 1, + CMIDI2_BINARY_READER_RESULT_NO_SPACE = 2, +}; + +typedef struct cmidi2_ump_binary_read_state +{ + void* context; + uint8_t* data; + size_t dataCapacity; + size_t dataSize; + bool continueOnCompletion; + enum cmidi2_ump_binary_reader_result_code resultCode; +} cmidi2_ump_binary_read_state; + +static inline void cmidi2_ump_binary_read_state_reset(cmidi2_ump_binary_read_state* state) +{ + state->dataSize = 0; + state->resultCode = CMIDI2_BINARY_READER_RESULT_INCOMPLETE; +} + +static inline void cmidi2_ump_binary_read_state_init( + cmidi2_ump_binary_read_state* state, void* context, uint8_t* dataBuffer, size_t dataCapacity, + bool continueOnCompletion) +{ + state->context = context; + state->data = dataBuffer; + state->dataCapacity = dataCapacity; + state->continueOnCompletion = continueOnCompletion; + cmidi2_ump_binary_read_state_reset(state); +} + +/// Binary stream selector for cmidi2_ump_get_sysex8_data() function. +/// If you do not support simultaneous streams, just return a reference to a fixed `cmidi2_ump_binary_read_state` instance. +/// If the targetSteramId does not indicate the streams the client handles, then it should return NULL. +/// Note that the resulting state must contain a non-null data and valid dataCapacity i.e. it must be already assigned. +/// Also note that the function implementation should not try to allocate a new instance of state +/// - otherwise it will break realtime safety. +typedef cmidi2_ump_binary_read_state* (*cmidi2_ump_stream_selector_func)( + uint8_t targetStreamId, void* context); + +/// Binary stream continuity checker for cmidi2_ump_get_sysex8_data() function. +/// If you do not support simultaneous streams, then it should reset stream state (dataSize etc.) when new stream starts. +/// Return true to continue parsing, otherwise return false. It is useful to determine whether it +/// should break or not when a stream is fully read (status code is 3 = CMIDI2_SYSEX_END). +/// `cmidi2_ump_get_sysex8_data()` can take NULL for this function pointer at `continuityChecker` argument, +/// then it falls back to the "default" behavior. +/// By default, it finishes parsing when a stream completed and `continueOnCompletion` was `false`. +typedef bool (*cmidi2_ump_binary_read_continuity_checker_func)( + cmidi2_ump_binary_read_state* stream, cmidi2_ump* ump); + +// it is a special copy function that only works with sysex8 memory state at `src` that can access beyond `sizeInBytes`. +static inline void cmidi2_internal_sysex8_copy_data_byte_swapping( + uint8_t* dst, uint8_t srcHead, uint32_t* srcTail, size_t sizeInBytes) +{ + if (sizeInBytes == 0) + return; + + // the first byte is at the lowest byte so it is always safe to copy as is. + dst[0] = srcHead; + uint8_t* d = dst + 1; + uint32_t* s = srcTail; + sizeInBytes--; + + // copy the rest + size_t i = 0; + while (i + 3 < sizeInBytes) + { + uint32_t i32 = *s; + d[i++] = i32 >> 24; + d[i++] = (i32 >> 16) & 0xFF; + d[i++] = (i32 >> 8) & 0xFF; + d[i++] = i32 & 0xFF; + s++; + } + for (; i < sizeInBytes; i++) + d[i] = s[i + 3 - i % 4]; +} + +/// Parse and store sysex8 binary, using some fine-tuned behavioral functions. +/// Return the number of 32-bit ints (number of `cmidi2_ump`s) +static inline size_t cmidi2_ump_get_sysex8_data( + cmidi2_ump_stream_selector_func streamSelector, void* streamSelectorContext, + cmidi2_ump_binary_read_continuity_checker_func continuityChecker, const cmidi2_ump* ump, + const size_t umpCapacityInInt) +{ + + cmidi2_ump *umpPtr = (cmidi2_ump*)ump, *umpEnd = (cmidi2_ump*)ump + umpCapacityInInt; + for (; umpPtr < umpEnd; umpPtr += cmidi2_ump_get_message_size_bytes(umpPtr) / sizeof(cmidi2_ump)) + { + if (cmidi2_ump_get_message_type(umpPtr) != CMIDI2_MESSAGE_TYPE_SYSEX8_MDS) + continue; + switch (cmidi2_ump_get_status_code(umpPtr)) + { + case CMIDI2_SYSEX_IN_ONE_UMP: + case CMIDI2_SYSEX_START: + case CMIDI2_SYSEX_END: + case CMIDI2_SYSEX_CONTINUE: + break; + default: + continue; // the only expected value here is MDS (8 or 9) + } + + cmidi2_ump_binary_read_state* state + = streamSelector(cmidi2_ump_get_sysex8_stream_id(umpPtr), streamSelectorContext); + if (state == NULL) + continue; + + size_t copySize = cmidi2_ump_get_sysex8_num_bytes(umpPtr) + - 1; // SysEx8 size field contains the size byte itself, hence -1. + if (state->dataSize + copySize >= state->dataCapacity) + { + state->resultCode = CMIDI2_BINARY_READER_RESULT_NO_SPACE; + return umpPtr - ump; + } + + if (cmidi2_util_is_platform_little_endian()) + // We need to swap data along with byte order for resulting data... + cmidi2_internal_sysex8_copy_data_byte_swapping( + state->data + state->dataSize, (*umpPtr) & 0xFF, umpPtr + 1, copySize); + else + memcpy(state->data + state->dataSize, ((uint8_t*)(void*)umpPtr) + 3, copySize); + + state->dataSize += copySize; + + if (continuityChecker != NULL) + if (continuityChecker(state, umpPtr)) + return umpPtr + cmidi2_ump_get_message_size_bytes(umpPtr) / sizeof(cmidi2_ump) + - ump; // "break here" is indicated + + // otherwise default continuity checker + switch (cmidi2_ump_get_status_code(umpPtr)) + { + case CMIDI2_SYSEX_IN_ONE_UMP: + case CMIDI2_SYSEX_END: + state->resultCode = CMIDI2_BINARY_READER_RESULT_COMPLETE; + if (state->continueOnCompletion) + return umpPtr + cmidi2_ump_get_message_size_bytes(umpPtr) / sizeof(cmidi2_ump) + - ump; // default "break here" condition. + break; + } + } + // finished parsing while no stream indicated "break here" for completion. + return umpPtr - ump; +} + +// -------- +// sequence iterator + +/* byte stream splitter */ + +static inline void* cmidi2_ump_sequence_next(const void* ptr) +{ + return (uint8_t*)ptr + cmidi2_ump_get_num_bytes(cmidi2_ump_read_uint32_bytes(ptr)); +} + +// similar to LV2_ATOM Utilities API... +#define CMIDI2_UMP_SEQUENCE_FOREACH(ptr, numBytes, iter) \ + for (uint8_t * (iter) = (uint8_t*)ptr; (iter) < ((uint8_t*)ptr) + numBytes; \ + (iter) = (uint8_t*)cmidi2_ump_sequence_next(iter)) + +static inline void* cmidi2_ump_sequence_next_le(const void* ptr) +{ + return (uint8_t*)ptr + cmidi2_ump_get_num_bytes(cmidi2_ump_read_uint32_bytes_le(ptr)); +} + +#define CMIDI2_UMP_SEQUENCE_FOREACH_LE(ptr, numBytes, iter) \ + for (uint8_t * (iter) = (uint8_t*)ptr; (iter) < ((uint8_t*)ptr) + numBytes; \ + (iter) = (uint8_t*)cmidi2_ump_sequence_next_le(iter)) + +static inline void* cmidi2_ump_sequence_next_be(const void* ptr) +{ + return (uint8_t*)ptr + cmidi2_ump_get_num_bytes(cmidi2_ump_read_uint32_bytes_be(ptr)); +} + +#define CMIDI2_UMP_SEQUENCE_FOREACH_BE(ptr, numBytes, iter) \ + for (uint8_t * (iter) = (uint8_t*)ptr; (iter) < ((uint8_t*)ptr) + numBytes; \ + (iter) = (uint8_t*)cmidi2_ump_sequence_next_be(iter)) + +// -------- +// MIDI CI support. + +#define CMIDI2_CI_SUB_ID 0xD +#define CMIDI2_CI_SUB_ID_2_DISCOVERY_INQUIRY 0x70 +#define CMIDI2_CI_SUB_ID_2_DISCOVERY_REPLY 0x71 +#define CMIDI2_CI_SUB_ID_2_INVALIDATE_MUID 0x7E +#define CMIDI2_CI_SUB_ID_2_ACK 0x7F +#define CMIDI2_CI_SUB_ID_2_NAK 0x7F +#define CMIDI2_CI_SUB_ID_2_PROTOCOL_NEGOTIATION_INQUIRY 0x10 +#define CMIDI2_CI_SUB_ID_2_PROTOCOL_NEGOTIATION_REPLY 0x11 +#define CMIDI2_CI_SUB_ID_2_SET_NEW_PROTOCOL 0x12 +#define CMIDI2_CI_SUB_ID_2_TEST_NEW_PROTOCOL_I2R 0x13 +#define CMIDI2_CI_SUB_ID_2_TEST_NEW_PROTOCOL_R2I 0x14 +#define CMIDI2_CI_SUB_ID_2_CONFIRM_NEW_PROTOCOL_ESTABLISHED 0x15 +#define CMIDI2_CI_SUB_ID_2_PROFILE_INQUIRY 0x20 +#define CMIDI2_CI_SUB_ID_2_PROFILE_INQUIRY_REPLY 0x21 +#define CMIDI2_CI_SUB_ID_2_SET_PROFILE_ON 0x22 +#define CMIDI2_CI_SUB_ID_2_SET_PROFILE_OFF 0x23 +#define CMIDI2_CI_SUB_ID_2_PROFILE_ENABLED_REPORT 0x24 +#define CMIDI2_CI_SUB_ID_2_PROFILE_DISABLED_REPORT 0x25 +#define CMIDI2_CI_SUB_ID_2_PROFILE_SPECIFIC_DATA 0x2F +#define CMIDI2_CI_SUB_ID_2_PROPERTY_CAPABILITIES_INQUIRY 0x30 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_CAPABILITIES_REPLY 0x31 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_HAS_DATA 0x32 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_HAS_DATA_REPLY 0x33 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_GET_DATA 0x34 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_GET_DATA_REPLY 0x35 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_SET_DATA 0x36 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_SET_DATA_REPLY 0x37 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_SUBSCRIBE 0x38 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_SUBSCRIBE_REPLY 0x39 +#define CMIDI2_CI_SUB_ID_2_PROPERTY_NOTIFY 0x3F +#define CMIDI2_CI_SUB_ID_2_PROCESS_GET_CAPABILITIES 0x40 +#define CMIDI2_CI_SUB_ID_2_PROCESS_GET_CAPABILITIES_REPLY 0x41 +#define CMIDI2_CI_SUB_ID_2_PROCESS_GET_MIDI_REPORT 0x42 +#define CMIDI2_CI_SUB_ID_2_PROCESS_GET_MIDI_REPORT_REPLY 0x43 +#define CMIDI2_CI_SUB_ID_2_PROCESS_GET_MIDI_REPORT_END 0x44 + +#define CMIDI2_CI_PROTOCOL_NEGOTIATION_SUPPORTED 2 +#define CMIDI2_CI_PROFILE_CONFIGURATION_SUPPORTED 4 +#define CMIDI2_CI_PROPERTY_EXCHANGE_SUPPORTED 8 + +#define CMIDI2_CI_DEVICE_ID_WHOLE_FUNCTION_BLOCK 0x7F + +typedef struct +{ + uint8_t type; + uint8_t version; + uint8_t extensions; + uint8_t reserved1; + uint8_t reserved2; +} cmidi2_ci_protocol_type_info; + +typedef struct +{ + uint8_t fixed_7e; // 0x7E + uint8_t bank; + uint8_t number; + uint8_t version; + uint8_t level; +} cmidi2_profile_id; + +// Assumes the input value is already 7-bit encoded if required. +static inline void cmidi2_ci_direct_uint16_at(uint8_t* buf, uint16_t v) +{ + buf[0] = v & 0xFF; + buf[1] = (v >> 8) & 0xFF; +} + +// Assumes the input value is already 7-bit encoded if required. +static inline void cmidi2_ci_direct_uint32_at(uint8_t* buf, uint32_t v) +{ + buf[0] = v & 0xFF; + buf[1] = (v >> 8) & 0xFF; + buf[2] = (v >> 16) & 0xFF; + buf[3] = (v >> 24) & 0xFF; +} + +static inline void cmidi2_ci_7bit_int14_at(uint8_t* buf, uint16_t v) +{ + buf[0] = v & 0x7F; + buf[1] = (v >> 7) & 0x7F; +} + +static inline void cmidi2_ci_7bit_int21_at(uint8_t* buf, uint32_t v) +{ + buf[0] = v & 0x7F; + buf[1] = (v >> 7) & 0x7F; + buf[2] = (v >> 14) & 0x7F; +} + +static inline void cmidi2_ci_7bit_int28_at(uint8_t* buf, uint32_t v) +{ + buf[0] = v & 0x7F; + buf[1] = (v >> 7) & 0x7F; + buf[2] = (v >> 14) & 0x7F; + buf[3] = (v >> 21) & 0x7F; +} + +static inline void cmidi2_ci_message_common( + uint8_t* buf, uint8_t destination, uint8_t sysexSubId2, uint8_t versionAndFormat, + uint32_t sourceMUID, uint32_t destinationMUID) +{ + buf[0] = 0x7E; + buf[1] = destination; + buf[2] = CMIDI2_CI_SUB_ID; + buf[3] = sysexSubId2; + buf[4] = versionAndFormat; + cmidi2_ci_direct_uint32_at(buf + 5, sourceMUID); + cmidi2_ci_direct_uint32_at(buf + 9, destinationMUID); +} + +// Discovery + +static inline void cmidi2_ci_discovery_common( + uint8_t* buf, uint8_t sysexSubId2, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint32_t deviceManufacturer3Bytes, uint16_t deviceFamily, + uint16_t deviceFamilyModelNumber, uint32_t softwareRevisionLevel, uint8_t ciCategorySupported, + uint32_t receivableMaxSysExSize, uint8_t initiatorOutputPathId) +{ + cmidi2_ci_message_common( + buf, CMIDI2_CI_DEVICE_ID_WHOLE_FUNCTION_BLOCK, sysexSubId2, versionAndFormat, sourceMUID, + destinationMUID); + cmidi2_ci_direct_uint32_at( + buf + 13, + deviceManufacturer3Bytes); // the last byte is extraneous, but will be overwritten next. + cmidi2_ci_direct_uint16_at(buf + 16, deviceFamily); + cmidi2_ci_direct_uint16_at(buf + 18, deviceFamilyModelNumber); + // LAMESPEC: Software Revision Level does not mention in which endianness this field is stored. + cmidi2_ci_direct_uint32_at(buf + 20, softwareRevisionLevel); + buf[24] = ciCategorySupported; + cmidi2_ci_direct_uint32_at(buf + 25, receivableMaxSysExSize); + buf[29] = initiatorOutputPathId; +} + +static inline void cmidi2_ci_discovery( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t deviceManufacturer, + uint16_t deviceFamily, uint16_t deviceFamilyModelNumber, uint32_t softwareRevisionLevel, + uint8_t ciCategorySupported, uint32_t receivableMaxSysExSize, uint8_t initiatorOutputPathId) +{ + cmidi2_ci_discovery_common( + buf, CMIDI2_CI_SUB_ID_2_DISCOVERY_INQUIRY, versionAndFormat, sourceMUID, 0x7F7F7F7F, + deviceManufacturer, deviceFamily, deviceFamilyModelNumber, softwareRevisionLevel, + ciCategorySupported, receivableMaxSysExSize, initiatorOutputPathId); +} + +static inline void cmidi2_ci_discovery_reply( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint32_t deviceManufacturer, uint16_t deviceFamily, uint16_t deviceFamilyModelNumber, + uint32_t softwareRevisionLevel, uint8_t ciCategorySupported, uint32_t receivableMaxSysExSize, + uint8_t initiatorOutputPathId, uint8_t functionBlockOr7Fh) +{ + cmidi2_ci_discovery_common( + buf, CMIDI2_CI_SUB_ID_2_DISCOVERY_REPLY, versionAndFormat, sourceMUID, destinationMUID, + deviceManufacturer, deviceFamily, deviceFamilyModelNumber, softwareRevisionLevel, + ciCategorySupported, receivableMaxSysExSize, initiatorOutputPathId); + buf[30] = functionBlockOr7Fh; +} + +static inline void cmidi2_ci_discovery_invalidate_muid( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t targetMUID) +{ + cmidi2_ci_message_common( + buf, 0x7F, CMIDI2_CI_SUB_ID_2_INVALIDATE_MUID, versionAndFormat, sourceMUID, 0x7F7F7F7F); + cmidi2_ci_direct_uint32_at(buf + 13, targetMUID); +} + +static inline void cmidi2_ci_ack_nak_common( + uint8_t* buf, uint8_t deviceId, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t originalTransactionSubID2Class, uint8_t statusCode, + uint8_t statusData, uint8_t* details5Bytes, uint16_t messageLength, const char* messageText) +{ + cmidi2_ci_message_common( + buf, deviceId, CMIDI2_CI_SUB_ID_2_ACK, versionAndFormat, sourceMUID, destinationMUID); + buf[13] = originalTransactionSubID2Class; + buf[14] = statusCode; + buf[15] = statusData; + memcpy(buf + 16, details5Bytes, 5); + cmidi2_ci_direct_uint16_at(buf + 21, messageLength); + memcpy(buf + 23, messageText, messageLength); +} + +static inline void cmidi2_ci_ack( + uint8_t* buf, uint8_t deviceId, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t originalTransactionSubID2Class, uint8_t ackStatusCode, + uint8_t ackStatusData, uint8_t* ackDetails5Bytes, uint16_t messageLength, + const char* messageText) +{ + cmidi2_ci_ack_nak_common( + buf, deviceId, versionAndFormat, sourceMUID, destinationMUID, originalTransactionSubID2Class, + ackStatusCode, ackStatusData, ackDetails5Bytes, messageLength, messageText); +} + +static inline void cmidi2_ci_nak( + uint8_t* buf, uint8_t deviceId, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t originalTransactionSubID2Class, uint8_t nakStatusCode, + uint8_t nakStatusData, uint8_t* nakDetails5Bytes, uint16_t messageLength, + const char* messageText) +{ + cmidi2_ci_ack_nak_common( + buf, deviceId, versionAndFormat, sourceMUID, destinationMUID, originalTransactionSubID2Class, + nakStatusCode, nakStatusData, nakDetails5Bytes, messageLength, messageText); +} + +// Protocol Negotiation +// Note that it was removed in MIDI 2.0 specification June 2023 Updates (MIDI-CI 1.2). + +static inline void cmidi2_ci_protocol_info(uint8_t* buf, cmidi2_ci_protocol_type_info info) +{ + buf[0] = info.type; + buf[1] = info.version; + buf[2] = info.extensions; + buf[3] = info.reserved1; + buf[4] = info.reserved2; +} + +static inline void cmidi2_ci_protocols( + uint8_t* buf, uint8_t numSupportedProtocols, cmidi2_ci_protocol_type_info* protocolTypes) +{ + buf[0] = numSupportedProtocols; + for (int i = 0; i < numSupportedProtocols; i++) + cmidi2_ci_protocol_info(buf + 1 + i * 5, protocolTypes[i]); +} + +static inline void cmidi2_ci_protocol_negotiation( + uint8_t* buf, bool isReply, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t authorityLevel, uint8_t numSupportedProtocols, + cmidi2_ci_protocol_type_info* protocolTypes) +{ + cmidi2_ci_message_common( + buf, 0x7F, + isReply ? CMIDI2_CI_SUB_ID_2_PROTOCOL_NEGOTIATION_REPLY + : CMIDI2_CI_SUB_ID_2_PROTOCOL_NEGOTIATION_INQUIRY, + 1, sourceMUID, destinationMUID); + buf[13] = authorityLevel; + cmidi2_ci_protocols(buf + 14, numSupportedProtocols, protocolTypes); +} + +static inline void cmidi2_ci_protocol_set( + uint8_t* buf, uint32_t sourceMUID, uint32_t destinationMUID, uint8_t authorityLevel, + cmidi2_ci_protocol_type_info newProtocolType) +{ + cmidi2_ci_message_common( + buf, 0x7F, CMIDI2_CI_SUB_ID_2_SET_NEW_PROTOCOL, 1, sourceMUID, destinationMUID); + buf[13] = authorityLevel; + cmidi2_ci_protocol_info(buf + 14, newProtocolType); +} + +static inline void cmidi2_ci_protocol_test( + uint8_t* buf, bool isInitiatorToResponder, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t authorityLevel, uint8_t* testData48Bytes) +{ + cmidi2_ci_message_common( + buf, 0x7F, + isInitiatorToResponder ? CMIDI2_CI_SUB_ID_2_TEST_NEW_PROTOCOL_I2R + : CMIDI2_CI_SUB_ID_2_TEST_NEW_PROTOCOL_R2I, + 1, sourceMUID, destinationMUID); + buf[13] = authorityLevel; + memcpy(buf + 14, testData48Bytes, 48); +} + +static inline void cmidi2_ci_protocol_confirm_established( + uint8_t* buf, uint32_t sourceMUID, uint32_t destinationMUID, uint8_t authorityLevel) +{ + cmidi2_ci_message_common( + buf, 0x7F, CMIDI2_CI_SUB_ID_2_CONFIRM_NEW_PROTOCOL_ESTABLISHED, 1, sourceMUID, + destinationMUID); + buf[13] = authorityLevel; +} + +static inline int32_t cmidi2_ci_try_parse_new_protocol(uint8_t* buf, size_t length) +{ + return (length != 19 || buf[0] != 0x7E || buf[1] != 0x7F || buf[2] != CMIDI2_CI_SUB_ID + || buf[3] != CMIDI2_CI_SUB_ID_2_SET_NEW_PROTOCOL || buf[4] != 1) + ? 0 + : buf[14]; +} + +// Profile Configuration + +static inline void cmidi2_ci_profile(uint8_t* buf, cmidi2_profile_id info) +{ + buf[0] = info.fixed_7e; + buf[1] = info.bank; + buf[2] = info.number; + buf[3] = info.version; + buf[4] = info.level; +} + +static inline void cmidi2_ci_profile_inquiry( + uint8_t* buf, uint8_t source, uint32_t sourceMUID, uint32_t destinationMUID) +{ + cmidi2_ci_message_common( + buf, source, CMIDI2_CI_SUB_ID_2_PROFILE_INQUIRY, 1, sourceMUID, destinationMUID); +} + +static inline void cmidi2_ci_profile_inquiry_reply( + uint8_t* buf, uint8_t source, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t numEnabledProfiles, cmidi2_profile_id* enabledProfiles, uint8_t numDisabledProfiles, + cmidi2_profile_id* disabledProfiles) +{ + cmidi2_ci_message_common( + buf, source, CMIDI2_CI_SUB_ID_2_PROFILE_INQUIRY_REPLY, 1, sourceMUID, destinationMUID); + buf[13] = numEnabledProfiles; + for (int i = 0; i < numEnabledProfiles; i++) + cmidi2_ci_profile(buf + 14 + i * 5, enabledProfiles[i]); + uint32_t pos = 14 + numEnabledProfiles * 5; + buf[pos++] = numDisabledProfiles; + for (int i = 0; i < numDisabledProfiles; i++) + cmidi2_ci_profile(buf + pos + i * 5, disabledProfiles[i]); +} + +static inline void cmidi2_ci_profile_set( + uint8_t* buf, uint8_t destination, bool turnOn, uint32_t sourceMUID, uint32_t destinationMUID, + cmidi2_profile_id profile) +{ + cmidi2_ci_message_common( + buf, destination, + turnOn ? CMIDI2_CI_SUB_ID_2_SET_PROFILE_ON : CMIDI2_CI_SUB_ID_2_SET_PROFILE_OFF, 1, + sourceMUID, destinationMUID); + cmidi2_ci_profile(buf + 13, profile); +} + +static inline void cmidi2_ci_profile_report( + uint8_t* buf, uint8_t source, bool isEnabledReport, uint32_t sourceMUID, + cmidi2_profile_id profile) +{ + cmidi2_ci_message_common( + buf, source, + isEnabledReport ? CMIDI2_CI_SUB_ID_2_PROFILE_ENABLED_REPORT + : CMIDI2_CI_SUB_ID_2_PROFILE_DISABLED_REPORT, + 1, sourceMUID, 0x7F7F7F7F); + cmidi2_ci_profile(buf + 13, profile); +} + +static inline void cmidi2_ci_profile_specific_data( + uint8_t* buf, uint8_t source, uint32_t sourceMUID, uint32_t destinationMUID, + cmidi2_profile_id profile, uint32_t dataSize, void* data) +{ + cmidi2_ci_message_common( + buf, source, CMIDI2_CI_SUB_ID_2_PROFILE_SPECIFIC_DATA, 1, sourceMUID, destinationMUID); + cmidi2_ci_profile(buf + 13, profile); + cmidi2_ci_direct_uint32_at(buf + 18, dataSize); + memcpy(buf + 22, data, dataSize); +} + +// Property Exchange + +static inline void cmidi2_ci_property_get_capabilities( + uint8_t* buf, uint8_t destination, bool isReply, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t maxSupportedRequests) +{ + cmidi2_ci_message_common( + buf, destination, + isReply ? CMIDI2_CI_SUB_ID_2_PROPERTY_CAPABILITIES_REPLY + : CMIDI2_CI_SUB_ID_2_PROPERTY_CAPABILITIES_INQUIRY, + 1, sourceMUID, destinationMUID); + buf[13] = maxSupportedRequests; +} + +// common to all of: has data & reply, get data & reply, set data & reply, subscribe & reply, notify +static inline void cmidi2_ci_property_common( + uint8_t* buf, uint8_t destination, uint8_t messageTypeSubId2, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t requestId, uint16_t headerSize, void* header, + uint16_t numChunks, uint16_t chunkIndex, uint16_t dataSize, void* data) +{ + cmidi2_ci_message_common(buf, destination, messageTypeSubId2, 1, sourceMUID, destinationMUID); + buf[13] = requestId; + cmidi2_ci_direct_uint16_at(buf + 14, headerSize); + memcpy(buf + 16, header, headerSize); + cmidi2_ci_direct_uint16_at(buf + 16 + headerSize, numChunks); + cmidi2_ci_direct_uint16_at(buf + 18 + headerSize, chunkIndex); + cmidi2_ci_direct_uint16_at(buf + 20 + headerSize, dataSize); + memcpy(buf + 22 + headerSize, data, dataSize); +} + +static inline void cmidi2_ci_property_get_capabilities_reply( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t maxSupportedRequests, uint8_t peMajorVersion, uint8_t peMinorVersion) +{ + cmidi2_ci_message_common( + buf, CMIDI2_CI_DEVICE_ID_WHOLE_FUNCTION_BLOCK, + CMIDI2_CI_SUB_ID_2_PROPERTY_CAPABILITIES_REPLY, versionAndFormat, sourceMUID, + destinationMUID); + buf[13] = maxSupportedRequests; + buf[14] = peMajorVersion; + buf[15] = peMinorVersion; +} + +static inline void cmidi2_ci_property_data_common( + uint8_t* buf, uint8_t subId2, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t requestId, size_t headerSize, const uint8_t* headerData, + uint16_t numChunksInMessage, uint16_t currentChunk, uint16_t propertyDataLength, + const char* propertyData) +{ + cmidi2_ci_message_common( + buf, CMIDI2_CI_DEVICE_ID_WHOLE_FUNCTION_BLOCK, subId2, versionAndFormat, sourceMUID, + destinationMUID); + buf[13] = requestId; + buf[14] = headerSize % 0xFF; + buf[15] = headerSize / 0xFF; + memcpy(buf + 16, headerData, headerSize); + cmidi2_ci_direct_uint16_at(buf + headerSize + 16, numChunksInMessage); + cmidi2_ci_direct_uint16_at(buf + headerSize + 18, currentChunk); + cmidi2_ci_direct_uint16_at(buf + headerSize + 20, propertyDataLength); + if (propertyData) + memcpy(buf + headerSize + 22, propertyData, propertyDataLength); +} + +static inline void cmidi2_ci_property_get_data( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t requestId, size_t headerSize, const uint8_t* headerData) +{ + cmidi2_ci_property_data_common( + buf, CMIDI2_CI_SUB_ID_2_PROPERTY_GET_DATA, versionAndFormat, sourceMUID, destinationMUID, + requestId, headerSize, headerData, 1, 1, 0, NULL); +} + +static inline void cmidi2_ci_property_get_data_reply( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t requestId, size_t headerSize, const uint8_t* headerData, uint16_t numChunksInMessage, + uint16_t currentChunk, uint16_t propertyDataLength, const char* propertyData) +{ + cmidi2_ci_property_data_common( + buf, CMIDI2_CI_SUB_ID_2_PROPERTY_GET_DATA_REPLY, versionAndFormat, sourceMUID, + destinationMUID, requestId, headerSize, headerData, numChunksInMessage, currentChunk, + propertyDataLength, propertyData); +} + +static inline void cmidi2_ci_property_set_data( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t requestId, size_t headerSize, const uint8_t* headerData, uint16_t numChunksInMessage, + uint16_t currentChunk, uint16_t propertyDataLength, const char* propertyData) +{ + cmidi2_ci_property_data_common( + buf, CMIDI2_CI_SUB_ID_2_PROPERTY_SET_DATA, versionAndFormat, sourceMUID, destinationMUID, + requestId, headerSize, headerData, numChunksInMessage, currentChunk, propertyDataLength, + propertyData); +} + +static inline void cmidi2_ci_property_set_data_reply( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t requestId, size_t headerSize, const uint8_t* headerData) +{ + cmidi2_ci_property_data_common( + buf, CMIDI2_CI_SUB_ID_2_PROPERTY_SET_DATA_REPLY, versionAndFormat, sourceMUID, + destinationMUID, requestId, headerSize, headerData, 1, 1, 0, NULL); +} + +static inline void cmidi2_ci_property_subscribe( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t requestId, size_t headerSize, const uint8_t* headerData, uint16_t numChunksInMessage, + uint16_t currentChunk, uint16_t propertyDataLength, const char* propertyData) +{ + cmidi2_ci_property_data_common( + buf, CMIDI2_CI_SUB_ID_2_PROPERTY_SUBSCRIBE, versionAndFormat, sourceMUID, destinationMUID, + requestId, headerSize, headerData, numChunksInMessage, currentChunk, propertyDataLength, + propertyData); +} + +static inline void cmidi2_ci_property_subscribe_reply( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t requestId, size_t headerSize, const uint8_t* headerData, uint16_t numChunksInMessage, + uint16_t currentChunk, uint16_t propertyDataLength, const char* propertyData) +{ + cmidi2_ci_property_data_common( + buf, CMIDI2_CI_SUB_ID_2_PROPERTY_SUBSCRIBE_REPLY, versionAndFormat, sourceMUID, + destinationMUID, requestId, headerSize, headerData, numChunksInMessage, currentChunk, + propertyDataLength, propertyData); +} + +static inline void cmidi2_ci_property_notify( + uint8_t* buf, uint8_t versionAndFormat, uint32_t sourceMUID, uint32_t destinationMUID, + uint8_t requestId, size_t headerSize, const uint8_t* headerData, uint16_t numChunksInMessage, + uint16_t currentChunk, uint16_t propertyDataLength, const char* propertyData) +{ + cmidi2_ci_property_data_common( + buf, CMIDI2_CI_SUB_ID_2_PROPERTY_NOTIFY, versionAndFormat, sourceMUID, destinationMUID, + requestId, headerSize, headerData, numChunksInMessage, currentChunk, propertyDataLength, + propertyData); +} + +// Process Inquiry + +static inline void cmidi2_ci_process_get_capabilities( + uint8_t* buf, uint8_t subId2, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID) +{ + (void)subId2; + cmidi2_ci_message_common( + buf, CMIDI2_CI_DEVICE_ID_WHOLE_FUNCTION_BLOCK, CMIDI2_CI_SUB_ID_2_PROCESS_GET_CAPABILITIES, + versionAndFormat, sourceMUID, destinationMUID); +} + +static inline void cmidi2_ci_process_get_capabilities_reply( + uint8_t* buf, uint8_t subId2, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t processInquirySupportedFeatures) +{ + (void)subId2; + cmidi2_ci_message_common( + buf, CMIDI2_CI_DEVICE_ID_WHOLE_FUNCTION_BLOCK, + CMIDI2_CI_SUB_ID_2_PROCESS_GET_CAPABILITIES_REPLY, versionAndFormat, sourceMUID, + destinationMUID); + buf[13] = processInquirySupportedFeatures; +} + +static inline void cmidi2_ci_process_midi_report_common( + uint8_t* buf, uint8_t deviceId, uint8_t subId2, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t messageDataControl, uint8_t requestedSystemMessages, + uint8_t requestedChannelControllerMessages, uint8_t requestedNoteDataMessages) +{ + (void)subId2; + cmidi2_ci_message_common( + buf, deviceId, CMIDI2_CI_SUB_ID_2_PROCESS_GET_CAPABILITIES, versionAndFormat, sourceMUID, + destinationMUID); + buf[13] = messageDataControl; + buf[14] = requestedSystemMessages; + buf[15] = 0; // reserved + buf[16] = requestedChannelControllerMessages; + buf[17] = requestedNoteDataMessages; +} + +static inline void cmidi2_ci_process_get_midi_report( + uint8_t* buf, uint8_t deviceId, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t messageDataControl, uint8_t requestedSystemMessages, + uint8_t requestedChannelControllerMessages, uint8_t requestedNoteDataMessages) +{ + cmidi2_ci_process_midi_report_common( + buf, deviceId, CMIDI2_CI_SUB_ID_2_PROCESS_GET_MIDI_REPORT, versionAndFormat, sourceMUID, + destinationMUID, messageDataControl, requestedSystemMessages, + requestedChannelControllerMessages, requestedNoteDataMessages); +} + +static inline void cmidi2_ci_process_get_midi_report_reply( + uint8_t* buf, uint8_t deviceId, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID, uint8_t messageDataControl, uint8_t requestedSystemMessages, + uint8_t requestedChannelControllerMessages, uint8_t requestedNoteDataMessages) +{ + cmidi2_ci_process_midi_report_common( + buf, deviceId, CMIDI2_CI_SUB_ID_2_PROCESS_GET_MIDI_REPORT_REPLY, versionAndFormat, + sourceMUID, destinationMUID, messageDataControl, requestedSystemMessages, + requestedChannelControllerMessages, requestedNoteDataMessages); +} + +static inline void cmidi2_ci_process_get_midi_report_end( + uint8_t* buf, uint8_t deviceId, uint8_t versionAndFormat, uint32_t sourceMUID, + uint32_t destinationMUID) +{ + cmidi2_ci_message_common( + buf, deviceId, CMIDI2_CI_SUB_ID_2_PROCESS_GET_MIDI_REPORT_END, versionAndFormat, sourceMUID, + destinationMUID); +} + +// Miscellaneous MIDI Utilities + +/** Encodes `value` into `bytes` stream. Returns the length of the encoded value in bytes. */ +static inline uint8_t cmidi2_midi1_write_7bit_encoded_int(uint8_t* bytes, uint32_t value) +{ + uint8_t pos = 0; + for (;; pos++) + { + bytes[pos] = value % 0x80; + if (value >= 0x80) + { + value /= 0x80; + bytes[pos] |= 0x80; + } + else + return pos + 1; + } +} + +/** Returns the length of `value` when it would be encoded into a byte stream. */ +static inline uint8_t cmidi2_midi1_get_7bit_encoded_int_length(uint32_t value) +{ + for (uint8_t ret = 1;; ret++) + { + if (value >= 0x80) + value /= 0x80; + else + return ret; + } + return 0; +} + +/** Returns the 7-bit encoded value in `bytes` stream of `length` bytes. */ +static inline uint32_t cmidi2_midi1_get_7bit_encoded_int(uint8_t* bytes, uint32_t length) +{ + uint8_t* start = bytes; + uint32_t value = 0; + for (int digits = 0;; digits++) + { + if (bytes >= start + length) + break; + value += (0x7F & (*bytes)) << (digits * 7); + if (*bytes < 0x80) + break; + bytes++; + } + bytes++; + return value; +} + +static inline uint32_t cmidi2_midi1_get_message_size(uint8_t* bytes, uint32_t length) +{ + uint32_t metaLength; + uint8_t* start = bytes; + uint8_t* end = bytes + length; + switch (bytes[0]) + { + case 0xF0: + for (bytes++; bytes < end; bytes++) + if (*bytes == 0xF7) + break; + bytes++; + break; + case 0xFF: + bytes++; + metaLength = cmidi2_midi1_get_7bit_encoded_int(bytes, end - bytes); + bytes += metaLength + cmidi2_midi1_get_7bit_encoded_int_length(metaLength); + break; + default: + switch (bytes[0] & 0xF0) + { + case 0xC0: + case 0xD0: + bytes += 2; + break; + default: + bytes += 3; + break; + } + break; + } + return bytes - start; +} + +// MIDI1 to UMP Translator + +/* Should we define some structs like this to support conversion from MIDI1 UMP to MIDI2 UMP and complicate the API? + I'm not sure if there are enough need, and it's better to avoid another cmidi2_convert_midi1_to_ump() (200-ish LoC). + +typedef struct cmidi2_midi1_sequence_midi1_bytes { + // input MIDI1 messages or SMF events. + uint8_t *midi1; + // size of the input stream to process in bytes + size_t midi1_num_bytes; + // it is updated as per cmidi2_convert_midi1_messages_to_ump() proceeds the MIDI1 stream. + size_t midi1_proceeded_bytes; +} cmidi2_midi1_sequence_midi1_bytes; + +typedef struct cmidi2_midi1_sequence_umps { + // MIDI1 UMP stream. + cmidi2_ump* ump; + // size (capacity) of the output stream in bytes + size_t ump_num_bytes; + // it is updated as per cmidi2_convert_midi1_messages_to_ump() proceeds the UMP stream. + size_t ump_proceeded_bytes; +} cmidi2_midi1_sequence_ump; +*/ + +enum cmidi2_translator_endianness +{ + CMIDI2_TRANSLATOR_DEFAULT_ENDIAN, + CMIDI2_TRANSLATOR_BIG_ENDIAN, + CMIDI2_TRANSLATOR_LITTLE_ENDIAN +}; + +// Conversion from MIDI1 message bytes or SMF event list to MIDI2 UMP stream +// +// The conversion requires some preserved context e.g. RPN/NRPN/DTE +// MSB/LSB, so we take this struct. +typedef struct cmidi2_midi_conversion_context +{ + // When it is true, it means the input MIDI1 stream contains delta time. + // TODO: implement support for it + bool is_midi1_smf; + // Context tempo as in SMF specification defines. + // If the MIDI1 stream is SMF and non-SMPTE delta time is used, then we have to calculate + // the actual delta time length in SMPTE (then to JR timestamp). + // TODO: implement support for it + int32_t tempo; + // input MIDI1 messages or SMF events. + uint8_t* midi1; + // size of the input stream to process in bytes + size_t midi1_num_bytes; + // it is updated as per cmidi2_convert_midi1_messages_to_ump() proceeds the MIDI1 stream. + size_t midi1_proceeded_bytes; + // output UMP stream. + cmidi2_ump* ump; + // size (capacity) of the output stream in bytes + size_t ump_num_bytes; + // it is updated as per cmidi2_convert_midi1_messages_to_ump() proceeds the UMP stream. + size_t ump_proceeded_bytes; + // DTE conversion target. + // cmidi2_convert_midi1_messages_to_ump() will return *_INVALID_RPN or *_INVALID_NRPN + // for such invalid sequences, and to report that correctly we need to preserve CC status. + // They are initialized to 0x8080 that implies both bank (MSB) and index (LSB) are invalid (> 0x7F). + // When they are assigned valid values, then (context_[n]rpn & 0x8080) will become 0. + // When cmidi2_convert_midi1_messages_to_ump() encountered DTE LSB, they are consumed + // and reset to the initial value (0x8080). + int32_t context_rpn; + int32_t context_nrpn; + int32_t context_dte; + // MIDI 2.0 Defalult Translation (UMP specification Appendix D.3) accepts only DTE LSB + // as the conversion terminator, but cmidi2 allows DTE LSB to come first, + // if this flag is enabled. + bool allow_reordered_dte; + // Bank Select CC is preserved for the next program change. + // The initial value is 0x8080, same as RPN/NRPN/DTE. + // After program change is set, it is reset to the initial value. + uint32_t context_bank; + // Group can be specified. + uint8_t group; + // Destination protocol: can be MIDI1 UMP or MIDI2 UMP. + enum cmidi2_ci_protocol_values midi_protocol; + // Sysex conversion can be done to sysex8 + bool use_sysex8; + // Determine whether delta time is skipped or not (inserted like SMF) in the output MIDI1 stream. + bool skip_delta_time; + // UMP serialization endianness + int32_t ump_serialization_endianness; +} cmidi2_midi_conversion_context; + +enum cmidi2_midi_conversion_result +{ + CMIDI2_CONVERSION_RESULT_OK = 0, + CMIDI2_CONVERSION_RESULT_OUT_OF_SPACE = 1, + CMIDI2_CONVERSION_RESULT_INVALID_SYSEX = 0x10, + CMIDI2_CONVERSION_RESULT_INVALID_DTE_SEQUENCE = 0x11, + CMIDI2_CONVERSION_RESULT_INVALID_STATUS = 0x13, + CMIDI2_CONVERSION_RESULT_INCOMPLETE_SYSEX7 = 0x20, +}; + +static inline void +cmidi2_midi_conversion_context_initialize(cmidi2_midi_conversion_context* context) +{ + context->is_midi1_smf = false; + context->tempo = 500000; + context->midi1 = NULL; + context->midi1_num_bytes = 0; + context->midi1_proceeded_bytes = 0; + context->ump = NULL; + context->ump_num_bytes = 0; + context->ump_proceeded_bytes = 0; + context->context_rpn = 0x8080; + context->context_nrpn = 0x8080; + context->context_dte = 0x8080; + context->context_bank = 0x8080; + context->allow_reordered_dte = false; + context->group = 0; + context->midi_protocol = CMIDI2_PROTOCOL_TYPE_MIDI2; + context->use_sysex8 = false; + context->skip_delta_time = false; + context->ump_serialization_endianness = CMIDI2_TRANSLATOR_DEFAULT_ENDIAN; +} + +typedef struct cmidi2_convert_sysex_context +{ + cmidi2_midi_conversion_context* conversion_context; + size_t dst_offset; +} cmidi2_convert_sysex_context; + +static inline void* +cmidi2_internal_convert_add_midi1_sysex7_ump_to_list(uint64_t data, void* context) +{ + cmidi2_convert_sysex_context* s7ctx = (cmidi2_convert_sysex_context*)context; + s7ctx->conversion_context->ump[s7ctx->dst_offset] = data >> 32; + s7ctx->conversion_context->ump[s7ctx->dst_offset + 1] = data & 0xFFFFFFFF; + s7ctx->conversion_context->ump_proceeded_bytes += 2; + return NULL; +} + +static inline void* cmidi2_internal_convert_add_midi1_sysex8_ump_to_list( + uint64_t data1, uint64_t data2, size_t index, void* context) +{ + (void)index; + cmidi2_convert_sysex_context* s8ctx = (cmidi2_convert_sysex_context*)context; + s8ctx->conversion_context->ump[s8ctx->dst_offset] = data1 >> 32; + s8ctx->conversion_context->ump[s8ctx->dst_offset + 1] = data1 & 0xFFFFFFFF; + s8ctx->conversion_context->ump[s8ctx->dst_offset + 2] = data2 >> 32; + s8ctx->conversion_context->ump[s8ctx->dst_offset + 3] = data2 & 0xFFFFFFFF; + s8ctx->conversion_context->ump_proceeded_bytes += 4; + return NULL; +} + +static inline uint64_t +cmidi2_internal_convert_midi1_dte_to_ump(cmidi2_midi_conversion_context* context, uint8_t channel) +{ + bool isRpn = (context->context_rpn & 0x8080) == 0; + uint8_t msb = (isRpn ? context->context_rpn : context->context_nrpn) >> 8; + uint8_t lsb = (isRpn ? context->context_rpn : context->context_nrpn) & 0xFF; + int32_t data = (context->context_dte >> 8 << 25) + ((context->context_dte & 0x7F) << 18); + // reset RPN/NRPN/DTE status to the initial values. + context->context_rpn = 0x8080; + context->context_nrpn = 0x8080; + context->context_dte = 0x8080; + return isRpn ? cmidi2_ump_midi2_rpn(context->group, channel, msb, lsb, data) + : cmidi2_ump_midi2_nrpn(context->group, channel, msb, lsb, data); +} + +static inline uint32_t cmidi2_internal_swap_endian(uint32_t v) +{ + return ((v & 0xFF) << 24) + (((v >> 8) & 0xFF) << 16) + (((v >> 16) & 0xFF) << 8) + + ((v >> 24) & 0xFF); +} + +/** converts MIDI1 bytestream which can contain deltaTime in SMF, to MIDI2 UMP stream. + * The resulting stream is native endianness. + */ +static enum cmidi2_midi_conversion_result +cmidi2_convert_midi1_to_ump(cmidi2_midi_conversion_context* context) +{ + uint8_t* dst = (uint8_t*)context->ump; + size_t sLen = context->midi1_num_bytes; + size_t dLen = context->ump_num_bytes; + uint8_t* sIdx = (uint8_t*)&context->midi1_proceeded_bytes; + uint8_t* dIdx = (uint8_t*)&context->ump_proceeded_bytes; + + while (*sIdx < sLen) + { + // FIXME: implement deltaTime to JR Timestamp conversion. + + if (context->midi1[*sIdx] == 0xF0) + { + // sysex + uint8_t* f7 + = (uint8_t*)memchr(context->midi1 + *sIdx, 0xF7, context->midi1_num_bytes - *sIdx); + if (f7 == NULL) + { + return CMIDI2_CONVERSION_RESULT_INVALID_SYSEX; // error + } + size_t sysexSize = f7 - context->midi1 - *sIdx - 1; // excluding 0xF7 + size_t numPackets = context->use_sysex8 ? cmidi2_ump_sysex8_get_num_packets(sysexSize) + : cmidi2_ump_sysex7_get_num_packets(sysexSize); + if (dLen - *dIdx < numPackets) + return CMIDI2_CONVERSION_RESULT_OUT_OF_SPACE; + cmidi2_convert_sysex_context sysExCtx; + sysExCtx.conversion_context = context; + sysExCtx.dst_offset = *dIdx; + if (context->use_sysex8) + { + // ignoring the return code as it never returns non-NULL... (size is already verified) + cmidi2_ump_sysex8_process( + context->group, context->midi1 + *sIdx, sysexSize, 0, + cmidi2_internal_convert_add_midi1_sysex8_ump_to_list, &sysExCtx); + } + else + { + // ignoring the return code as it never returns non-NULL... (size is already verified) + cmidi2_ump_sysex7_process( + context->group, context->midi1 + *sIdx, + cmidi2_internal_convert_add_midi1_sysex7_ump_to_list, &sysExCtx); + } + *sIdx += sysexSize + 1; // +1 for 0xF7 + } + else + { + // fixed sized message + size_t len = cmidi2_midi1_get_message_size(context->midi1 + *sIdx, sLen - *sIdx); + uint8_t byte2 = context->midi1[*sIdx + 1]; + uint8_t byte3 = len > 2 ? context->midi1[*sIdx + 2] : 0; + uint8_t channel = context->midi1[*sIdx] & 0xF; + if (context->midi_protocol == CMIDI2_PROTOCOL_TYPE_MIDI1) + { + // generate MIDI1 UMPs + dst[*dIdx] = cmidi2_ump_midi1_message( + context->group, context->midi1[*sIdx] & 0xF0, channel, byte2, byte3); + *sIdx += len; + *dIdx += 4; + } + else + { + // generate MIDI2 UMPs + uint64_t m2; + const int8_t NO_ATTRIBUTE_TYPE = 0; + const int16_t NO_ATTRIBUTE_DATA = 0; + bool bankValid, bankMsbValid, bankLsbValid; + bool skipEmitUmp = false; + switch (context->midi1[*sIdx] & 0xF0) + { + case CMIDI2_STATUS_NOTE_OFF: + m2 = cmidi2_ump_midi2_note_off( + context->group, channel, byte2, NO_ATTRIBUTE_TYPE, byte3 << 9, NO_ATTRIBUTE_DATA); + break; + case CMIDI2_STATUS_NOTE_ON: + m2 = cmidi2_ump_midi2_note_on( + context->group, channel, byte2, NO_ATTRIBUTE_TYPE, byte3 << 9, NO_ATTRIBUTE_DATA); + break; + case CMIDI2_STATUS_PAF: + m2 = cmidi2_ump_midi2_paf(context->group, channel, byte2, byte3 << 25); + break; + case CMIDI2_STATUS_CC: + switch (byte2) + { + case CMIDI2_CC_RPN_MSB: + context->context_rpn = (context->context_rpn & 0xFF) | (byte3 << 8); + skipEmitUmp = true; + break; + case CMIDI2_CC_RPN_LSB: + context->context_rpn = (context->context_rpn & 0xFF00) | byte3; + skipEmitUmp = true; + break; + case CMIDI2_CC_NRPN_MSB: + context->context_nrpn = (context->context_nrpn & 0xFF) | byte3 << 8; + skipEmitUmp = true; + break; + case CMIDI2_CC_NRPN_LSB: + context->context_nrpn = (context->context_nrpn & 0xFF00) | byte3; + skipEmitUmp = true; + break; + case CMIDI2_CC_DTE_MSB: + context->context_dte = (context->context_dte & 0xFF) | (byte3 << 8); + + if (context->allow_reordered_dte && (context->context_dte & 0x8080) == 0) + m2 = cmidi2_internal_convert_midi1_dte_to_ump(context, channel); + else + skipEmitUmp = true; + + break; + case CMIDI2_CC_DTE_LSB: + context->context_dte = (context->context_dte & 0xFF00) | byte3; + + if ((context->context_dte & 0x8000) && !context->allow_reordered_dte) + return CMIDI2_CONVERSION_RESULT_INVALID_DTE_SEQUENCE; + if ((context->context_rpn & 0x8080) && (context->context_nrpn & 0x8080)) + return CMIDI2_CONVERSION_RESULT_INVALID_DTE_SEQUENCE; + m2 = cmidi2_internal_convert_midi1_dte_to_ump(context, channel); + + break; + case CMIDI2_CC_BANK_SELECT: + context->context_bank = (context->context_bank & 0xFF) | (byte3 << 8); + skipEmitUmp = true; + break; + case CMIDI2_CC_BANK_SELECT_LSB: + context->context_bank = (context->context_bank & 0xFF00) | byte3; + skipEmitUmp = true; + break; + default: + m2 = cmidi2_ump_midi2_cc(context->group, channel, byte2, byte3 << 25); + break; + } + break; + case CMIDI2_STATUS_PROGRAM: + bankMsbValid = (context->context_bank & 0x8000) == 0; + bankLsbValid = (context->context_bank & 0x80) == 0; + bankValid = bankMsbValid || bankLsbValid; + m2 = cmidi2_ump_midi2_program( + context->group, channel, + bankValid ? CMIDI2_PROGRAM_CHANGE_OPTION_BANK_VALID + : CMIDI2_PROGRAM_CHANGE_OPTION_NONE, + byte2, bankMsbValid ? context->context_bank >> 8 : 0, + bankLsbValid ? context->context_bank & 0x7F : 0); + context->context_bank = 0x8080; + break; + case CMIDI2_STATUS_CAF: + m2 = cmidi2_ump_midi2_caf(context->group, channel, byte2 << 25); + break; + case CMIDI2_STATUS_PITCH_BEND: + // Note: Pitch Bend values in the MIDI 1.0 Protocol are presented as Little Endian. + m2 = cmidi2_ump_midi2_pitch_bend_direct( + context->group, channel, ((byte3 << 7) + byte2) << 18); + break; + default: + return CMIDI2_CONVERSION_RESULT_INVALID_STATUS; + } + if (!skipEmitUmp) + { + int platEndian = cmidi2_util_is_platform_little_endian() + ? CMIDI2_TRANSLATOR_LITTLE_ENDIAN + : CMIDI2_TRANSLATOR_BIG_ENDIAN; + int actualEndian + = context->ump_serialization_endianness != CMIDI2_TRANSLATOR_DEFAULT_ENDIAN + ? context->ump_serialization_endianness + : platEndian; + if (platEndian != actualEndian) + m2 = (((uint64_t)cmidi2_internal_swap_endian(m2 >> 32)) << 32) + | cmidi2_internal_swap_endian(m2 & 0xFFFFFFFF); + *(uint32_t*)(dst + *dIdx) = m2 >> 32; + *dIdx += 4; + *(uint32_t*)(dst + *dIdx) = m2 & 0xFFFFFFFF; + *dIdx += 4; + } + *sIdx += len; + } + } + } + // incomplete UMP sequence will be reported as CMIDI2_CONVERSION_RESULT_OUT_OF_SPACE, + // so it is safe to judge that incomplete RPN/NRPN/DTE state at this state means invalid. + if (context->context_rpn != 0x8080 || context->context_nrpn != 0x8080 + || context->context_dte != 0x8080) + return CMIDI2_CONVERSION_RESULT_INVALID_DTE_SEQUENCE; + + return CMIDI2_CONVERSION_RESULT_OK; +} + +// UMP to MIDI1 Translator + +static int32_t cmidi2_internal_convert_jr_timestamp_to_timecode( + int32_t deltaTime, cmidi2_midi_conversion_context* context) +{ + (void)context; + // FIXME: implement + return deltaTime; +} + +static size_t cmidi2_internal_add_midi1_delta_time( + uint8_t* dst, cmidi2_midi_conversion_context* context, int32_t deltaTime) +{ + if (!context || context->skip_delta_time) + return 0; + size_t* dIdx = (size_t*)&context->midi1_proceeded_bytes; + int32_t len = cmidi2_midi1_get_7bit_encoded_int_length(deltaTime); + cmidi2_midi1_write_7bit_encoded_int(dst, deltaTime); + *dIdx += len; + return len; +} + +/// Convert one single UMP (without JR Timestamp) to MIDI 1.0 Message (without delta time) +/// It is a lengthy function, so it is recommended to wrap it in another non-inline function. +static inline size_t cmidi2_convert_single_ump_to_timed_midi1( + uint8_t* dst, size_t maxBytes, cmidi2_ump* ump, int32_t deltaTime, + cmidi2_midi_conversion_context* context, uint8_t* sysex7Buffer, size_t* sysex7BufferIndex) +{ + (void)maxBytes; + size_t midiEventSize = 0; + uint64_t sysex7U64; + uint8_t sysex7NumBytesInUmp; + + uint8_t messageType = cmidi2_ump_get_message_type(ump); + uint8_t statusCode = cmidi2_ump_get_status_code(ump); // may not apply, but won't break. + +#define CMIDI2_INTERNAL_ADD_DELTA_TIME \ + dst += cmidi2_internal_add_midi1_delta_time(dst, context, deltaTime); \ + dst[0] = statusCode | cmidi2_ump_get_channel(ump); + + switch (messageType) + { + case CMIDI2_MESSAGE_TYPE_SYSTEM: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 1; + switch (statusCode) + { + case 0xF1: + case 0xF3: + case 0xF9: + dst[1] = cmidi2_ump_get_system_message_byte2(ump); + midiEventSize = 2; + break; + } + break; + case CMIDI2_MESSAGE_TYPE_MIDI_1_CHANNEL: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 3; + dst[1] = cmidi2_ump_get_midi1_byte2(ump); + switch (statusCode) + { + case 0xC0: + case 0xD0: + midiEventSize = 2; + break; + default: + dst[2] = cmidi2_ump_get_midi1_byte3(ump); + break; + } + break; + case CMIDI2_MESSAGE_TYPE_MIDI_2_CHANNEL: + // FIXME: convert MIDI2 to MIDI1 as long as possible + switch (statusCode) + { + case CMIDI2_STATUS_RPN: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 12; + dst[0] = cmidi2_ump_get_channel(ump) + CMIDI2_STATUS_CC; + dst[1] = CMIDI2_CC_RPN_MSB; + dst[2] = cmidi2_ump_get_midi2_rpn_msb(ump); + dst[3] = dst[0]; // CC + channel + dst[4] = CMIDI2_CC_RPN_LSB; + dst[5] = cmidi2_ump_get_midi2_rpn_lsb(ump); + dst[6] = dst[0]; // CC + channel + dst[7] = CMIDI2_CC_DTE_MSB; + dst[8] = (cmidi2_ump_get_midi2_rpn_data(ump) >> 25) & 0x7F; + dst[9] = dst[0]; // CC + channel + dst[10] = CMIDI2_CC_DTE_LSB; + dst[11] = (cmidi2_ump_get_midi2_rpn_data(ump) >> 18) & 0x7F; + break; + case CMIDI2_STATUS_NRPN: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 12; + dst[0] = cmidi2_ump_get_channel(ump) + CMIDI2_STATUS_CC; + dst[1] = CMIDI2_CC_NRPN_MSB; + dst[2] = cmidi2_ump_get_midi2_nrpn_msb(ump); + dst[3] = dst[0]; // CC + channel + dst[4] = CMIDI2_CC_NRPN_LSB; + dst[5] = cmidi2_ump_get_midi2_nrpn_lsb(ump); + dst[6] = dst[0]; // CC + channel + dst[7] = CMIDI2_CC_DTE_MSB; + dst[8] = (cmidi2_ump_get_midi2_nrpn_data(ump) >> 25) & 0x7F; + dst[9] = dst[0]; // CC + channel + dst[10] = CMIDI2_CC_DTE_LSB; + dst[11] = (cmidi2_ump_get_midi2_nrpn_data(ump) >> 18) & 0x7F; + break; + case CMIDI2_STATUS_NOTE_OFF: + case CMIDI2_STATUS_NOTE_ON: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 3; + dst[1] = cmidi2_ump_get_midi2_note_note(ump); + dst[2] = cmidi2_ump_get_midi2_note_velocity(ump) / 0x200; + break; + case CMIDI2_STATUS_PAF: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 3; + dst[1] = cmidi2_ump_get_midi2_paf_note(ump); + dst[2] = cmidi2_ump_get_midi2_paf_data(ump) / 0x2000000; + break; + case CMIDI2_STATUS_CC: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 3; + dst[0] = statusCode | cmidi2_ump_get_channel(ump); + dst[1] = cmidi2_ump_get_midi2_cc_index(ump); + dst[2] = cmidi2_ump_get_midi2_cc_data(ump) / 0x2000000; + break; + case CMIDI2_STATUS_PROGRAM: + CMIDI2_INTERNAL_ADD_DELTA_TIME + dst[0] = statusCode | cmidi2_ump_get_channel(ump); + if (cmidi2_ump_get_midi2_program_options(ump) & CMIDI2_PROGRAM_CHANGE_OPTION_BANK_VALID) + { + midiEventSize = 8; + dst[6] = dst[0]; // copy + dst[7] = cmidi2_ump_get_midi2_program_program(ump); + dst[0] = (dst[6] & 0xF) + CMIDI2_STATUS_CC; + dst[1] = 0; // Bank MSB + dst[2] = cmidi2_ump_get_midi2_program_bank_msb(ump); + dst[3] = (dst[6] & 0xF) + CMIDI2_STATUS_CC; + dst[4] = 32; // Bank LSB + dst[5] = cmidi2_ump_get_midi2_program_bank_lsb(ump); + } + else + { + midiEventSize = 2; + dst[1] = cmidi2_ump_get_midi2_program_program(ump); + } + break; + case CMIDI2_STATUS_CAF: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 2; + dst[1] = cmidi2_ump_get_midi2_caf_data(ump) / 0x2000000; + break; + case CMIDI2_STATUS_PITCH_BEND: + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 3; + uint32_t pitchBendV1 = cmidi2_ump_get_midi2_pitch_bend_data(ump) / 0x40000; + dst[1] = pitchBendV1 % 0x80; + dst[2] = pitchBendV1 / 0x80; + break; + // skip for other status bytes; we cannot support them. + } + break; + case CMIDI2_MESSAGE_TYPE_SYSEX7: + if (sysex7Buffer) + { + // sysex7 buffer is processed at cmidi2_convert_ump_to_midi1(). + midiEventSize = 0; + sysex7U64 = cmidi2_ump_read_uint64_bytes(ump); + sysex7NumBytesInUmp = cmidi2_ump_get_sysex7_num_bytes(ump); + for (size_t i = 0; i < sysex7NumBytesInUmp; i++) + sysex7Buffer[*sysex7BufferIndex + i] = cmidi2_ump_get_byte_from_uint64(sysex7U64, 2 + i); + *sysex7BufferIndex += sysex7NumBytesInUmp; + } + else + { + // minimal implementation for single-byte sysex7 + CMIDI2_INTERNAL_ADD_DELTA_TIME + midiEventSize = 1 + cmidi2_ump_get_sysex7_num_bytes(ump); + sysex7U64 = cmidi2_ump_read_uint64_bytes(ump); + for (size_t i = 0; i < midiEventSize - 1; i++) + dst[i] = cmidi2_ump_get_byte_from_uint64(sysex7U64, 2 + i); + } + break; + case CMIDI2_MESSAGE_TYPE_SYSEX8_MDS: + // By the UMP specification they cannot be translated in Default Translation + midiEventSize = 0; + break; + } + return midiEventSize; +} + +// Left for API backward compatibility. +static inline size_t +cmidi2_convert_single_ump_to_midi1(uint8_t* dst, size_t maxBytes, cmidi2_ump* ump) +{ + return cmidi2_convert_single_ump_to_timed_midi1(dst, maxBytes, ump, 0, NULL, NULL, 0); +} + +#define IS_JR_TIMESTAMP(ump) \ + (cmidi2_ump_get_message_type(ump) == CMIDI2_MESSAGE_TYPE_UTILITY \ + && cmidi2_ump_get_status_code(ump) == CMIDI2_UTILITY_STATUS_JR_TIMESTAMP) + +static enum cmidi2_midi_conversion_result +cmidi2_convert_ump_to_midi1(cmidi2_midi_conversion_context* context) +{ + uint8_t* dst = (uint8_t*)context->midi1; + size_t sLen = context->ump_num_bytes; + size_t dLen = context->midi1_num_bytes; + size_t* sIdx = (size_t*)&context->ump_proceeded_bytes; + size_t* dIdx = (size_t*)&context->midi1_proceeded_bytes; + uint8_t sysex7_buffer[1024]; + size_t sysex7_buffer_index = 0; + + while (*sIdx < sLen) + { + int32_t deltaTimeInJRTimestamp = 0; + cmidi2_ump* ump; + do + { + ump = (cmidi2_ump*)((uint8_t*)context->ump + *sIdx); + if (IS_JR_TIMESTAMP(ump)) + { + if (!context->skip_delta_time) + deltaTimeInJRTimestamp += cmidi2_ump_get_jr_timestamp_timestamp(ump); + } + else + break; + *sIdx += 4; // 4 = sizeof JR Timestamp message + } while (*sIdx < sLen); + + if (*sIdx >= sLen) + break; + + int32_t deltaTime + = cmidi2_internal_convert_jr_timestamp_to_timecode(deltaTimeInJRTimestamp, context); + + int32_t len = cmidi2_convert_single_ump_to_timed_midi1( + dst + *dIdx, dLen - *dIdx, ump, deltaTime, context, sysex7_buffer, &sysex7_buffer_index); + *dIdx += len; + *sIdx += cmidi2_ump_get_num_bytes(*ump); + + if (cmidi2_ump_get_message_type(ump) == CMIDI2_MESSAGE_TYPE_SYSEX7) + { + switch (cmidi2_ump_get_status_code(ump)) + { + case CMIDI2_SYSEX_END: + case CMIDI2_SYSEX_IN_ONE_UMP: + if (!context->skip_delta_time) + cmidi2_internal_add_midi1_delta_time(dst + *dIdx, context, deltaTime); + dst[*dIdx] = 0xF0; + *dIdx += 1; + memcpy(dst + *dIdx, sysex7_buffer, sysex7_buffer_index); + *dIdx += sysex7_buffer_index; + sysex7_buffer_index = 0; + dst[*dIdx] = 0xF7; + *dIdx += 1; + break; + } + } + deltaTime = 0; + } + + return sysex7_buffer_index > 0 ? CMIDI2_CONVERSION_RESULT_INCOMPLETE_SYSEX7 + : CMIDI2_CONVERSION_RESULT_OK; +} + +// UMP forge (like LV2 Atom Forge, but flat and simpler) + +typedef struct cmidi2_ump_forge +{ + cmidi2_ump* ump; + size_t capacity; + size_t offset; +} cmidi2_ump_forge; + +static inline void +cmidi2_ump_forge_init(cmidi2_ump_forge* forge, cmidi2_ump* buffer, size_t capacityInBytes) +{ + forge->ump = buffer; + forge->capacity = capacityInBytes; + forge->offset = 0; +} + +static inline bool cmidi2_ump_forge_add_packet_32(cmidi2_ump_forge* forge, uint32_t ump) +{ + int size = 4; + if (forge->offset + size > forge->capacity) + return false; + uint8_t* p = (uint8_t*)forge->ump + forge->offset; + cmidi2_ump_write32((cmidi2_ump*)p, ump); + forge->offset += size; + return true; +} + +static inline bool cmidi2_ump_forge_add_packet_64(cmidi2_ump_forge* forge, uint64_t ump) +{ + int size = 8; + if (forge->offset + size > forge->capacity) + return false; + uint8_t* p = (uint8_t*)forge->ump + forge->offset; + cmidi2_ump_write64((cmidi2_ump*)p, ump); + forge->offset += size; + return true; +} + +static inline bool +cmidi2_ump_forge_add_packet_128(cmidi2_ump_forge* forge, uint64_t ump1, uint64_t ump2) +{ + int size = 16; + if (forge->offset + size > forge->capacity) + return false; + uint8_t* p = ((uint8_t*)forge->ump) + forge->offset; + cmidi2_ump_write128((cmidi2_ump*)p, ump1, ump2); + forge->offset += size; + return true; +} + +static inline bool cmidi2_ump_forge_add_single_packet(cmidi2_ump_forge* forge, cmidi2_ump* ump) +{ + int size = cmidi2_ump_get_message_size_bytes(ump); + if (forge->offset + size > forge->capacity) + return false; + memcpy((uint8_t*)forge->ump + forge->offset, ump, size); + forge->offset += size; + return true; +} + +static inline bool +cmidi2_ump_forge_add_packets(cmidi2_ump_forge* forge, cmidi2_ump* ump, int32_t size) +{ + if (forge->offset + size > forge->capacity) + return false; + memcpy((uint8_t*)forge->ump + forge->offset, ump, size); + forge->offset += size; + return true; +} + +// UMP sequence merger + +static inline bool cmidi2_internal_ump_merge_sequence_write_delta_time( + int32_t timestamp1, int32_t timestamp2, int32_t* lastTimestamp, void* dst, int32_t* dIdx, + size_t dstSize) +{ + int32_t deltaTime = (timestamp1 <= timestamp2 ? timestamp1 : timestamp2) - *lastTimestamp; + uint8_t jrTSSize = deltaTime / 0x10000 + (deltaTime % 0x10000 ? 1 : 0); + if (*dIdx + jrTSSize * 4 > (int64_t)dstSize) + return false; + for (int32_t dt = deltaTime; dt > 0; dt -= 0x10000) + { + cmidi2_ump_write32( + (cmidi2_ump*)((uint8_t*)dst + *dIdx), + cmidi2_ump_jr_timestamp_direct(0, deltaTime > 0xFFFF ? 0xFFFF : deltaTime)); + *dIdx += 4; + } + *lastTimestamp += deltaTime; + return true; +} + +static inline size_t cmidi2_ump_merge_sequences( + cmidi2_ump* dst, size_t dstCapacity, cmidi2_ump* seq1, size_t seq1Size, cmidi2_ump* seq2, + size_t seq2Size) +{ + int32_t dIdx = 0, seq1Idx = 0, seq2Idx = 0; + int32_t timestamp1 = 0, timestamp2 = 0, lastTimestamp = 0; + while (true) + { + cmidi2_ump* s1 = (cmidi2_ump*)((uint8_t*)seq1 + seq1Idx); + while (cmidi2_ump_get_message_type(s1) == CMIDI2_MESSAGE_TYPE_UTILITY + && cmidi2_ump_get_status_code(s1) == CMIDI2_UTILITY_STATUS_JR_TIMESTAMP) + { + timestamp1 += cmidi2_ump_get_jr_timestamp_timestamp(s1); + seq1Idx += 4; + if (seq1Idx >= (int64_t)seq1Size) + break; + s1 = (cmidi2_ump*)((uint8_t*)seq1 + seq1Idx); + } + cmidi2_ump* s2 = (cmidi2_ump*)((uint8_t*)seq2 + seq2Idx); + while (cmidi2_ump_get_message_type(s2) == CMIDI2_MESSAGE_TYPE_UTILITY + && cmidi2_ump_get_status_code(s2) == CMIDI2_UTILITY_STATUS_JR_TIMESTAMP) + { + timestamp2 += cmidi2_ump_get_jr_timestamp_timestamp(s2); + seq2Idx += 4; + if (seq2Idx >= (int64_t)seq2Size) + break; + s2 = (cmidi2_ump*)((uint8_t*)seq2 + seq2Idx); + } + if (seq1Idx >= (int64_t)seq1Size || seq2Idx >= (int64_t)seq2Size) + break; + if (!cmidi2_internal_ump_merge_sequence_write_delta_time( + timestamp1, timestamp2, &lastTimestamp, dst, &dIdx, dstCapacity)) + break; + + if (timestamp1 <= timestamp2) + { + cmidi2_ump* sp = (cmidi2_ump*)((uint8_t*)seq1 + seq1Idx); + uint8_t size = cmidi2_ump_get_message_size_bytes(sp); + if (size == 0) + return dIdx; // invalid bytes + memcpy((uint8_t*)dst + dIdx, sp, size); + seq1Idx += size; + dIdx += size; + } + else + { + cmidi2_ump* sp = (cmidi2_ump*)((uint8_t*)seq2 + seq2Idx); + uint8_t size = cmidi2_ump_get_message_size_bytes(sp); + if (size == 0) + return dIdx; // invalid bytes + memcpy((uint8_t*)dst + dIdx, sp, size); + seq2Idx += size; + dIdx += size; + } + } + if (!cmidi2_internal_ump_merge_sequence_write_delta_time( + timestamp1, timestamp2, &lastTimestamp, dst, &dIdx, dstCapacity)) + return dIdx; + if (seq1Idx < (int64_t)seq1Size && dIdx + (int64_t)seq1Size - seq1Idx < (int64_t)dstCapacity) + { + cmidi2_ump* sp = (cmidi2_ump*)((uint8_t*)seq1 + seq1Idx); + memcpy((uint8_t*)dst + dIdx, sp, seq1Size - seq1Idx); + dIdx += seq1Size - seq1Idx; + } + if (seq2Idx < (int64_t)seq2Size && dIdx + (int64_t)seq2Size - seq2Idx < (int64_t)dstCapacity) + { + cmidi2_ump* sp = (cmidi2_ump*)((uint8_t*)seq2 + seq2Idx); + memcpy((uint8_t*)dst + dIdx, sp, seq2Size - seq2Idx); + dIdx += seq2Size - seq2Idx; + } + return dIdx; +} + +#ifdef __cplusplus +} +#endif + +#endif /* CMIDI2_H_INCLUDED */ diff --git a/include/libremidi/config.hpp b/include/libremidi/config.hpp new file mode 100644 index 0000000..47ac916 --- /dev/null +++ b/include/libremidi/config.hpp @@ -0,0 +1,68 @@ +#pragma once + +#if defined(MSC_VER) + #define NOMINMAX 1 + #define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#include +#include +#include + +#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() + #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() && !defined(LIBREMIDI_NO_BOOST) + + #if LIBREMIDI_SLIM_MESSAGE > 0 + #include +namespace libremidi +{ +using midi_bytes = boost::container::static_vector; +} + #else + #include +namespace libremidi +{ +static constexpr int small_vector_minimum_size + = sizeof(boost::container::small_vector); +using midi_bytes = boost::container::small_vector; +} + #endif +#else +namespace libremidi +{ +using midi_bytes = std::vector; +} +#endif + +#if __has_include() && 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 diff --git a/include/libremidi/configurations.hpp b/include/libremidi/configurations.hpp new file mode 100644 index 0000000..c905fea --- /dev/null +++ b/include/libremidi/configurations.hpp @@ -0,0 +1,35 @@ +#pragma once + +#if defined(__linux__) + #include + #include + #include + #include + #include +#endif + +#if defined(__APPLE__) + #include + #include +#endif + +#if defined(_WIN32) + #include + #include + #include +#endif + +#if defined(__EMSCRIPTEN__) + #include +#endif + +#include + +namespace libremidi +{ + +struct dummy_configuration +{ +}; + +} diff --git a/include/libremidi/defaults.hpp b/include/libremidi/defaults.hpp new file mode 100644 index 0000000..6d380dc --- /dev/null +++ b/include/libremidi/defaults.hpp @@ -0,0 +1,98 @@ +#pragma once +#include +#include +#include +#include +#include + +#include +#include + +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 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 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 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 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 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 out_default_port() noexcept +{ + return libremidi::out_default_port(default_api()); +} +} +} diff --git a/include/libremidi/detail/memory.hpp b/include/libremidi/detail/memory.hpp new file mode 100644 index 0000000..d398c42 --- /dev/null +++ b/include/libremidi/detail/memory.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace libremidi +{ +template +struct deleter +{ + template + void operator()(U* x) noexcept(noexcept(func(x))) + { + func(x); + } +}; + +template +using unique_handle = std::unique_ptr>; + +template +std::shared_ptr instance() +{ + static std::mutex mut; + static std::weak_ptr cache; + + std::lock_guard _{mut}; + + if (auto ptr = cache.lock()) + { + return ptr; + } + else + { + auto shared = std::make_shared(); + cache = shared; + return shared; + } +} +} diff --git a/include/libremidi/detail/midi_api.hpp b/include/libremidi/detail/midi_api.hpp new file mode 100644 index 0000000..1bf8965 --- /dev/null +++ b/include/libremidi/detail/midi_api.hpp @@ -0,0 +1,87 @@ +#pragma once +#include +#include +#include +#include + +#include +#include + +namespace libremidi +{ +struct error_handler +{ + //! Error reporting function for libremidi classes. Throws. + template + 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_{}; +}; +} diff --git a/include/libremidi/detail/midi_in.hpp b/include/libremidi/detail/midi_in.hpp new file mode 100644 index 0000000..797b0da --- /dev/null +++ b/include/libremidi/detail/midi_in.hpp @@ -0,0 +1,52 @@ +#pragma once +#include +#include + +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 +std::unique_ptr make(libremidi::input_configuration&& conf, Arg&& arg) +{ + return std::make_unique(std::move(conf), std::move(arg)); +} + +template +std::unique_ptr make(libremidi::ump_input_configuration&& conf, Arg&& arg) +{ + return std::make_unique(std::move(conf), std::move(arg)); +} +} diff --git a/include/libremidi/detail/midi_out.hpp b/include/libremidi/detail/midi_out.hpp new file mode 100644 index 0000000..e3ae1c3 --- /dev/null +++ b/include/libremidi/detail/midi_out.hpp @@ -0,0 +1,95 @@ +#pragma once +#include +#include +#include + +#include + +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(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(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 +std::unique_ptr make(libremidi::output_configuration&& conf, Arg&& arg) +{ + return std::make_unique(std::move(conf), std::forward(arg)); +} +} diff --git a/include/libremidi/detail/midi_stream_decoder.hpp b/include/libremidi/detail/midi_stream_decoder.hpp new file mode 100644 index 0000000..2b48dab --- /dev/null +++ b/include/libremidi/detail/midi_stream_decoder.hpp @@ -0,0 +1,314 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace libremidi +{ +static inline int64_t system_ns() noexcept +{ + namespace clk = std::chrono; + return clk::duration_cast(clk::steady_clock::now().time_since_epoch()).count(); +} + +struct timestamp_backend_info +{ + // The API provides some kind of timestamping + bool has_absolute_timestamps{}; + + // The provided timestamping is equivalent or more precise than + // e.g. clock_gettime(CLOCK_MONOTONIC) + bool absolute_is_monotonic{}; + + // The API can provide samples in a buffer cycle (only PipeWire and JACK so far) + bool has_samples{}; +}; + +namespace midi1 +{ +struct input_state_machine +{ + const input_configuration& configuration; + explicit input_state_machine(const input_configuration& conf) + : configuration{conf} + { + } + + bool has_finished_sysex(std::span bytes) const noexcept + { + return (((bytes.front() == 0xF0) || (state == in_sysex)) && (bytes.back() == 0xF7)); + } + + // Function to process a byte stream which may contain multiple successive + // MIDI events (CoreMIDI, ALSA Sequencer can work like this) + void on_bytes_multi(std::span bytes, int64_t timestamp) + { + int64_t nBytes = bytes.size(); + int64_t iByte = 0; + + const bool finished_sysex = has_finished_sysex(bytes); + switch (state) + { + case in_sysex: { + return on_continue_sysex(bytes, finished_sysex); + } + case main: { + while (iByte < nBytes) + { + int64_t size = 1; + // We are expecting that the next byte in the packet is a status + // byte. + const auto status = bytes[iByte]; + if (!(status & 0x80)) + break; + + // Determine the number of bytes in the MIDI message. + if (status < 0xC0) + size = 3; + else if (status < 0xE0) + size = 2; + else if (status < 0xF0) + size = 3; + else if (status == 0xF0) + { + if (configuration.ignore_sysex) + { + size = 0; + iByte = nBytes; + } + else + { + size = nBytes - iByte; + } + + if (bytes[nBytes - 1] != 0xF7) + { + // We know per CoreMIDI API there can't be anything else in this packet + state = in_sysex; + message.assign(bytes.begin(), bytes.begin() + size); + message.timestamp = timestamp; + return; + } + } + else if (status == 0xF1) + { + // A MIDI time code message + if (configuration.ignore_timing) + { + size = 0; + iByte += 2; + } + else + { + size = 2; + } + } + else if (status == 0xF2) + size = 3; + else if (status == 0xF3) + size = 2; + else if (status == 0xF8) + { + // A MIDI timing tick message + if (configuration.ignore_timing) + { + size = 0; + iByte += 1; + } + else + { + size = 1; + } + } + else if (status == 0xFE) + { + // A MIDI active sensing message + if (configuration.ignore_sensing) + { + size = 0; + iByte += 1; + } + else + { + size = 1; + } + } + else + { + // Remaining real-time messages + size = 1; + } + + // Now process the actual bytes of the message + if (size > 0) + { + auto begin = bytes.begin() + iByte; + message.assign(begin, begin + size); + message.timestamp = timestamp; + + this->configuration.on_message(std::move(message)); + message.clear(); + + iByte += size; + } + } + } + } + } + + void on_continue_sysex(std::span bytes, bool finished_sysex) + { + if (finished_sysex) + state = main; + + if (configuration.ignore_sysex) + { + return; + } + else + { + message.insert(message.end(), bytes.begin(), bytes.end()); + if (finished_sysex) + { + this->configuration.on_message(std::move(message)); + message.clear(); + } + } + return; + } + + void on_main(std::span bytes, int64_t timestamp, bool finished_sysex) + { + switch (bytes[0]) + { + // SYSEX start + case 0xF0: { + if (!finished_sysex) + state = in_sysex; + + if (!this->configuration.ignore_sysex) + { + message.assign(bytes.begin(), bytes.end()); + message.timestamp = timestamp; + if (finished_sysex) + { + this->configuration.on_message(std::move(message)); + message.clear(); + } + } + + return; + } + + case 0xF1: + case 0xF8: + if (this->configuration.ignore_timing) + return; + break; + + case 0xFE: + if (this->configuration.ignore_sensing) + return; + break; + + default: + break; + } + + message.assign(bytes.begin(), bytes.end()); + message.timestamp = timestamp; + + this->configuration.on_message(std::move(message)); + message.clear(); + } + + // Function to process bytes corresponding to at most one midi event + // e.g. a midi channel event or a single sysex + void on_bytes(std::span bytes, int64_t timestamp) + { + if (bytes.empty()) + return; + + const bool finished_sysex = has_finished_sysex(bytes); + switch (state) + { + case in_sysex: + return on_continue_sysex(bytes, finished_sysex); + + case main: + return on_main(bytes, timestamp, finished_sysex); + } + } + + template + int64_t timestamp(auto to_ns, int64_t samples) + { + switch (configuration.timestamps) + { + default: + case timestamp_mode::NoTimestamp: + return 0; + + case timestamp_mode::Relative: { + int64_t time_ns; + + if constexpr (info.has_absolute_timestamps) + time_ns = to_ns(); + else + time_ns = system_ns(); + + int64_t res; + if (first_message) + { + first_message = false; + res = 0; + } + else + { + res = time_ns - last_time_ns; + } + + last_time_ns = time_ns; + return res; + } + + case timestamp_mode::Absolute: + if constexpr (info.has_absolute_timestamps) + return to_ns(); + else + return system_ns(); + + case timestamp_mode::SystemMonotonic: + if constexpr (info.absolute_is_monotonic) + return to_ns(); + else + return system_ns(); + + case timestamp_mode::AudioFrame: + if constexpr (info.has_samples) + return samples; + else + return 0; + + case timestamp_mode::Custom: + return configuration.get_timestamp(to_ns()); + } + } + + libremidi::message message; + + int64_t last_time_ns = 0; + enum + { + main, + in_sysex + } state{main}; + + bool first_message = true; +}; +} +} diff --git a/include/libremidi/detail/observer.hpp b/include/libremidi/detail/observer.hpp new file mode 100644 index 0000000..4fe86bc --- /dev/null +++ b/include/libremidi/detail/observer.hpp @@ -0,0 +1,26 @@ +#pragma once +#include +#include +#include + +#include +#include + +namespace libremidi +{ +class observer_api +{ +public: + virtual ~observer_api() = default; + + virtual libremidi::API get_current_api() const noexcept = 0; + virtual std::vector get_input_ports() const noexcept = 0; + virtual std::vector get_output_ports() const noexcept = 0; +}; + +template +std::unique_ptr make(libremidi::observer_configuration&& conf, Arg&& arg) +{ + return std::make_unique(std::move(conf), std::move(arg)); +} +} diff --git a/include/libremidi/detail/semaphore.hpp b/include/libremidi/detail/semaphore.hpp new file mode 100644 index 0000000..255c45f --- /dev/null +++ b/include/libremidi/detail/semaphore.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +struct semaphore_pair_lock +{ + std::binary_semaphore sem_cleanup{0}; + std::binary_semaphore sem_needpost{0}; + void prepare_release_client() + { + using namespace std::literals; + + // FIXME if jack is not running we can skip this + this->sem_needpost.release(); + this->sem_cleanup.try_acquire_for(1s); + } + + void check_client_released() + { + if (!this->sem_needpost.try_acquire()) + this->sem_cleanup.release(); + } +}; diff --git a/include/libremidi/detail/ump_stream.hpp b/include/libremidi/detail/ump_stream.hpp new file mode 100644 index 0000000..6a0b90a --- /dev/null +++ b/include/libremidi/detail/ump_stream.hpp @@ -0,0 +1,58 @@ +#pragma once +#include + +#include +#include + +namespace libremidi +{ +enum class segmentation_error +{ + no_error, + need_space, + other +}; + +/** + * Utility function to segment an ump stream into individual messages. + * Used to send a stream to APIs that work message-by-message. + */ +inline void +segment_ump_stream(const uint32_t* ump_stream, int64_t count, auto write_func, auto realloc_func) +{ + while (count > 0) + { + // Handle NOOP (or padding) + while (count > 0 && ump_stream[0] == 0) + { + count--; + ump_stream++; + } + + if (count == 0) + break; + + const auto ump_bytes = cmidi2_ump_get_num_bytes(ump_stream[0]); + + // FIXME std::expected, propagate the error back to caller? + switch (write_func(ump_stream, ump_bytes)) + { + case segmentation_error::no_error: + break; + case segmentation_error::need_space: + // Try again if we didn't have enough space in the OS queue + realloc_func(); + if (write_func(ump_stream, ump_bytes) != segmentation_error::no_error) + return; + break; + case segmentation_error::other: + return; + } + + const auto ump_uints = ump_bytes / 4; + ump_stream += ump_uints; + count -= ump_uints; + } +} + +} diff --git a/include/libremidi/error.hpp b/include/libremidi/error.hpp new file mode 100644 index 0000000..c84cd5b --- /dev/null +++ b/include/libremidi/error.hpp @@ -0,0 +1,89 @@ +#pragma once +#include + +#include +#include +#include + +namespace libremidi +{ +//! Defines various error types. +enum midi_error +{ + WARNING, /*!< A non-critical error. */ + UNSPECIFIED, /*!< The default, unspecified error type. */ + NO_DEVICES_FOUND, /*!< No devices found on system. */ + INVALID_DEVICE, /*!< An invalid device ID was specified. */ + MEMORY_ERROR, /*!< An error occured during memory allocation. */ + INVALID_PARAMETER, /*!< An invalid parameter was specified to a function. */ + INVALID_USE, /*!< The function was called incorrectly. */ + DRIVER_ERROR, /*!< A system driver error occured. */ + SYSTEM_ERROR, /*!< A system error occured. */ + THREAD_ERROR /*!< A thread error occured. */ +}; + +//! Base exception class for MIDI problems +struct LIBREMIDI_EXPORT midi_exception : public std::runtime_error +{ + using std::runtime_error::runtime_error; + ~midi_exception() override; +}; + +struct LIBREMIDI_EXPORT no_devices_found_error final : public midi_exception +{ + static constexpr auto code = midi_error::NO_DEVICES_FOUND; + using midi_exception::midi_exception; + ~no_devices_found_error() override; +}; +struct LIBREMIDI_EXPORT invalid_device_error final : public midi_exception +{ + static constexpr auto code = midi_error::INVALID_DEVICE; + using midi_exception::midi_exception; + ~invalid_device_error() override; +}; +struct LIBREMIDI_EXPORT memory_error final : public midi_exception +{ + static constexpr auto code = midi_error::MEMORY_ERROR; + using midi_exception::midi_exception; + ~memory_error() override; +}; +struct LIBREMIDI_EXPORT invalid_parameter_error final : public midi_exception +{ + static constexpr auto code = midi_error::INVALID_PARAMETER; + using midi_exception::midi_exception; + ~invalid_parameter_error() override; +}; +struct LIBREMIDI_EXPORT invalid_use_error final : public midi_exception +{ + static constexpr auto code = midi_error::INVALID_USE; + using midi_exception::midi_exception; + ~invalid_use_error() override; +}; +struct LIBREMIDI_EXPORT driver_error final : public midi_exception +{ + static constexpr auto code = midi_error::DRIVER_ERROR; + using midi_exception::midi_exception; + ~driver_error() override; +}; +struct LIBREMIDI_EXPORT system_error final : public midi_exception +{ + static constexpr auto code = midi_error::SYSTEM_ERROR; + using midi_exception::midi_exception; + ~system_error() override; +}; +struct LIBREMIDI_EXPORT thread_error final : public midi_exception +{ + static constexpr auto code = midi_error::THREAD_ERROR; + using midi_exception::midi_exception; + ~thread_error() override; +}; + +/*! \brief Error callback function + \param type Type of error. + \param errorText Error description. + + Note that class behaviour is undefined after a critical error (not + a warning) is reported. + */ +using midi_error_callback = std::function; +} diff --git a/include/libremidi/input_configuration.hpp b/include/libremidi/input_configuration.hpp new file mode 100644 index 0000000..77d8444 --- /dev/null +++ b/include/libremidi/input_configuration.hpp @@ -0,0 +1,118 @@ +#pragma once +#include +#include +#include +#include + +#include +#include + +namespace libremidi +{ +//! Specify how timestamps are handled in the system +enum timestamp_mode +{ + //! No timestamping at all, all timestamps are zero + NoTimestamp, + + //! In nanoseconds, timestamp is the time since the previous event (or zero) + Relative, + + //! In nanoseconds, as per an arbitrary reference which may be provided by the host API, + //! e.g. since the JACK cycle start, ALSA sequencer queue creation, through AudioHostTime on macOS. + //! It offers the most precise ordering between events as it's the closest to the real timestamp of + //! the event as provided by the host API. + //! If the API does not provide any timing, it will be mapped to SystemMonotonic instead. + Absolute, + + //! In nanoseconds, as per std::steady_clock::now() or equivalent (raw if possible). + //! May be less precise than Absolute as timestamping is done within the library, + //! but is more useful for system-wide synchronization. + //! Note: depending on the backend, Absolute and SystemMonotonic may be the same. + SystemMonotonic, + + //! For APIs which are based on audio process cycles such as JACK, timestamps will be in frames since + //! the beginning of the current cycle's audio buffer + AudioFrame, + + //! Will call the custom timestamping function provided by the user in the input configuration. + Custom +}; + +using timestamp = int64_t; +using message_callback = std::function; +using raw_callback = std::function, timestamp)>; +using timestamp_callback = std::function; +struct input_configuration +{ + //! Set a callback function to be invoked for incoming MIDI messages. + //! Mandatory! + message_callback on_message; + + //! Set a custom callback function to be invoked for timestamping MIDI messages. + //! Input: the API provided timestamp in nanoseconds, if available, for reference. + //! (e.g. the same as "Absolute"). + //! Mandatory if timestamps == timestamp_mode::Custom, unused otherwise. + timestamp_callback get_timestamp; + + //! Set an 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{}; + + //! 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; + + //! Timestamp mode. See @libremidi::timestamp_mode + uint32_t timestamps : 3 = timestamp_mode::Absolute; +}; + +using ump_callback = std::function; +struct ump_input_configuration +{ + //! Set a callback function to be invoked for incoming UMP messages. + ump_callback on_message; + + //! Set a custom callback function to be invoked for timestamping MIDI messages. + //! Input: the API provided timestamp in nanoseconds, if available, for reference. + //! (e.g. the same as "Absolute"). + //! Mandatory if timestamps == timestamp_mode::Custom, unused otherwise. + timestamp_callback get_timestamp; + + //! Set an 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{}; + + //! 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; +}; +} diff --git a/include/libremidi/libremidi.cpp b/include/libremidi/libremidi.cpp new file mode 100644 index 0000000..19254b5 --- /dev/null +++ b/include/libremidi/libremidi.cpp @@ -0,0 +1,186 @@ +#if !defined(LIBREMIDI_HEADER_ONLY) + #include +#endif + +#include +#include + +#include + +#include +#include + +namespace libremidi +{ + +LIBREMIDI_INLINE +std::string_view get_version() noexcept +{ + return LIBREMIDI_VERSION; +} + +LIBREMIDI_INLINE std::string_view get_api_name(libremidi::API api) +{ + std::string_view ret; + midi_any::for_backend(api, [&](auto& b) { ret = b.name; }); + return ret; +} + +LIBREMIDI_INLINE std::string_view get_api_display_name(libremidi::API api) +{ + std::string_view ret; + midi_any::for_backend(api, [&](auto& b) { ret = b.display_name; }); + return ret; +} + +LIBREMIDI_INLINE libremidi::API get_compiled_api_by_name(std::string_view name) +{ + libremidi::API ret = libremidi::API::UNSPECIFIED; + midi_any::for_all_backends([&](auto& b) { + if (name == b.name) + ret = b.API; + }); + return ret; +} + +[[nodiscard]] LIBREMIDI_INLINE std::vector available_apis() noexcept +{ + std::vector apis; + midi1::for_all_backends([&](auto b) { apis.push_back(b.API); }); + return apis; +} + +[[nodiscard]] LIBREMIDI_INLINE std::vector available_ump_apis() noexcept +{ + std::vector apis; + midi2::for_all_backends([&](auto b) { apis.push_back(b.API); }); + return apis; +} + +LIBREMIDI_INLINE midi_exception::~midi_exception() = default; +LIBREMIDI_INLINE no_devices_found_error::~no_devices_found_error() = default; +LIBREMIDI_INLINE invalid_device_error::~invalid_device_error() = default; +LIBREMIDI_INLINE memory_error::~memory_error() = default; +LIBREMIDI_INLINE invalid_parameter_error::~invalid_parameter_error() = default; +LIBREMIDI_INLINE invalid_use_error::~invalid_use_error() = default; +LIBREMIDI_INLINE driver_error::~driver_error() = default; +LIBREMIDI_INLINE system_error::~system_error() = default; +LIBREMIDI_INLINE thread_error::~thread_error() = default; + +LIBREMIDI_INLINE +std::any midi_in_configuration_for(libremidi::API api) +{ + std::any ret; + midi_any::for_backend(api, [&](T) { + using conf_type = typename T::midi_in_configuration; + ret = conf_type{}; + }); + return ret; +} + +LIBREMIDI_INLINE +std::any midi_out_configuration_for(libremidi::API api) +{ + std::any ret; + midi_any::for_backend(api, [&](T) { + using conf_type = typename T::midi_out_configuration; + ret = conf_type{}; + }); + return ret; +} + +LIBREMIDI_INLINE +std::any observer_configuration_for(libremidi::API api) +{ + std::any ret; + midi_any::for_backend(api, [&](T) { + using conf_type = typename T::midi_observer_configuration; + ret = conf_type{}; + }); + return ret; +} + +LIBREMIDI_INLINE +std::any midi_in_configuration_for(const libremidi::observer& obs) +{ + return midi_in_configuration_for(obs.get_current_api()); +} + +LIBREMIDI_INLINE +std::any midi_out_configuration_for(const libremidi::observer& obs) +{ + // FIXME reuse context when meaningful, e.g. ALSA, JACK... + return midi_out_configuration_for(obs.get_current_api()); +} + +LIBREMIDI_INLINE +std::optional in_default_port(libremidi::API api) noexcept +try +{ + libremidi::observer obs{{}, observer_configuration_for(api)}; + if (auto ports = obs.get_input_ports(); !ports.empty()) + return ports.front(); + return std::nullopt; +} +catch (const std::exception& e) +{ + return std::nullopt; +} + +LIBREMIDI_INLINE +std::optional out_default_port(libremidi::API api) noexcept +try +{ + libremidi::observer obs{{}, observer_configuration_for(api)}; + if (auto ports = obs.get_output_ports(); !ports.empty()) + return ports.front(); + return std::nullopt; +} +catch (const std::exception& e) +{ + return std::nullopt; +} + +namespace midi1 +{ +LIBREMIDI_INLINE +std::any in_default_configuration() +{ + return midi_in_configuration_for(default_api()); +} + +LIBREMIDI_INLINE +std::any out_default_configuration() +{ + return midi_out_configuration_for(default_api()); +} + +LIBREMIDI_INLINE +std::any observer_default_configuration() +{ + return observer_configuration_for(default_api()); +} +} + +namespace midi2 +{ +LIBREMIDI_INLINE +std::any in_default_configuration() +{ + return midi_in_configuration_for(default_api()); +} + +LIBREMIDI_INLINE +std::any out_default_configuration() +{ + return midi_out_configuration_for(default_api()); +} + +LIBREMIDI_INLINE +std::any observer_default_configuration() +{ + return observer_configuration_for(default_api()); +} +} + +} diff --git a/include/libremidi/libremidi.hpp b/include/libremidi/libremidi.hpp new file mode 100644 index 0000000..bf9f547 --- /dev/null +++ b/include/libremidi/libremidi.hpp @@ -0,0 +1,271 @@ +#pragma once +/* This software is based on the RtMidi and ModernMidi libraries. + + RtMidi WWW site: http://music.mcgill.ca/~gary/libremidi/ + + RtMidi: realtime MIDI i/o C++ classes + Copyright (c) 2003-2017 Gary P. Scavone + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + Any person wishing to distribute modifications to the Software is + asked to send the modifications to the original developer so that + they can be incorporated into the canonical version. This is, + however, not a binding provision of this license. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR + ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + ------------ + + ModernMidi Copyright (c) 2015, Dimitri Diakopoulos All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include +#include +#include + +#if LIBREMIDI_NI_MIDI2_COMPAT + #include + #include +#endif +#include +#include + +namespace libremidi +{ +//! Main class for observing hotplug of MIDI 1.0 and 2.0 devices. +//! The callbacks will be called whenever a device is added or removed +//! for a given API. +class LIBREMIDI_EXPORT observer +{ +public: + //! Open an observer instance with the given configuration. + //! + //! * api_conf can be an instance of observer_configuration, + //! such as jack_observer_configuration, winmm_observer_configuration, etc... + //! * if no callbacks are passed, no secondary thread will be created unless absolutely necessary + explicit observer(const observer_configuration& conf = {}) noexcept; + explicit observer(observer_configuration conf, std::any api_conf); + observer(const observer&) = delete; + observer(observer&& other) noexcept; + observer& operator=(const observer&) = delete; + observer& operator=(observer&& other) noexcept; + ~observer(); + + [[nodiscard]] libremidi::API get_current_api() const noexcept; + + //! Return identifiers for the available MIDI ports + [[nodiscard]] std::vector get_input_ports() const noexcept; + [[nodiscard]] std::vector get_output_ports() const noexcept; + +private: + std::unique_ptr impl_; +}; + +//! Main class for receiving MIDI 1.0 and 2.0 messages. +class LIBREMIDI_EXPORT midi_in +{ +public: + //! Construct a midi_in object with the default MIDI 1 back-end for the platform + explicit midi_in(const input_configuration& conf) noexcept; + + //! Construct a midi_in object with a configuration object for a specific MIDI 1 back-end + //! see configuration.hpp for the available configuration types. + //! An exception will be thrown if the requested back-end cannot be opened. + explicit midi_in(input_configuration conf, std::any api_conf); + + //! Construct a midi_in object with the default MIDI 2 back-end for the platform + explicit midi_in(ump_input_configuration conf) noexcept; + + //! Construct a midi_in object with a configuration object for a specific MIDI 2 back-end + //! see configuration.hpp for the available configuration types. + //! An exception will be thrown if the requested back-end cannot be opened. + explicit midi_in(ump_input_configuration conf, std::any api_conf); + + midi_in(const midi_in&) = delete; + midi_in(midi_in&& other) noexcept; + midi_in& operator=(const midi_in&) = delete; + midi_in& operator=(midi_in&& other) noexcept; + ~midi_in(); + + //! Returns the MIDI API specifier for the current instance of midi_in. + [[nodiscard]] libremidi::API get_current_api() const noexcept; + + //! Open a MIDI input connection + void open_port(const input_port& pt, std::string_view local_port_name = "libremidi input"); + + //! Create a virtual input port, with optional name, to allow software + //! connections. + //! + //! \param portName An optional name for the application port that is + //! used to connect to portId can be specified. + void open_virtual_port(std::string_view portName = "libremidi virtual port"); + + void set_port_name(std::string_view portName); + + //! Close an open MIDI connection (if one exists). + void close_port(); + + //! Returns true if a port has been opened successfully with open_port or open_virtual_port + [[nodiscard]] bool is_port_open() const noexcept; + + //! Returns true if a port is connected to another port. + //! Never true for virtual ports. + [[nodiscard]] bool is_port_connected() const noexcept; + + //! Returns the current timestamp for absolute ticks. + timestamp absolute_timestamp() const noexcept; + +private: + std::unique_ptr impl_; +}; + +//! Main class for sending MIDI 1.0 and 2.0 messages. +class LIBREMIDI_EXPORT midi_out +{ +public: + //! Construct a midi_out object with the default back-end for the platform + explicit midi_out(const output_configuration& conf = {}) noexcept; + + //! Construct a midi_out object with a configuration object for a specific back-end + //! see configuration.hpp for the available configuration types. + //! An exception will be thrown if the requested back-end cannot be opened. + explicit midi_out(output_configuration conf, std::any api_conf); + + midi_out(const midi_out&) = delete; + midi_out(midi_out&& other) noexcept; + midi_out& operator=(const midi_out&) = delete; + midi_out& operator=(midi_out&& other) noexcept; + ~midi_out(); + + //! Returns the MIDI API specifier for the current instance of midi_out. + [[nodiscard]] libremidi::API get_current_api() const noexcept; + + //! Open a MIDI output connection. + void + open_port(const output_port& pt, std::string_view local_port_name = "libremidi output") const; + + //! Close an open MIDI connection (if one exists). + void close_port() const; + + //! Returns true if a port has been opened successfully with open_port or open_virtual_port + [[nodiscard]] bool is_port_open() const noexcept; + + //! Returns true if a port is connected to another port. + //! Never true for virtual ports. + [[nodiscard]] bool is_port_connected() const noexcept; + + //! Create a virtual output port, with optional name, to allow software + //! connections. + //! + //! \param portName An optional name for the application port that is + //! used to connect to portId can be specified. + void open_virtual_port(std::string_view portName = "libremidi virtual port") const; + + void set_port_name(std::string_view portName) const; + + //! Immediately send a single message out an open MIDI output port. + /*! + An exception is thrown if an error occurs during output or an + output connection was not previously established. + */ + void send_message(const libremidi::message& message) const; + + //! Immediately send a single message to an open MIDI output port. + void send_message(const unsigned char* message, size_t size) const; + void send_message(std::span) const; + void send_message(unsigned char b0) const; + void send_message(unsigned char b0, unsigned char b1) const; + void send_message(unsigned char b0, unsigned char b1, unsigned char b2) const; + + //! Current time in the timestamp referential + int64_t current_time(); + + //! Try to schedule a message later in time if the underlying API supports it + //! (currently not implemented anywhere) + void schedule_message(int64_t timestamp, const unsigned char* message, size_t size); + + //! Immediately send a single UMP packet to an open MIDI output port. + void send_ump(const uint32_t* message, size_t size) const; + void send_ump(const libremidi::ump&) const; + void send_ump(std::span) const; + void send_ump(uint32_t b0) const; + void send_ump(uint32_t b0, uint32_t b1) const; + void send_ump(uint32_t b0, uint32_t b1, uint32_t b2) const; + void send_ump(uint32_t b0, uint32_t b1, uint32_t b2, uint32_t b3) const; + +// Interop with ni-midi2 +#if LIBREMIDI_NI_MIDI2_COMPAT + void send_ump(const midi::universal_packet& pkt) const { send_ump(pkt.data, pkt.size()); } + void send_ump(const midi::sysex7& msg, int group = 0) + { + midi::send_sysex7(msg, group, [&](const midi::sysex7_packet& x) { send_ump(x.data); }); + } + void send_ump(const midi::sysex8& msg, int stream, int group = 0) + { + midi::send_sysex8(msg, stream, group, [&](const midi::sysex8_packet& x) { send_ump(x.data); }); + } +#endif + + //! Try to schedule an UMP packet later in time if the underlying API supports it + //! (currently not implemented anywhere) + void schedule_ump(int64_t timestamp, const uint32_t* message, size_t size); + +private: + std::unique_ptr impl_; +}; +} + +#if defined(LIBREMIDI_HEADER_ONLY) + #include + #include + #include + #include + + #if defined(__EMSCRIPTEN__) + #include + #include + #include + #include + #endif +#endif diff --git a/include/libremidi/message.hpp b/include/libremidi/message.hpp new file mode 100644 index 0000000..541e5d0 --- /dev/null +++ b/include/libremidi/message.hpp @@ -0,0 +1,287 @@ +#pragma once +#include + +#include + +namespace libremidi +{ +enum class message_type : uint8_t +{ + INVALID = 0x0, + // Standard Message + NOTE_OFF = 0x80, + NOTE_ON = 0x90, + POLY_PRESSURE = 0xA0, + CONTROL_CHANGE = 0xB0, + PROGRAM_CHANGE = 0xC0, + AFTERTOUCH = 0xD0, + PITCH_BEND = 0xE0, + + // System Common Messages + SYSTEM_EXCLUSIVE = 0xF0, + TIME_CODE = 0xF1, + SONG_POS_POINTER = 0xF2, + SONG_SELECT = 0xF3, + RESERVED1 = 0xF4, + RESERVED2 = 0xF5, + TUNE_REQUEST = 0xF6, + EOX = 0xF7, + + // System Realtime Messages + TIME_CLOCK = 0xF8, + RESERVED3 = 0xF9, + START = 0xFA, + CONTINUE = 0xFB, + STOP = 0xFC, + RESERVED4 = 0xFD, + ACTIVE_SENSING = 0xFE, + SYSTEM_RESET = 0xFF +}; + +enum class meta_event_type : uint8_t +{ + SEQUENCE_NUMBER = 0x00, + TEXT = 0x01, + COPYRIGHT = 0x02, + TRACK_NAME = 0x03, + INSTRUMENT = 0x04, + LYRIC = 0x05, + MARKER = 0x06, + CUE = 0x07, + PATCH_NAME = 0x08, + DEVICE_NAME = 0x09, + CHANNEL_PREFIX = 0x20, + MIDI_PORT = 0x21, + END_OF_TRACK = 0x2F, + TEMPO_CHANGE = 0x51, + SMPTE_OFFSET = 0x54, + TIME_SIGNATURE = 0x58, + KEY_SIGNATURE = 0x59, + PROPRIETARY = 0x7F, + UNKNOWN = 0xFF +}; + +struct message +{ + midi_bytes bytes; + int64_t timestamp{}; + + message() noexcept = default; + operator std::span() const noexcept { return {bytes.data(), bytes.size()}; } + + message(const midi_bytes& src_bytes, int64_t src_timestamp) noexcept + : bytes(src_bytes) + , timestamp(src_timestamp) + { + } + message(std::initializer_list args) noexcept + : bytes{args} + { + } + + template + auto assign(Args&&... args) + { + return bytes.assign(std::forward(args)...); + } + template + auto insert(Args&&... args) + { + return bytes.insert(std::forward(args)...); + } + + auto size() const noexcept { return bytes.size(); } + auto empty() const noexcept { return bytes.empty(); } + + auto clear() noexcept { bytes.clear(); } + + auto& operator[](midi_bytes::size_type i) const noexcept { return bytes[i]; } + auto& operator[](midi_bytes::size_type i) noexcept { return bytes[i]; } + + auto& front() const { return bytes.front(); } + auto& back() const { return bytes.back(); } + auto& front() { return bytes.front(); } + auto& back() { return bytes.back(); } + + auto begin() const noexcept { return bytes.begin(); } + auto end() const noexcept { return bytes.end(); } + auto begin() noexcept { return bytes.begin(); } + auto end() noexcept { return bytes.end(); } + auto cbegin() const noexcept { return bytes.cbegin(); } + auto cend() const noexcept { return bytes.cend(); } + auto cbegin() noexcept { return bytes.cbegin(); } + auto cend() noexcept { return bytes.cend(); } + auto rbegin() const noexcept { return bytes.rbegin(); } + auto rend() const noexcept { return bytes.rend(); } + auto rbegin() noexcept { return bytes.rbegin(); } + auto rend() noexcept { return bytes.rend(); } + + bool uses_channel(int channel) const + { +#if defined(__cpp_exceptions) + if (channel <= 0 || channel > 16) + throw std::range_error("message::uses_channel: out of range"); +#endif + return ((bytes[0] & 0xF) == channel - 1) && ((bytes[0] & 0xF0) != 0xF0); + } + + int get_channel() const noexcept + { + if ((bytes[0] & 0xF0) != 0xF0) + return (bytes[0] & 0xF) + 1; + return 0; + } + + bool is_meta_event() const noexcept { return bytes[0] == 0xFF; } + + meta_event_type get_meta_event_type() const noexcept + { + if (!is_meta_event()) + return meta_event_type::UNKNOWN; + return static_cast(bytes[1]); + } + + message_type get_message_type() const noexcept + { + if (bytes[0] >= static_cast(message_type::SYSTEM_EXCLUSIVE)) + { + return static_cast(bytes[0] & 0xFF); + } + else + { + return static_cast(bytes[0] & 0xF0); + } + } + + bool is_note_on_or_off() const noexcept + { + const auto status = get_message_type(); + return (status == message_type::NOTE_ON) || (status == message_type::NOTE_OFF); + } +}; + +struct channel_events +{ + static constexpr uint8_t clamp_channel(int channel) noexcept + { + channel--; + if (channel < 0) + channel = 0; + else if (channel > 15) + channel = 15; + return static_cast(channel); + } + + static uint8_t make_command(const message_type type, const int channel) noexcept + { + return static_cast(static_cast(type) | clamp_channel(channel)); + } + + static message note_on(uint8_t channel, uint8_t note, uint8_t velocity) noexcept + { + return {make_command(message_type::NOTE_ON, channel), note, velocity}; + } + + static message note_off(uint8_t channel, uint8_t note, uint8_t velocity) noexcept + { + return {make_command(message_type::NOTE_OFF, channel), note, velocity}; + } + + static message control_change(uint8_t channel, uint8_t control, uint8_t value) noexcept + { + return {make_command(message_type::CONTROL_CHANGE, channel), control, value}; + } + + static message program_change(uint8_t channel, uint8_t value) noexcept + { + return {make_command(message_type::PROGRAM_CHANGE, channel), value}; + } + + static message pitch_bend(uint8_t channel, int value) noexcept + { + return { + make_command(message_type::PITCH_BEND, channel), static_cast(value & 0x7F), + static_cast((value >> 7) & 0x7F)}; + } + + static message pitch_bend(uint8_t channel, uint8_t lsb, uint8_t msb) noexcept + { + return {make_command(message_type::PITCH_BEND, channel), lsb, msb}; + } + + static message poly_pressure(uint8_t channel, uint8_t note, uint8_t value) noexcept + { + return {make_command(message_type::POLY_PRESSURE, channel), note, value}; + } + + static message aftertouch(uint8_t channel, uint8_t value) noexcept + { + return {make_command(message_type::AFTERTOUCH, channel), value}; + } +}; + +struct meta_events +{ + static message end_of_track() noexcept { return {0xFF, 0x2F, 0}; } + + static message channel(int channel) noexcept + { + return {0xff, 0x20, 0x01, static_cast(std::clamp(0, 0xff, channel - 1))}; + } + + static message tempo(int mpqn) noexcept + { + return { + 0xff, + 81, + 3, + static_cast(mpqn >> 16), + static_cast(mpqn >> 8), + static_cast(mpqn)}; + } + + static message time_signature(int numerator, int denominator) + { + int n = 1; + int powTwo = 0; + + while (n < denominator) + { + n <<= 1; + ++powTwo; + } + + return {0xff, 0x58, 0x04, static_cast(numerator), static_cast(powTwo), + 1, 96}; + } + + // Where key index goes from -7 (7 flats, Câ™­ Major) to +7 (7 sharps, C♯ + // Major) + static message key_signature(int keyIndex, bool isMinor) + { +#if defined(__cpp_exceptions) + if (keyIndex < -7 || keyIndex > 7) + throw std::range_error("meta_events::key_signature: out of range"); +#endif + return { + 0xff, 0x59, 0x02, static_cast(keyIndex), + isMinor ? static_cast(1) : static_cast(0)}; + } + + static message song_position(int positionInBeats) noexcept + { + return { + 0xf2, static_cast(positionInBeats & 127), + static_cast((positionInBeats >> 7) & 127)}; + } +}; + +struct track_event +{ + int tick = 0; + int track = 0; + message m; +}; + +typedef std::vector midi_track; +} diff --git a/include/libremidi/midi_in.cpp b/include/libremidi/midi_in.cpp new file mode 100644 index 0000000..d529ac6 --- /dev/null +++ b/include/libremidi/midi_in.cpp @@ -0,0 +1,164 @@ +#if !defined(LIBREMIDI_HEADER_ONLY) + #include +#endif + +#include +#include + +#include + +namespace libremidi +{ +LIBREMIDI_INLINE auto make_midi_in(auto base_conf, std::any api_conf, auto backends) +{ + std::unique_ptr ptr; + + assert(base_conf.on_message); + + auto from_api = [&](T& /*backend*/) mutable { + if (auto conf = std::any_cast(&api_conf)) + { + ptr = libremidi::make(std::move(base_conf), std::move(*conf)); + return true; + } + return false; + }; + std::apply([&](auto&&... b) { (from_api(b) || ...); }, backends); + return ptr; +} + +LIBREMIDI_INLINE midi_in::midi_in(const input_configuration& base_conf) noexcept +{ + for (const auto& api : available_apis()) + { + try + { + impl_ = make_midi_in(base_conf, midi_in_configuration_for(api), midi1::available_backends); + } + catch (const std::exception& e) + { + } + + if (impl_) + return; + } + if (!impl_) + impl_ = std::make_unique(input_configuration{}, dummy_configuration{}); +} + +LIBREMIDI_INLINE +midi_in::midi_in(input_configuration base_conf, std::any api_conf) + : impl_{make_midi_in(base_conf, api_conf, midi1::available_backends)} +{ + if (!impl_) + throw midi_exception("Could not open midi in for the given api"); +} + +LIBREMIDI_INLINE midi_in::midi_in(ump_input_configuration base_conf) noexcept +{ + for (const auto& api : available_apis()) + { + try + { + impl_ = make_midi_in(base_conf, midi_in_configuration_for(api), midi2::available_backends); + } + catch (const std::exception& e) + { + } + + if (impl_) + return; + } + if (!impl_) + impl_ = std::make_unique(input_configuration{}, dummy_configuration{}); +} + +LIBREMIDI_INLINE +midi_in::midi_in(ump_input_configuration base_conf, std::any api_conf) + : impl_{make_midi_in(base_conf, api_conf, midi2::available_backends)} +{ + if (!impl_) + throw midi_exception("Could not open midi in for the given api"); +} + +LIBREMIDI_INLINE midi_in::~midi_in() = default; + +LIBREMIDI_INLINE midi_in::midi_in(midi_in&& other) noexcept + : impl_{std::move(other.impl_)} +{ + other.impl_ + = std::make_unique(input_configuration{}, dummy_configuration{}); +} + +LIBREMIDI_INLINE +void midi_in::set_port_name(std::string_view portName) +{ + impl_->set_port_name(portName); +} + +LIBREMIDI_INLINE midi_in& midi_in::operator=(midi_in&& other) noexcept +{ + this->impl_ = std::move(other.impl_); + other.impl_ + = std::make_unique(input_configuration{}, dummy_configuration{}); + return *this; +} + +LIBREMIDI_INLINE +libremidi::API midi_in::get_current_api() const noexcept +{ + return impl_->get_current_api(); +} + +LIBREMIDI_INLINE +void midi_in::open_port(const input_port& port, std::string_view portName) +{ + if (impl_->is_port_open()) + return; + + if (impl_->open_port(port, portName)) + { + impl_->connected_ = true; + impl_->port_open_ = true; + } +} + +LIBREMIDI_INLINE +void midi_in::open_virtual_port(std::string_view portName) +{ + if (impl_->is_port_open()) + return; + + if (impl_->open_virtual_port(portName)) + { + impl_->port_open_ = true; + } +} + +LIBREMIDI_INLINE +void midi_in::close_port() +{ + impl_->close_port(); + + impl_->connected_ = false; + impl_->port_open_ = false; +} + +LIBREMIDI_INLINE +bool midi_in::is_port_open() const noexcept +{ + return impl_->is_port_open(); +} + +LIBREMIDI_INLINE +bool midi_in::is_port_connected() const noexcept +{ + return impl_->is_port_connected(); +} + +LIBREMIDI_INLINE +int64_t midi_in::absolute_timestamp() const noexcept +{ + return impl_->absolute_timestamp(); +} +} diff --git a/include/libremidi/midi_out.cpp b/include/libremidi/midi_out.cpp new file mode 100644 index 0000000..66e6b7b --- /dev/null +++ b/include/libremidi/midi_out.cpp @@ -0,0 +1,215 @@ +#if !defined(LIBREMIDI_HEADER_ONLY) + #include +#endif + +#include +#include + +#include +#include + +namespace libremidi +{ +LIBREMIDI_INLINE auto make_midi_out(auto base_conf, std::any api_conf) +{ + std::unique_ptr ptr; + auto from_api = [&](T& /*backend*/) mutable { + if (auto conf = std::any_cast(&api_conf)) + { + ptr = libremidi::make(std::move(base_conf), std::move(*conf)); + return true; + } + return false; + }; + std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi1::available_backends); + std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi2::available_backends); + return ptr; +} + +LIBREMIDI_INLINE midi_out::midi_out(const output_configuration& base_conf) noexcept +{ + for (const auto& api : available_apis()) + { + try + { + impl_ = make_midi_out(base_conf, midi_out_configuration_for(api)); + } + catch (const std::exception& e) + { + } + + if (impl_) + return; + } + if (!impl_) + impl_ = std::make_unique(output_configuration{}, dummy_configuration{}); +} + +LIBREMIDI_INLINE +midi_out::midi_out(output_configuration base_conf, std::any api_conf) + : impl_{make_midi_out(base_conf, api_conf)} +{ + if (!impl_) + throw midi_exception("Could not open midi out for the given api"); +} + +LIBREMIDI_INLINE midi_out::~midi_out() = default; + +LIBREMIDI_INLINE midi_out::midi_out(midi_out&& other) noexcept + : impl_{std::move(other.impl_)} +{ + other.impl_ + = std::make_unique(output_configuration{}, dummy_configuration{}); +} +LIBREMIDI_INLINE midi_out& midi_out::operator=(midi_out&& other) noexcept +{ + this->impl_ = std::move(other.impl_); + other.impl_ + = std::make_unique(output_configuration{}, dummy_configuration{}); + return *this; +} + +LIBREMIDI_INLINE +void midi_out::set_port_name(std::string_view portName) const +{ + impl_->set_port_name(portName); +} + +LIBREMIDI_INLINE +libremidi::API midi_out::get_current_api() const noexcept +{ + return impl_->get_current_api(); +} + +LIBREMIDI_INLINE +void midi_out::open_port(const output_port& port, std::string_view portName) const +{ + if (impl_->is_port_open()) + return; + + if (impl_->open_port(port, portName)) + { + impl_->connected_ = true; + impl_->port_open_ = true; + } +} + +LIBREMIDI_INLINE +void midi_out::open_virtual_port(std::string_view portName) const +{ + if (impl_->is_port_open()) + return; + + if (impl_->open_virtual_port(portName)) + { + impl_->port_open_ = true; + } +} + +LIBREMIDI_INLINE +void midi_out::close_port() const +{ + impl_->close_port(); + impl_->connected_ = false; + impl_->port_open_ = false; +} + +LIBREMIDI_INLINE +bool midi_out::is_port_open() const noexcept +{ + return impl_->is_port_open(); +} + +LIBREMIDI_INLINE +bool midi_out::is_port_connected() const noexcept +{ + return impl_->is_port_connected(); +} + +LIBREMIDI_INLINE +void midi_out::send_message(const libremidi::message& message) const +{ + send_message(message.bytes.data(), message.bytes.size()); +} + +LIBREMIDI_INLINE +void midi_out::send_message(std::span message) const +{ + send_message(message.data(), message.size()); +} + +LIBREMIDI_INLINE +void midi_out::send_message(unsigned char b0) const +{ + send_message(&b0, 1); +} + +LIBREMIDI_INLINE +void midi_out::send_message(unsigned char b0, unsigned char b1) const +{ + send_message(std::to_array({b0, b1})); +} + +LIBREMIDI_INLINE +void midi_out::send_message(unsigned char b0, unsigned char b1, unsigned char b2) const +{ + send_message(std::to_array({b0, b1, b2})); +} + +LIBREMIDI_INLINE +void midi_out::send_message(const unsigned char* message, size_t size) const +{ +#if defined(LIBREMIDI_ASSERTIONS) + assert(size > 0); +#endif + + impl_->send_message(message, size); +} + +LIBREMIDI_INLINE +void midi_out::send_ump(const uint32_t* message, size_t size) const +{ +#if defined(LIBREMIDI_ASSERTIONS) + assert(size > 0); + assert(size <= 4); +#endif + + impl_->send_ump(message, size); +} +LIBREMIDI_INLINE +void midi_out::send_ump(const libremidi::ump& message) const +{ + send_ump(message.data, message.size()); +} + +LIBREMIDI_INLINE +void midi_out::send_ump(std::span message) const +{ + send_ump(message.data(), message.size()); +} + +LIBREMIDI_INLINE +void midi_out::send_ump(uint32_t b0) const +{ + send_ump(&b0, 1); +} + +LIBREMIDI_INLINE +void midi_out::send_ump(uint32_t b0, uint32_t b1) const +{ + send_ump(std::to_array({b0, b1})); +} + +LIBREMIDI_INLINE +void midi_out::send_ump(uint32_t b0, uint32_t b1, uint32_t b2) const +{ + send_ump(std::to_array({b0, b1, b2})); +} + +LIBREMIDI_INLINE +void midi_out::send_ump(uint32_t b0, uint32_t b1, uint32_t b2, uint32_t b3) const +{ + send_ump(std::to_array({b0, b1, b2, b3})); +} + +} diff --git a/include/libremidi/observer.cpp b/include/libremidi/observer.cpp new file mode 100644 index 0000000..178361e --- /dev/null +++ b/include/libremidi/observer.cpp @@ -0,0 +1,87 @@ +#if !defined(LIBREMIDI_HEADER_ONLY) + #include +#endif + +#include +#include + +namespace libremidi +{ +LIBREMIDI_INLINE auto make_observer(auto base_conf, std::any api_conf) +{ + std::unique_ptr ptr; + auto from_api = [&](T& /*backend*/) mutable { + if (auto conf = std::any_cast(&api_conf)) + { + ptr = libremidi::make(std::move(base_conf), std::move(*conf)); + return true; + } + return false; + }; + std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi1::available_backends); + std::apply([&](auto&&... b) { (from_api(b) || ...); }, midi2::available_backends); + return ptr; +} + +LIBREMIDI_INLINE observer::observer(const observer_configuration& base_conf) noexcept +{ + for (const auto& api : available_apis()) + { + try + { + impl_ = make_observer(base_conf, observer_configuration_for(api)); + } + catch (const std::exception& e) + { + } + + if (impl_) + return; + } + if (!impl_) + impl_ = std::make_unique(observer_configuration{}, dummy_configuration{}); +} + +LIBREMIDI_INLINE observer::observer(observer_configuration base_conf, std::any api_conf) + : impl_{make_observer(base_conf, api_conf)} +{ + if (!impl_) + throw midi_exception("Could not open observer for the given API"); +} + +LIBREMIDI_INLINE observer::observer(observer&& other) noexcept + : impl_{std::move(other.impl_)} +{ + other.impl_ = std::make_unique( + observer_configuration{}, dummy_configuration{}); +} + +LIBREMIDI_INLINE observer& observer::operator=(observer&& other) noexcept +{ + this->impl_ = std::move(other.impl_); + other.impl_ = std::make_unique( + observer_configuration{}, dummy_configuration{}); + return *this; +} + +LIBREMIDI_INLINE +observer::~observer() = default; + +LIBREMIDI_INLINE +libremidi::API observer::get_current_api() const noexcept +{ + return impl_->get_current_api(); +} + +LIBREMIDI_INLINE +std::vector observer::get_input_ports() const noexcept +{ + return impl_->get_input_ports(); +} + +LIBREMIDI_INLINE +std::vector observer::get_output_ports() const noexcept +{ + return impl_->get_output_ports(); +} +} diff --git a/include/libremidi/observer_configuration.hpp b/include/libremidi/observer_configuration.hpp new file mode 100644 index 0000000..5f59c72 --- /dev/null +++ b/include/libremidi/observer_configuration.hpp @@ -0,0 +1,86 @@ +#pragma once +#include +#include + +#include +#include + +namespace libremidi +{ +using client_handle = std::uint64_t; +using port_handle = std::uint64_t; + +struct LIBREMIDI_EXPORT port_information +{ + // Handle to the client object: + + // ALSA Raw: unused + // ALSA Seq: snd_seq_t* + // CoreMIDI: MidiClientRef + // WebMIDI: unused + // JACK: jack_client_t* + // PipeWire: unused + // WinMM: unused + // WinUWP: unused + client_handle client; + + // ALSA Raw: { uint16_t card, device, sub, padding; } + // ALSA Seq: { uint32_t client, uint32_t port; } + // CoreMIDI: MidiObjectRef + // WebMIDI: unused + // JACK: jack_port_id_t + // PipeWire: port.id + // WinMM: unset, identified by port_name + // WinUWP: unused + port_handle port; + + std::string manufacturer; + std::string device_name; + std::string port_name; + std::string display_name; + + bool operator==(const port_information& other) const noexcept = default; + std::strong_ordering operator<=>(const port_information& other) const noexcept = default; +}; + +struct input_port : port_information +{ + bool operator==(const input_port& other) const noexcept = default; + std::strong_ordering operator<=>(const input_port& other) const noexcept = default; +}; +struct output_port : port_information +{ + bool operator==(const output_port& other) const noexcept = default; + std::strong_ordering operator<=>(const output_port& other) const noexcept = default; +}; + +using input_port_callback = std::function; +using output_port_callback = std::function; +struct observer_configuration +{ + midi_error_callback on_error{}; + midi_error_callback on_warning{}; + + input_port_callback input_added; + input_port_callback input_removed; + output_port_callback output_added; + output_port_callback output_removed; + + // Observe hardware ports + uint32_t track_hardware : 1 = true; + + // Observe software (virtual) ports if the API provides it + uint32_t track_virtual : 1 = false; + + // Observe any port - some systems have other weird port types than hw / sw, this covers them + uint32_t track_any : 1 = false; + + // Notify of the existing ports in the observer constructor + uint32_t notify_in_constructor : 1 = true; + + bool has_callbacks() const noexcept + { + return input_added || input_removed || output_added || output_removed; + } +}; +} diff --git a/include/libremidi/output_configuration.hpp b/include/libremidi/output_configuration.hpp new file mode 100644 index 0000000..3588289 --- /dev/null +++ b/include/libremidi/output_configuration.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include + +#include + +namespace libremidi +{ +struct output_configuration +{ + //! Set an 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{}; + + //! Timestamp mode for the timestamps passed to schedule_message + uint32_t timestamps : 3 = timestamp_mode::Absolute; +}; +} diff --git a/include/libremidi/reader.cpp b/include/libremidi/reader.cpp new file mode 100644 index 0000000..e4272bc --- /dev/null +++ b/include/libremidi/reader.cpp @@ -0,0 +1,752 @@ +/* +Copyright (c) 2015, Dimitri Diakopoulos All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#if !defined(LIBREMIDI_HEADER_ONLY) + #include +#endif +#include + +#include +#include + +// File Parsing Validation Todo: +// ============================== +// [] Bad file name +// [] Bad header +// [] Unknown header type +// [] Bad header size +// [] Bad type +// [] Bad tmecode +// [] Header too short +// [] Track too short +// [] Event too short +// ============================== + +#if defined(__LIBREMIDI_DEBUG__) +std::ostream& operator<<(std::ostream& s, const libremidi::message& m) +{ + s << "[ MIDI: "; + for (auto b : m) + s << (unsigned int)b << ' '; + s << "]\n"; + return s; +} +#endif + +namespace libremidi +{ +namespace util +{ +struct no_validator +{ + static inline bool validate_track([[maybe_unused]] const midi_track& track) { return true; } +}; + +struct validator +{ + static inline bool validate_track(const midi_track& track) + { + if (track.empty()) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: empty track" << std::endl; +#endif + return false; + } + + // Ensure that there is a unique EOT at the end of the track + auto it = std::find_if(track.begin(), track.end(), [](const libremidi::track_event& msg) { + static const auto eot = meta_events::end_of_track(); + return msg.m.bytes == eot.bytes; + }); + + if (it == track.end()) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: track has no END OF TRACK" << std::endl; +#endif + return false; + } + + if (&it->m != &track.back().m) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << std::distance(it, track.end()); + std::cerr << "libremidi::reader: track does not end with END OF TRACK" << std::endl; +#endif + return false; + } + + return true; + } +}; + +// Used when we know that we have enough space +struct read_unchecked +{ + // Read a MIDI-style variable-length integer (big-endian value in groups of 7 bits, + // with top bit set to signify that another byte follows). + static inline void ensure_size( + [[maybe_unused]] const uint8_t* begin, [[maybe_unused]] const uint8_t* end, + [[maybe_unused]] int64_t needed) + { + } + + static inline uint32_t + read_variable_length(uint8_t const*& data, [[maybe_unused]] uint8_t const* end) + { + uint32_t result = 0; + while (true) + { + uint8_t b = *data++; + if (b & 0x80) + { + result += (b & 0x7F); + result <<= 7; + } + else + { + return result + b; // b is the last byte + } + } + } + + static inline void read_bytes( + midi_bytes& buffer, uint8_t const*& data, [[maybe_unused]] const uint8_t* end, + const std::size_t num) + { + buffer.reserve(buffer.size() + num); + for (std::size_t i = 0; i < num; ++i) + buffer.push_back(*data++); + } + + static inline void read_bytes( + midi_bytes& buffer, uint8_t const*& data, [[maybe_unused]] const uint8_t* end, const int num) + { + read_bytes(buffer, data, end, static_cast(num)); + } + + static inline uint16_t read_uint16_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end) + { + uint16_t result = *data++ << 8u; + result += *data++; + return result; + } + + static inline uint32_t read_uint24_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end) + { + uint32_t result = *data++ << 16u; + result += static_cast(*data++ << 8u); + result += *data++; + return result; + } + + static inline uint32_t read_uint32_be(uint8_t const*& data, [[maybe_unused]] const uint8_t* end) + { + uint32_t result = *data++ << 24u; + result += static_cast(*data++ << 16u); + result += static_cast(*data++ << 8u); + result += *data++; + return result; + } +}; + +// Used when we do not know if we have enough bytes and have to check before reading +struct read_checked +{ + // Read a MIDI-style variable-length integer (big-endian value in groups of 7 bits, + // with top bit set to signify that another byte follows). + static inline void + ensure_size(const uint8_t* begin, const uint8_t* end, const std::size_t needed) + { + if (const auto available = static_cast(end - begin); available < needed) + throw std::runtime_error("MIDI reader: not enough data to process"); + } + + static inline std::size_t read_variable_length(uint8_t const*& data, uint8_t const* end) + { + std::size_t result = 0; + while (true) + { + ensure_size(data, end, 1); + uint8_t b = *data++; + if (b & 0x80) + { + result += (b & 0x7F); + result <<= 7; + } + else + { + return result + b; // b is the last byte + } + } + } + + static inline void + read_bytes(midi_bytes& buffer, uint8_t const*& data, uint8_t const* end, const std::size_t num) + { + ensure_size(data, end, num); + read_unchecked::read_bytes(buffer, data, end, num); + } + + static inline uint16_t read_uint16_be(uint8_t const*& data, uint8_t const* end) + { + ensure_size(data, end, 2); + return read_unchecked::read_uint16_be(data, end); + } + + static inline uint32_t read_uint24_be(uint8_t const*& data, uint8_t const* end) + { + ensure_size(data, end, 3); + return read_unchecked::read_uint24_be(data, end); + } + + static inline uint32_t read_uint32_be(uint8_t const*& data, uint8_t const* end) + { + ensure_size(data, end, 4); + return read_unchecked::read_uint32_be(data, end); + } +}; +} + +#if defined(LIBREMIDI_UNCHECKED) +using byte_reader = util::read_unchecked; +#else +using byte_reader = util::read_checked; +#endif + +#if defined(LIBREMIDI_UNVALIDATED) +using validator = util::no_validator; +#else +using validator = util::validator; +#endif + +LIBREMIDI_INLINE +track_event parse_event( + int tick, int track, const uint8_t*& dataStart, const uint8_t* dataEnd, + message_type lastEventTypeByte) +{ + byte_reader::ensure_size(dataStart, dataEnd, 1); + auto type = static_cast(*dataStart++); + + track_event event{tick, track, message{}}; + + if ((static_cast(type) & 0xF0) == 0xF0) + { + // Meta event + if (static_cast(type) == 0xFF) + { + byte_reader::ensure_size(dataStart, dataEnd, 1); + auto subtype = static_cast(*dataStart++); + + event.m.bytes.reserve(3); + event.m.bytes.push_back(static_cast(type)); + event.m.bytes.push_back(static_cast(subtype)); + + uint32_t length = 0; + // Here we read the meta-event length manually, as this way we can also put it into + // event.m.bytes + while (true) + { + byte_reader::ensure_size(dataStart, dataEnd, 1); + uint8_t b = *dataStart++; + event.m.bytes.push_back(b); + if (b & 0x80) + { + const uint8_t byte = (b & 0x7F); + + length += byte; + length <<= 7; + } + else + { + length += b; // b is the last byte + break; + } + } + + switch (subtype) + { + case meta_event_type::SEQUENCE_NUMBER: { + switch (length) + { + case 0: + return event; + case 2: + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, 2); + return event; + default: + throw std::invalid_argument("Expected length for SEQUENCE_NUMBER event is 0 or 2"); + } + } + case meta_event_type::TEXT: + case meta_event_type::COPYRIGHT: + case meta_event_type::TRACK_NAME: + case meta_event_type::INSTRUMENT: + case meta_event_type::LYRIC: + case meta_event_type::MARKER: + case meta_event_type::CUE: + case meta_event_type::PATCH_NAME: + case meta_event_type::DEVICE_NAME: { + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + + case meta_event_type::END_OF_TRACK: { + if (length != 0) + throw std::invalid_argument("Expected length for END_OF_TRACK event is 0"); + return event; + } + case meta_event_type::TEMPO_CHANGE: { + if (length != 3) + throw std::invalid_argument("Expected length for TEMPO_CHANGE event is 3"); + // event.m.bytes[3] = read_uint24_be(dataStart); // @dimitri TOFIX + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + case meta_event_type::SMPTE_OFFSET: { + if (length != 5) + throw std::invalid_argument("Expected length for SMPTE_OFFSET event is 5"); + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + auto& b = event.m.bytes; + + uint8_t format = (b[3] & 0b01100000) >> 5; + uint8_t h = (b[3] & 0b00011111); + + if (format > 3) + throw std::invalid_argument("SMPTE_OFFSET has unknown format"); + + int max = 0; + switch (format) + { + case 0: // 24 + max = 24; + break; + case 1: // 25 + max = 25; + break; + case 2: // 29 + max = 29; + break; + case 3: // 30 + max = 30; + break; + default: + break; + } + + if (h >= 24 || b[4] >= 60 || b[5] >= 60 || b[6] >= max || b[7] >= 100) + throw std::invalid_argument("SMPTE_OFFSET is out-of-23:59:59:xx:99 bounds"); + return event; + } + case meta_event_type::TIME_SIGNATURE: { + if (length != 4) + throw std::invalid_argument("Expected length for TIME_SIGNATURE event is 4"); + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + case meta_event_type::KEY_SIGNATURE: { + if (length != 2) + throw std::invalid_argument("Expected length for KEY_SIGNATURE event is 2"); + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + auto k = static_cast(event.m[3]); + if (k < -7 || k > 7) + throw std::invalid_argument("Invalid KEY_SIGNATURE"); + if (event.m[4] > 1) + throw std::invalid_argument("Invalid KEY_SIGNATURE"); + return event; + } + case meta_event_type::PROPRIETARY: { + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + case meta_event_type::CHANNEL_PREFIX: { + if (length != 1) + throw std::invalid_argument("Expected length for CHANNEL_PREFIX event is 1"); + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + case meta_event_type::MIDI_PORT: { + if (length != 1) + throw std::invalid_argument("Expected length for MIDI_PORT event is 1"); + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + case meta_event_type::UNKNOWN: + default: { + // Unknown events? + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + } + } + + else if (type == message_type::SYSTEM_EXCLUSIVE) + { + const auto length = byte_reader::read_variable_length(dataStart, dataEnd); + event.m.bytes = {static_cast(type)}; + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + + else if (type == message_type::EOX) + { + const auto length = byte_reader::read_variable_length(dataStart, dataEnd); + byte_reader::read_bytes(event.m.bytes, dataStart, dataEnd, length); + return event; + } + else + { + throw std::runtime_error("Unrecognised MIDI event type byte"); + } + } + + // Channel events + else + { + event.m.bytes.clear(); + + // Running status... + if ((static_cast(type) & 0x80) == 0) + { + // Reuse lastEventTypeByte as the event type. + // eventTypeByte is actually the first parameter + event.m.bytes.push_back(static_cast(lastEventTypeByte)); + event.m.bytes.push_back(static_cast(type)); + type = lastEventTypeByte; + } + else + { + event.m.bytes.push_back(static_cast(type)); + + byte_reader::ensure_size(dataStart, dataEnd, 1); + event.m.bytes.push_back(static_cast(*dataStart++)); + lastEventTypeByte = type; + } + + static constexpr auto validate = [](midi_bytes& b) { + if (b[1] < 128 && b[2] < 128) + return true; + throw std::invalid_argument("MIDI message has arguments > 127"); + }; + + switch (static_cast(static_cast(type) & 0xF0)) + { + case message_type::NOTE_OFF: + case message_type::NOTE_ON: + case message_type::POLY_PRESSURE: + case message_type::CONTROL_CHANGE: + byte_reader::ensure_size(dataStart, dataEnd, 1); + event.m.bytes.push_back(*dataStart++); + validate(event.m.bytes); + return event; + case message_type::PROGRAM_CHANGE: + if (event.m.bytes[1] >= 128) + throw std::invalid_argument("MIDI PC has arguments > 127"); + return event; + case message_type::AFTERTOUCH: + if (event.m.bytes[1] >= 128) + throw std::invalid_argument("MIDI Atertouch has arguments > 127"); + return event; + case message_type::PITCH_BEND: + byte_reader::ensure_size(dataStart, dataEnd, 1); + event.m.bytes.push_back(*dataStart++); + validate(event.m.bytes); + return event; + + case message_type::TIME_CODE: + throw std::runtime_error("Unsupported MIDI event type TIME_CODE"); + case message_type::SONG_POS_POINTER: + throw std::runtime_error("Unsupported MIDI event type SONG_POS_POINTER"); + case message_type::SONG_SELECT: + throw std::runtime_error("Unsupported MIDI event type SONG_SELECT"); + case message_type::RESERVED1: + throw std::runtime_error("Unsupported MIDI event type RESERVED1"); + case message_type::RESERVED2: + throw std::runtime_error("Unsupported MIDI event type RESERVED2"); + case message_type::TUNE_REQUEST: + throw std::runtime_error("Unsupported MIDI event type TUNE_REQUEST"); + case message_type::EOX: + throw std::runtime_error("Unsupported MIDI event type EOX"); + // System Realtime Messages : + case message_type::TIME_CLOCK: + throw std::runtime_error("Unsupported MIDI event type TIME_CLOCK"); + case message_type::RESERVED3: + throw std::runtime_error("Unsupported MIDI event type RESERVED3"); + case message_type::START: + throw std::runtime_error("Unsupported MIDI event type START"); + case message_type::CONTINUE: + throw std::runtime_error("Unsupported MIDI event type CONTINUE"); + case message_type::STOP: + throw std::runtime_error("Unsupported MIDI event type STOP"); + case message_type::RESERVED4: + throw std::runtime_error("Unsupported MIDI event type RESERVED4"); + case message_type::ACTIVE_SENSING: + throw std::runtime_error("Unsupported MIDI event type ACTIVE_SENSING"); + case message_type::SYSTEM_RESET: + throw std::runtime_error("Unsupported MIDI event type SYSTEM_RESET"); + case message_type::INVALID: + throw std::runtime_error("Unsupported MIDI event type INVALID"); + case message_type::SYSTEM_EXCLUSIVE: + throw std::runtime_error("Unsupported MIDI event type SYSTEM_EXCLUSIVE"); + default: + throw std::runtime_error("Unsupported MIDI event type"); + } + } +} + +LIBREMIDI_INLINE +reader::reader(bool useAbsolute) + : ticksPerBeat(480) + , startingTempo(120) + , useAbsoluteTicks(useAbsolute) +{ +} + +LIBREMIDI_INLINE +reader::~reader() { } + +constexpr int str_to_headerid(const char* str) +{ + return str[0] << 24 | str[1] << 16 | str[2] << 8 | str[3]; +} + +LIBREMIDI_INLINE +auto reader::parse(const uint8_t* dataPtr, std::size_t size) noexcept -> parse_result +try +{ + using namespace libremidi::util; + + tracks.clear(); + + if (size == 0) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: empty buffer passed to parse." << std::endl; +#endif + return parse_result::invalid; + } + + const uint8_t* const dataEnd = dataPtr + size; + + uint32_t headerId = read_checked::read_uint32_be(dataPtr, dataEnd); + uint32_t headerLength = read_checked::read_uint32_be(dataPtr, dataEnd); + + if (static_cast(headerId) != str_to_headerid("MThd") || headerLength != 6) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: couldn't parse header" << std::endl; +#endif + return parse_result::invalid; + } + + format = read_checked::read_uint16_be( + dataPtr, dataEnd); //@tofix format type -> save for later eventually + if (format > 2) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: unknown format" << std::endl; +#endif + return parse_result::invalid; + } + int trackCount = read_checked::read_uint16_be(dataPtr, dataEnd); + uint16_t timeDivision = read_checked::read_uint16_be(dataPtr, dataEnd); + + // CBB: deal with the SMPTE style time coding + // timeDivision is described here http://www.sonicspot.com/guide/midifiles.html + if (timeDivision & 0x8000) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: found SMPTE time frames (unsupported)" << std::endl; + int fps = (timeDivision >> 16) & 0x7f; + if (fps != -30 && fps != -29 && fps != -25 && fps != -24) + return parse_result::invalid; + int ticksPerFrame = timeDivision & 0xff; +#endif + // given beats per second, timeDivision should be derivable. + return parse_result::invalid; + } + + startingTempo = 120.0f; // midi default + ticksPerBeat = float(timeDivision); // ticks per beat (a beat is defined as a quarter note) + + parse_result result = parse_result::validated; + + for (int i = 0; i < trackCount; ++i) + { + midi_track track; + + headerId = read_checked::read_uint32_be(dataPtr, dataEnd); + headerLength = read_checked::read_uint32_be(dataPtr, dataEnd); + + if (headerId != str_to_headerid("MTrk")) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: couldn't find track header" << std::endl; +#endif + return parse_result::incomplete; + } + + int64_t available = dataEnd - dataPtr; + if (available < headerLength) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: not enough data available" << std::endl; +#endif + return parse_result::incomplete; + } + + track.reserve(headerLength / 3); + + const uint8_t* const trackEnd = dataPtr + headerLength; + + auto runningEvent = message_type::INVALID; + + std::size_t tickCount = 0; + + while (dataPtr < trackEnd) + { + const auto tick = read_checked::read_variable_length(dataPtr, trackEnd); + if (useAbsoluteTicks) + { + tickCount += tick; + } + else + { + tickCount = tick; + } + + try + { + track_event ev + = parse_event(static_cast(tickCount), i, dataPtr, trackEnd, runningEvent); + if (!ev.m.empty()) + { + if (!ev.m.is_meta_event()) + { + runningEvent = static_cast(ev.m.bytes[0]); + } + } + else + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: could not read event" << std::endl; +#endif + dataPtr = trackEnd; + result = parse_result::incomplete; + continue; + } + + track.push_back(std::move(ev)); + } + catch (const std::exception& e) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: " << e.what() << std::endl; +#endif + dataPtr = trackEnd; + result = parse_result::incomplete; + continue; + } + } + + if (result == parse_result::validated) + { + if (!validator::validate_track(track)) + { + result = parse_result::complete; + } + } + tracks.push_back(std::move(track)); + } + + if (result == parse_result::validated) + { + if (dataPtr != dataEnd) + { +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "midifile has junk at end: " << std::intptr_t(dataEnd - dataPtr) << std::endl; +#endif + result = parse_result::complete; + } + } + return result; +} +catch (const std::exception& e) +{ +#if defined(__LIBREMIDI_DEBUG__) + std::cerr << "libremidi::reader: " << e.what() << std::endl; +#endif + return parse_result::invalid; +} + +// In ticks +LIBREMIDI_INLINE +double reader::get_end_time() const noexcept +{ + if (useAbsoluteTicks) + { + double totalLength = 0.; + for (const auto& t : tracks) + { + if (!t.empty()) + { + const auto& last_event = t.back(); + if (last_event.tick > totalLength) + totalLength = last_event.tick; + } + } + return totalLength; + } + else + { + double totalLength = 0.; + for (const auto& t : tracks) + { + double trackLength = 0.; + for (const auto& e : t) + trackLength += e.tick; + + if (trackLength > totalLength) + totalLength = trackLength; + } + return totalLength; + } +} + +LIBREMIDI_INLINE +auto reader::parse(const std::vector& buffer) noexcept -> parse_result +{ + return parse(buffer.data(), buffer.size()); +} + +#if defined(LIBREMIDI_HAS_SPAN) +LIBREMIDI_INLINE +auto reader::parse(std::span buffer) noexcept -> parse_result +{ + return parse(buffer.data(), buffer.size()); +} +#endif +} diff --git a/include/libremidi/reader.hpp b/include/libremidi/reader.hpp new file mode 100644 index 0000000..1d99b46 --- /dev/null +++ b/include/libremidi/reader.hpp @@ -0,0 +1,75 @@ +/* +Copyright (c) 2015, Dimitri Diakopoulos All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include + +namespace libremidi +{ +/** + * @brief reads Standard MIDI files (SMF). + * + * Usage: + + * ``` + * libremidi::reader r; + * auto res = r.parse(midi_bytes, num_bytes); + * ``` + */ +class LIBREMIDI_EXPORT reader +{ +public: + enum parse_result + { + invalid, //! Nothing could be parsed + incomplete, //! Some of the data could be parsed, but not all: there may be missing events / + //! tracks + complete, //! All the data could be parsed but not necessarily validated + validated //! The data could be parsed and conforms to SMF rules + }; + explicit reader(bool useAbsolute = false); + ~reader(); + + parse_result parse(const uint8_t* data, std::size_t size) noexcept; + parse_result parse(const std::vector& buffer) noexcept; + parse_result parse(std::span buffer) noexcept; + + [[nodiscard]] double get_end_time() const noexcept; + + float ticksPerBeat{}; // precision (number of ticks distinguishable per second) + float startingTempo{}; + int format{}; + + std::vector tracks; + +private: + bool useAbsoluteTicks{}; +}; +} + +#if defined(LIBREMIDI_HEADER_ONLY) + #include +#endif diff --git a/include/libremidi/shared_context.hpp b/include/libremidi/shared_context.hpp new file mode 100644 index 0000000..f794aa2 --- /dev/null +++ b/include/libremidi/shared_context.hpp @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +#include + +namespace libremidi +{ + +class shared_context +{ +public: + shared_context() = default; + virtual ~shared_context() = default; + shared_context(const shared_context&) = delete; + shared_context(shared_context&&) = delete; + shared_context& operator=(const shared_context&) = delete; + shared_context& operator=(shared_context&&) = delete; + + virtual void start_processing() = 0; + virtual void stop_processing() = 0; +}; + +struct shared_configurations +{ + std::shared_ptr context; + std::any observer, in, out; +}; + +LIBREMIDI_EXPORT +shared_configurations create_shared_context(libremidi::API api, std::string_view client_name); + +} diff --git a/include/libremidi/ump.hpp b/include/libremidi/ump.hpp new file mode 100644 index 0000000..59d3091 --- /dev/null +++ b/include/libremidi/ump.hpp @@ -0,0 +1,113 @@ +#pragma once +#include + +#include + +#if LIBREMIDI_NI_MIDI2_COMPAT + #include +#endif + +namespace libremidi +{ +struct ump +{ + alignas(4) uint32_t data[4] = {}; + int64_t timestamp{}; + + constexpr ump() noexcept = default; + constexpr ~ump() = default; + + explicit constexpr ump(uint32_t b0) noexcept + : data{b0, 0, 0, 0} + { + } + constexpr ump(uint32_t b0, uint32_t b1) noexcept + : data{b0, b1, 0, 0} + { + } + constexpr ump(uint32_t b0, uint32_t b1, uint32_t b2) noexcept + : data{b0, b1, b2, 0} + { + } + constexpr ump(uint32_t b0, uint32_t b1, uint32_t b2, uint32_t b3) noexcept + : data{b0, b1, b2, b3} + { + } + + // Compatibility with ni-midi2: +#if LIBREMIDI_NI_MIDI2_COMPAT + constexpr operator midi::universal_packet() const noexcept + { + return {data[0], data[1], data[2], data[3]}; + } + explicit constexpr ump(midi::universal_packet b) noexcept + : data{b.data[0], b.data[1], b.data[2], b.data[3]} + { + } + constexpr ump& operator=(midi::universal_packet b) noexcept + { + data[0] = b.data[0]; + data[1] = b.data[1]; + data[2] = b.data[2]; + data[3] = b.data[3]; + return *this; + } +#endif + + // Compatibility with cmidi2: + operator uint32_t*() & noexcept { return data; } + operator const uint32_t*() const& noexcept { return data; } + operator uint32_t*() && noexcept = delete; + operator const uint32_t*() const&& noexcept = delete; + + constexpr std::size_t size() const noexcept + { + // Imported from cmidi2 + enum midi2_message_type + { + // MIDI 2.0 UMP Section 3. + UTILITY = 0, + SYSTEM = 1, + MIDI_1_CHANNEL = 2, + SYSEX7 = 3, + MIDI_2_CHANNEL = 4, + SYSEX8_MDS = 5, + }; + + switch (((data[0] & 0xF0000000) >> 28) & 0xF) + { + case UTILITY: + case SYSTEM: + case MIDI_1_CHANNEL: + return 1; + case MIDI_2_CHANNEL: + case SYSEX7: + return 2; + case SYSEX8_MDS: + return 4; + default: + return 0; + } + } + + constexpr void clear() noexcept + { + data[0] = 0; + data[1] = 0; + data[2] = 0; + data[3] = 0; + } + + constexpr auto& operator[](int i) const noexcept { return data[i]; } + constexpr auto& operator[](int i) noexcept { return data[i]; } + + constexpr auto begin() const noexcept { return data; } + constexpr auto end() const noexcept { return data + size(); } + constexpr auto begin() noexcept { return data; } + constexpr auto end() noexcept { return data + size(); } + constexpr auto cbegin() const noexcept { return data; } + constexpr auto cend() const noexcept { return data + size(); } + constexpr auto cbegin() noexcept { return data; } + constexpr auto cend() noexcept { return data + size(); } +}; +} diff --git a/include/libremidi/writer.cpp b/include/libremidi/writer.cpp new file mode 100644 index 0000000..7401b5e --- /dev/null +++ b/include/libremidi/writer.cpp @@ -0,0 +1,274 @@ +/* +Copyright (c) 2015, Dimitri Diakopoulos All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#if !defined(LIBREMIDI_HEADER_ONLY) + #include +#endif +#include +#include +#include +#include + +namespace libremidi +{ +namespace util +{ +static LIBREMIDI_INLINE std::ostream& write_uint16_be(std::ostream& out, uint16_t value) +{ + union + { + uint8_t bytes[2]; + uint16_t v; + } data; + data.v = value; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + +[[maybe_unused]] static LIBREMIDI_INLINE std::ostream& +write_int16_be(std::ostream& out, int16_t value) +{ + union + { + uint8_t bytes[2]; + int16_t v; + } data; + data.v = value; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + +static LIBREMIDI_INLINE std::ostream& write_uint32_be(std::ostream& out, uint32_t value) +{ + union + { + uint8_t bytes[4]; + uint32_t v; + } data; + data.v = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + +[[maybe_unused]] static LIBREMIDI_INLINE std::ostream& +write_int32_be(std::ostream& out, int32_t value) +{ + union + { + uint8_t bytes[4]; + int32_t v; + } data; + data.v = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + +[[maybe_unused]] static LIBREMIDI_INLINE std::ostream& +write_float_be(std::ostream& out, float value) +{ + union + { + uint8_t bytes[4]; + float v; + } data; + data.v = value; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + +[[maybe_unused]] static LIBREMIDI_INLINE std::ostream& +write_double_be(std::ostream& out, double value) +{ + union + { + uint8_t bytes[8]; + double v; + } data; + data.v = value; + out << data.bytes[7]; + out << data.bytes[6]; + out << data.bytes[5]; + out << data.bytes[4]; + out << data.bytes[3]; + out << data.bytes[2]; + out << data.bytes[1]; + out << data.bytes[0]; + return out; +} + +// Write a number to the midifile +// as a variable length value which segments a file into 7-bit +// values. Maximum size of aValue is 0x7fffffff +static LIBREMIDI_INLINE void write_variable_length(uint32_t aValue, std::vector& outdata) +{ + uint8_t bytes[5] = {0}; + + bytes[0] = static_cast((aValue >> 28) & 0x7F); // most significant 5 bits + bytes[1] = static_cast((aValue >> 21) & 0x7F); // next largest 7 bits + bytes[2] = static_cast((aValue >> 14) & 0x7F); + bytes[3] = static_cast((aValue >> 7) & 0x7F); + bytes[4] = static_cast((aValue)&0x7F); // least significant 7 bits + + int start = 0; + while (start < 5 && bytes[start] == 0) + start++; + + for (int i = start; i < 4; i++) + { + bytes[i] = bytes[i] | 0x80; + outdata.push_back(bytes[i]); + } + outdata.push_back(bytes[4]); +} + +static LIBREMIDI_INLINE void +add_event_track_count_check(std::vector& tracks, int track) +{ + if (track < 0) + throw std::out_of_range("Refusing to add an event to track " + std::to_string(track) + "."); + if (track > 65535) + throw std::out_of_range( + "Refusing to add an event to track " + std::to_string(track) + + " ; change add_event_track_count_check in libremidi writer.cpp to increase the limit."); + + while (tracks.size() < static_cast(track + 1)) // NOLINT(*-misplaced-widening-cast) + tracks.emplace_back(); +} +} + +LIBREMIDI_INLINE +void writer::add_event(const int tick, const int track, const message& m) +{ + util::add_event_track_count_check(tracks, track); + + tracks[static_cast(track)].push_back({tick, track, m}); +} + +LIBREMIDI_INLINE +void writer::add_event(int track, const track_event& m) +{ + util::add_event_track_count_check(tracks, track); + + tracks[static_cast(track)].push_back(m); +} + +LIBREMIDI_INLINE +void writer::add_track() +{ + util::add_event_track_count_check(tracks, static_cast(tracks.size() + 1)); +} + +LIBREMIDI_INLINE +void writer::write(std::ostream& out) const +{ + // MIDI File Header + out << 'M'; + out << 'T'; + out << 'h'; + out << 'd'; + util::write_uint32_be(out, 6); + util::write_uint16_be(out, (tracks.size() == 1) ? 0 : 1); + util::write_uint16_be(out, static_cast(tracks.size())); + util::write_uint16_be(out, ticksPerQuarterNote); + + std::vector trackRawData; + for (const auto& event_list : tracks) + { + trackRawData.clear(); + // Rough estimation of the memory to allocate + trackRawData.reserve(event_list.size() * 3); + + for (const auto& event : event_list) + { + const auto& msg = event.m; + if (msg.empty()) + continue; + + // Suppress end-of-track meta messages (one will be added + // automatically after all track data has been written). + if (msg.get_meta_event_type() == meta_event_type::END_OF_TRACK) + continue; + + util::write_variable_length(static_cast(event.tick), trackRawData); + + if ((msg.get_message_type() == message_type::SYSTEM_EXCLUSIVE) + || (event.m.get_message_type() == message_type::EOX)) + { + // 0xf0 == Complete sysex message (0xf0 is part of the raw MIDI). + // 0xf7 == Raw byte message (0xf7 not part of the raw MIDI). + // Print the first byte of the message (0xf0 or 0xf7), then + // print a VLV length for the rest of the bytes in the message. + // In other words, when creating a 0xf0 or 0xf7 MIDI message, + // do not insert the VLV byte length yourself, as this code will + // do it for you automatically. + trackRawData.emplace_back(msg.bytes[0]); // 0xf0 or 0xf7; + + util::write_variable_length(static_cast(msg.size()) - 1, trackRawData); + + trackRawData.insert( + trackRawData.end(), msg.bytes.data() + 1, msg.bytes.data() + msg.bytes.size()); + } + else + { + // Non-sysex type of message, so just output the bytes of the message: + trackRawData.insert( + trackRawData.end(), msg.bytes.data(), msg.bytes.data() + msg.bytes.size()); + } + } + + auto size = trackRawData.size(); + const auto eot = meta_events::end_of_track(); + + if ((size < 3) || !((trackRawData[size - 3] == 0xFF) && (trackRawData[size - 2] == 0x2F))) + { + trackRawData.emplace_back(0x0); // tick + trackRawData.emplace_back(eot[0]); + trackRawData.emplace_back(eot[1]); + trackRawData.emplace_back(eot[2]); + } + + // Write the track ID marker "MTrk": + out << 'M'; + out << 'T'; + out << 'r'; + out << 'k'; + util::write_uint32_be(out, static_cast(trackRawData.size())); + out.write( + reinterpret_cast(trackRawData.data()), + static_cast(trackRawData.size())); + } +} +} diff --git a/include/libremidi/writer.hpp b/include/libremidi/writer.hpp new file mode 100644 index 0000000..7ea90bd --- /dev/null +++ b/include/libremidi/writer.hpp @@ -0,0 +1,51 @@ +/* +Copyright (c) 2015, Dimitri Diakopoulos All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once +#include + +#include +#include + +namespace libremidi +{ +struct LIBREMIDI_EXPORT writer +{ +public: + int ticksPerQuarterNote{120}; + std::vector tracks; + + void add_event(int tick, int track, const message& m); + void add_event(int track, const track_event& m); + + void add_track(); + + void write(std::ostream& out) const; +}; +} + +#if defined(LIBREMIDI_HEADER_ONLY) + #include +#endif diff --git a/src/datatypes.h b/src/datatypes.h new file mode 100644 index 0000000..e5a5d15 --- /dev/null +++ b/src/datatypes.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +// 2D Vectors + +template +class Vec2D{ + public: + + Vec2D() {}; + Vec2D(T x, T y) : x(x), y(y) {} + T x = 0; + T y = 0; + T dist(Vec2D* v) { return sqrt(pow((v->x - x), 2) + pow((v->y - y), 2));}; + T length() { return sqrt(pow(x, 2) + pow(y, 2));}; + + Vec2D& norm() { return *this * (1.0/length());}; + + Vec2D& operator=(const Vec2D& vec){ + if (this == &vec){ + return *this; + } + x = vec.x; + y = vec.y; + return *this; + }; + + Vec2D& operator+=(const Vec2D& vec){ + this->x += vec.x; + this->y += vec.y; + return *this; + }; + + friend Vec2D operator+(const Vec2D& left, const Vec2D& right){ + return Vec2D(left.x + right.x, left.y + right.y); + }; + + Vec2D& operator-=(const Vec2D& vec){ + this->x -= vec.x; + this->y -= vec.y; + return *this; + }; + + friend Vec2D operator-(const Vec2D& left, const Vec2D& right){ + return Vec2D(left.x - right.x, left.y - right.y); + }; + + Vec2D& operator*(const Vec2D& vec){ + this->x *= vec.x; + this->y *= vec.y; + return *this; + }; + + Vec2D& operator*(T scalar){ + this->x *= scalar; + this->y *= scalar; + return *this; + }; +}; + +struct Color{ + int r; + int g; + int b; +}; diff --git a/src/entity.h b/src/entity.h new file mode 100644 index 0000000..cad2aae --- /dev/null +++ b/src/entity.h @@ -0,0 +1,46 @@ +#pragma once + +#include "datatypes.h" +#include +//#include +#include +#include +#include + +class Entity { + + public: + Entity() {} + Entity(float x, float y) : position(x, y) {} + Entity(float x, float y, int w, int h) : position(x, y), rect(w, h) {} + + bool load(SDL_Renderer *renderer, std::string path) { + + SDL_Surface *asset = SDL_LoadBMP(path.c_str()); + if(asset == NULL) { + SDL_Log("Unable to load image: %s", SDL_GetError()); + return false; + } + + texture = SDL_CreateTextureFromSurface(renderer, asset); + return true; + }; + + Vec2D position; + Vec2D size; + + SDL_Texture *texture; + SDL_FRect rect; + Color color = {100,100,100}; + + void draw(SDL_Renderer *renderer){ + rect.x = position.x; + rect.y = position.y; + rect.w = size.x; + rect.h = size.y; + + //SDL_SetTextureColorMod(texture, color.r, color.g, color.b); + SDL_RenderTexture(renderer, texture, NULL, &rect); + }; +}; + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..9fce337 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,289 @@ + +/* + * + * VISEQ - Visual Sequencer + * + * +*/ + +#include "SFML/Graphics/Text.hpp" +#include "libremidi/libremidi.hpp" +#include "libremidi/message.hpp" +#include "libremidi/observer_configuration.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "node.h" +#include "signal.h" + +libremidi::observer obs; +libremidi::midi_out midi_out; + +std::vector midi_in_ports; +std::vector midi_out_ports; + +std::vector nodes; +std::vector signals; + +Vec2D mouse_pos; +Vec2D drag_start; + +bool mouse_pressed = false; +bool mouse_clicked = false; +bool mouse_touched = false; + + +int main(){ + + srand(time(NULL)); + + sf::RenderWindow window(sf::VideoMode({2560,1440}), "VISEQ"); + window.setFramerateLimit(120); + + for(const libremidi::input_port& port : obs.get_input_ports()){ + midi_in_ports.push_back(port); + } + + for(const libremidi::output_port& port : obs.get_output_ports()){ + midi_out_ports.push_back(port); + } + + for(int i = 0; i < 10; i++){ + nodes.emplace(nodes.end(), Node((float)(rand() % 2000 + 280), (float)(rand() % 1000 + 220), 15.0f)); + } + + for(int i = 0; i < 10; i++){ + signals.emplace(signals.end(), Signal(2560.f/2.f, 1440.f/2.f, 5.0f)); + signals[i].target_node_id = rand() % 9; + } + + midi_out.open_port(midi_out_ports[2]); + + sf::Clock deltaClock; + + while(window.isOpen()){ + sf::Event event; + while(window.pollEvent(event) ){ + + if(event.type == sf::Event::Closed){ + window.close(); + } + } + + window.clear(); + + for( auto& n : nodes){ + + if(mouse_clicked && n.hover_move){ + n.clicked = true; + } + if(mouse_pressed && n.clicked){ + n.position = mouse_pos; + } + if(!mouse_pressed) n.clicked = false; + n.update(); + n.draw(&window); + } + + for( auto& n : signals){ + n.speed = 2.0f; + n.direction = nodes[n.target_node_id].position - n.position; + if(n.position.dist(&nodes[n.target_node_id].position) < n.speed * 1.5) { + n.target_node_id++; + midi_out.send_message(libremidi::channel_events::control_change(2, n.target_node_id, 50)); + } + if(n.target_node_id == 10) n.target_node_id = 0; + n.update(); + n.draw(&window); + } + + //window.draw(); + window.display(); + } + +} + +/***/ +/*SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv){*/ +/**/ +/* if (not SDL_Init(SDL_INIT_VIDEO)) {*/ +/* SDL_Log("Unable to initialize SDL: %s", SDL_GetError());*/ +/* return SDL_APP_FAILURE;*/ +/* }*/ +/**/ +/* SDL_Window* window = SDL_CreateWindow("Window", 1920, 1080, SDL_WINDOW_OPENGL|SDL_WINDOW_FULLSCREEN);*/ +/* if (not window){*/ +/* SDL_Log("Unable to create window: %s", SDL_GetError());*/ +/* return SDL_APP_FAILURE;*/ +/* }*/ +/**/ +/* gl = SDL_GL_CreateContext(window);*/ +/**/ +/* SDL_ShowWindow(window);*/ +/**/ +/* int window_width;*/ +/* int window_height;*/ +/* SDL_GetWindowSize(window, &window_width, &window_height);*/ +/**/ +/**/ +/*// cairo_device_t* device = cairo_glx_device_create();*/ +/**/ +/* sdl_cairo_surface = SDL_CreateSurface(renderer_width, renderer_height, SDL_PIXELFORMAT_RGBA32);*/ +/**/ +/* int stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, sdl_cairo_surface->w);*/ +/* cr_surface = cairo_image_surface_create_for_data((unsigned char*) sdl_cairo_surface->pixels, CAIRO_FORMAT_RGB24, sdl_cairo_surface->w, sdl_cairo_surface->h, stride);*/ +/**/ +/* cr = cairo_create(cr_surface);*/ +/* SDL_Log("%d", cairo_surface_status(cr_surface));*/ +/**/ +/* int cairo_x_multiplier = renderer_width / window_width;*/ +/* int cairo_y_multiplier = renderer_height / window_height;*/ +/**/ +/* cairo_surface_set_device_scale(cr_surface, cairo_x_multiplier, cairo_y_multiplier);*/ +/**/ +/* for(const libremidi::input_port& port : obs.get_input_ports()){*/ +/* midi_in_ports.push_back(port);*/ +/* }*/ +/**/ +/* for(const libremidi::output_port& port : obs.get_output_ports()){*/ +/* midi_out_ports.push_back(port);*/ +/* }*/ +/**/ +/* for(int i = 0; i < 10; i++){*/ +/* nodes.emplace(nodes.end(), Node(i*10.0f, i*10.0f, 15.0f));*/ +/* }*/ +/**/ +/* for(int i = 0; i < 10; i++){*/ +/* signals.emplace(signals.end(), Signal(i*10.0f, i*10.0f, 5.0f));*/ +/* }*/ +/**/ +/* IMGUI_CHECKVERSION();*/ +/* ImGui::CreateContext();*/ +/* io = ImGui::GetIO(); (void)io;*/ +/**/ +/* ImGui::StyleColorsDark();*/ +/* //ImGui::StyleColorsLight();*/ +/**/ +/* // Setup Platform/Renderer backends*/ +/* ImGui_ImplSDL3_InitForSDLRenderer(window, renderer);*/ +/* ImGui_ImplSDLRenderer3_Init(renderer);*/ +/**/ +/* *appstate = new AppContext{*/ +/* window,*/ +/* renderer,*/ +/* (float)window_width,*/ +/* (float)window_height*/ +/* };*/ +/**/ +/* return SDL_APP_CONTINUE;*/ +/*}*/ +/**/ +/*SDL_AppResult SDL_AppIterate(void *appstate){*/ +/* auto* app = (AppContext*)appstate;*/ +/**/ +/* SDL_SetRenderDrawColor(app->renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);*/ +/* SDL_RenderClear(app->renderer); */ +/**/ +/* // ---------- cairo*/ +/**/ +/**/ +/* cairo_set_source_rgba(cr, 0, 0, 0, 1.0);*/ +/* cairo_rectangle(cr, 0, 0, app->width, app->height);*/ +/* cairo_fill(cr);*/ +/**/ +/* for( auto& n : nodes){*/ +/**/ +/* if(mouse_clicked && n.hover_move){*/ +/* n.clicked = true;*/ +/* }*/ +/* if(mouse_pressed && n.clicked){*/ +/* n.position = mouse_pos;*/ +/* }*/ +/* if(!mouse_pressed) n.clicked = false;*/ +/* n.update();*/ +/* n.draw(cr, mouse_pos);*/ +/* }*/ +/**/ +/* for( auto& n : signals){*/ +/* n.update();*/ +/* n.draw(cr);*/ +/* }*/ +/**/ +/* SDL_Texture *texture = SDL_CreateTextureFromSurface(app->renderer, sdl_cairo_surface);*/ +/**/ +/* // ---------- ImGUI*/ +/**/ + /*ImGui_ImplSDLRenderer3_NewFrame();*/ + /*ImGui_ImplSDL3_NewFrame();*/ + /*ImGui::NewFrame();*/ + /**/ + /*ImGui::Begin("MIDI Config");*/ + /*ImGui::SameLine();*/ + /**/ + /*for(auto port : midi_out_ports){*/ + /* if(ImGui::Button(port.port_name.c_str())){*/ + /* midi_out.open_port(port);*/ + /* midi_out.send_message(144, 100, 40);*/ + /* };*/ + /*}*/ + /**/ + /*ImGui::Text("%f %f", mouse_pos.x, mouse_pos.y);*/ + /**/ + /*ImGui::End();*/ + /*ImGui::Render();*/ +/**/ +/**/ +/* // ---------- Render*/ +/**/ +/* SDL_RenderTexture(app->renderer, texture, NULL, NULL);*/ +/*// ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), app->renderer); */ +/**/ +/* SDL_RenderPresent(app->renderer);*/ +/**/ +/*// auto time = SDL_GetTicks();*/ +/* SDL_Delay(5);*/ +/* return SDL_APP_CONTINUE;*/ +/*}*/ +/**/ +/*SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event){*/ +/* //auto* app = (AppContext*)appstate;*/ +/* mouse_clicked = false;*/ +/**/ +/* const SDL_Event *ev = event;*/ +/* ImGui_ImplSDL3_ProcessEvent(ev);*/ +/**/ +/* if (event->type == SDL_EVENT_QUIT) {*/ +/* return SDL_APP_SUCCESS;*/ +/* }*/ +/**/ +/* if(event->type == SDL_EVENT_MOUSE_MOTION){*/ +/* mouse_pos = {event->motion.x, event->motion.y};*/ +/* }*/ +/**/ +/* if(event->type == SDL_EVENT_MOUSE_BUTTON_DOWN){*/ +/* if(event->button.button == SDL_BUTTON_LEFT) {*/ +/* if(!mouse_pressed) {*/ +/* drag_start = mouse_pos;*/ +/* mouse_clicked = true;*/ +/* }*/ +/* mouse_pressed = true;*/ +/* }*/ +/* }*/ +/**/ +/* if(event->type == SDL_EVENT_MOUSE_BUTTON_UP){*/ +/* if(event->button.button == SDL_BUTTON_LEFT) mouse_pressed = false;*/ +/* }*/ +/**/ +/* return SDL_APP_CONTINUE;*/ +/*}*/ +/**/ + diff --git a/src/moveable.h b/src/moveable.h new file mode 100644 index 0000000..df5aea6 --- /dev/null +++ b/src/moveable.h @@ -0,0 +1,21 @@ +#pragma once + +#include "SFML/Graphics/RenderWindow.hpp" +#include "datatypes.h" + +class Movable{ + + public: + Movable(){} + Movable(Vec2D p, Vec2D d, float s) : speed(s), position(p), direction(d) {} + Movable(Vec2D p, float s) : speed(s), position(p) {} + + float speed = 1.0f; + + Vec2D position; + Vec2D direction; + + virtual void update() {}; + virtual void draw(sf::RenderWindow *window) {}; + +}; diff --git a/src/node.h b/src/node.h new file mode 100644 index 0000000..19bd1ac --- /dev/null +++ b/src/node.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#include "datatypes.h" +#include "moveable.h" + +#include + +class Node : public Movable{ + + public: + Node() {} + Node(float x, float y, float r) : Movable(Vec2D(x, y), 0.0f), radius(r) {} + + float radius; + + bool clicked = false; + bool hover_move = false; + bool hover_connect = false; + + std::vector connections; + + void update() {}; + + void draw(sf::RenderWindow* window) { + + sf::CircleShape circle(radius); + circle.setFillColor(sf::Color::Green); + circle.setPosition(position.x, position.y); + window->draw(circle); + } +}; diff --git a/src/signal.h b/src/signal.h new file mode 100644 index 0000000..4ca21ae --- /dev/null +++ b/src/signal.h @@ -0,0 +1,30 @@ +#pragma once + +#include "SFML/Graphics/CircleShape.hpp" +#include "SFML/Graphics/Color.hpp" +#include "SFML/Graphics/RenderWindow.hpp" +#include "datatypes.h" +#include "moveable.h" + +class Signal : public Movable{ + public: + + Signal(); + Signal(float x, float y, float r) : Movable(Vec2D(x, y), Vec2D(0,0), 0.0), radius(r) {} + + int id = 0; + + int target_node_id = 1; + int source_node_id = 0; + + float radius; + + void update() { position += direction.norm() * speed;}; + + void draw(sf::RenderWindow *window) { + sf::CircleShape circle(radius); + circle.setFillColor(sf::Color::Red); + circle.setPosition(position.x, position.y); + window->draw(circle); + }; +};