diff --git a/CMakeLists.txt b/CMakeLists.txt index 691f76e5..86449ada 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.22 FATAL_ERROR) project(libremidi - VERSION 5.0.1 + VERSION 5.1.0 DESCRIPTION "A cross-platform MIDI library" LANGUAGES C CXX HOMEPAGE_URL "https://github.com/jcelerier/libremidi" diff --git a/README.md b/README.md index 8ce180ea..a2870cc5 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,11 @@ If you use this work as part of academic research, please kindly cite the [paper ## Changelog +### Since v5.1 + +* Report USB device identifiers with ALSA and udev +* PipeWire and JACK UMP support (requires PipeWire v1.4+) + ### Since v5 * Use stdx::error for error reporting until C++26 and std::error are widely available :-) * Hunt exceptions down diff --git a/bindings/python/pylibremidi.cpp b/bindings/python/pylibremidi.cpp index 69b42819..5e28249d 100644 --- a/bindings/python/pylibremidi.cpp +++ b/bindings/python/pylibremidi.cpp @@ -212,6 +212,8 @@ NB_MODULE(pylibremidi, m) { .value("WINDOWS_MIDI_SERVICES", libremidi::API::WINDOWS_MIDI_SERVICES) .value("KEYBOARD_UMP", libremidi::API::KEYBOARD_UMP) .value("NETWORK_UMP", libremidi::API::NETWORK_UMP) + .value("JACK_UMP", libremidi::API::JACK_UMP) + .value("PIPEWIRE_UMP", libremidi::API::PIPEWIRE_UMP) .value("DUMMY", libremidi::API::DUMMY) .export_values(); @@ -345,10 +347,12 @@ NB_MODULE(pylibremidi, m) { nb::class_(m, "CoremidiUmpInputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::coremidi_ump::input_configuration::client_name); nb::class_(m, "EmscriptenInputConfiguration").def(nb::init<>()); nb::class_(m, "JackInputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::jack_input_configuration::client_name); + nb::class_(m, "JackUmpInputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::jack_ump::input_configuration::client_name); nb::class_(m, "KeyboardInputConfiguration").def(nb::init<>()); nb::class_(m, "DatagramInputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::net::dgram_input_configuration::client_name); nb::class_(m, "DatagramUmpInputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::net_ump::dgram_input_configuration::client_name); nb::class_(m, "PipewireInputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::pipewire_input_configuration::client_name); + nb::class_(m, "PipewireUmpInputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::pipewire_ump::input_configuration::client_name); nb::class_(m, "WinmidiInputConfiguration").def(nb::init<>()); nb::class_(m, "WinmmInputConfiguration").def(nb::init<>()); nb::class_(m, "WinuwpInputConfiguration").def(nb::init<>()); @@ -361,9 +365,11 @@ NB_MODULE(pylibremidi, m) { nb::class_(m, "CoremidiUmpOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::coremidi_ump::output_configuration::client_name); nb::class_(m, "EmscriptenOutputConfiguration").def(nb::init<>()); nb::class_(m, "JackOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::jack_output_configuration::client_name); + nb::class_(m, "JackUmpOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::jack_ump::output_configuration::client_name); nb::class_(m, "DatagramOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::net::dgram_output_configuration::client_name); nb::class_(m, "DatagramUmpOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::net_ump::dgram_output_configuration::client_name); nb::class_(m, "PipewireOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::pipewire_output_configuration::client_name); + nb::class_(m, "PipewireUmpOutputConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::pipewire_ump::output_configuration::client_name); nb::class_(m, "WinmidiOutputConfiguration").def(nb::init<>()); nb::class_(m, "WinmmOutputConfiguration").def(nb::init<>()); nb::class_(m, "WinuwpOutputConfiguration").def(nb::init<>()); @@ -380,11 +386,15 @@ NB_MODULE(pylibremidi, m) { .def_rw("client_name", &libremidi::coremidi_ump::observer_configuration::client_name); nb::class_(m, "EmscriptenObserverConfiguration").def(nb::init<>()); nb::class_(m, "JackObserverConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::jack_observer_configuration::client_name); + nb::class_(m, "JackUmpObserverConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::jack_ump::observer_configuration::client_name); nb::class_(m, "DatagramObserverConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::net::dgram_observer_configuration::client_name); nb::class_(m, "DatagramUmpObserverConfiguration") .def(nb::init<>()) .def_rw("client_name", &libremidi::net_ump::dgram_observer_configuration::client_name); nb::class_(m, "PipewireObserverConfiguration").def(nb::init<>()).def_rw("client_name", &libremidi::pipewire_observer_configuration::client_name); + nb::class_(m, "PipewireObserverConfiguration") + .def(nb::init<>()) + .def_rw("client_name", &libremidi::pipewire_ump::observer_configuration::client_name); nb::class_(m, "WinmidiObserverConfiguration").def(nb::init<>()); nb::class_(m, "WinmmObserverConfiguration").def(nb::init<>()); nb::class_(m, "WinuwpObserverConfiguration").def(nb::init<>()); diff --git a/cmake/libremidi.examples.cmake b/cmake/libremidi.examples.cmake index 3c2f82c1..42c2e94b 100644 --- a/cmake/libremidi.examples.cmake +++ b/cmake/libremidi.examples.cmake @@ -63,12 +63,19 @@ endif() if(LIBREMIDI_HAS_JACK) add_example(jack_share) + add_backend_example(midi2_in_jack) + add_backend_example(midi2_out_jack) endif() if(LIBREMIDI_HAS_PIPEWIRE) add_example(pipewire_share) add_backend_example(midi1_in_pipewire) add_backend_example(midi1_out_pipewire) + + if(LIBREMIDI_HAS_PIPEWIRE_UMP) + add_backend_example(midi2_in_pipewire) + add_backend_example(midi2_out_pipewire) + endif() endif() if(LIBREMIDI_HAS_COREMIDI) diff --git a/cmake/libremidi.pipewire.cmake b/cmake/libremidi.pipewire.cmake index fabdc0e3..7916e4b8 100644 --- a/cmake/libremidi.pipewire.cmake +++ b/cmake/libremidi.pipewire.cmake @@ -22,6 +22,11 @@ if(PIPEWIRE_INCLUDEDIR AND SPA_INCLUDEDIR) message(STATUS "libremidi: using PipeWire") set(LIBREMIDI_HAS_PIPEWIRE 1) + set(CMAKE_REQUIRED_INCLUDES "${SPA_INCLUDEDIR}/spa-0.2") + check_cxx_source_compiles("#include \nint main() { return sizeof(SPA_CONTROL_UMP); }" LIBREMIDI_HAS_PIPEWIRE_UMP) + + unset(CMAKE_REQUIRED_INCLUDES) + target_compile_options(libremidi ${_public} $<$:-Wno-gnu-statement-expression-from-macro-expansion> @@ -30,6 +35,7 @@ if(PIPEWIRE_INCLUDEDIR AND SPA_INCLUDEDIR) target_compile_definitions(libremidi ${_public} LIBREMIDI_PIPEWIRE + $<$:LIBREMIDI_PIPEWIRE_UMP> ) target_include_directories(libremidi SYSTEM ${_public} diff --git a/cmake/libremidi.sources.cmake b/cmake/libremidi.sources.cmake index 774f1533..667846e7 100644 --- a/cmake/libremidi.sources.cmake +++ b/cmake/libremidi.sources.cmake @@ -1,38 +1,46 @@ target_sources(libremidi PRIVATE - include/libremidi/backends/alsa_seq/config.hpp - include/libremidi/backends/alsa_seq/helpers.hpp - include/libremidi/backends/alsa_seq/midi_in.hpp - include/libremidi/backends/alsa_seq/midi_out.hpp - include/libremidi/backends/alsa_seq/observer.hpp - include/libremidi/backends/alsa_seq/shared_handler.hpp - include/libremidi/backends/alsa_raw/config.hpp include/libremidi/backends/alsa_raw/helpers.hpp include/libremidi/backends/alsa_raw/midi_in.hpp include/libremidi/backends/alsa_raw/midi_out.hpp include/libremidi/backends/alsa_raw/observer.hpp + include/libremidi/backends/alsa_raw.hpp include/libremidi/backends/alsa_raw_ump/config.hpp include/libremidi/backends/alsa_raw_ump/helpers.hpp include/libremidi/backends/alsa_raw_ump/midi_in.hpp include/libremidi/backends/alsa_raw_ump/midi_out.hpp include/libremidi/backends/alsa_raw_ump/observer.hpp + include/libremidi/backends/alsa_raw_ump.hpp + + include/libremidi/backends/alsa_seq/config.hpp + include/libremidi/backends/alsa_seq/helpers.hpp + include/libremidi/backends/alsa_seq/midi_in.hpp + include/libremidi/backends/alsa_seq/midi_out.hpp + include/libremidi/backends/alsa_seq/observer.hpp + include/libremidi/backends/alsa_seq/shared_handler.hpp + include/libremidi/backends/alsa_seq.hpp include/libremidi/backends/alsa_seq_ump/config.hpp include/libremidi/backends/alsa_seq_ump/helpers.hpp include/libremidi/backends/alsa_seq_ump/midi_out.hpp + include/libremidi/backends/alsa_seq_ump.hpp include/libremidi/backends/coremidi/config.hpp include/libremidi/backends/coremidi/helpers.hpp include/libremidi/backends/coremidi/midi_in.hpp include/libremidi/backends/coremidi/midi_out.hpp include/libremidi/backends/coremidi/observer.hpp + include/libremidi/backends/coremidi.hpp include/libremidi/backends/coremidi_ump/config.hpp include/libremidi/backends/coremidi_ump/helpers.hpp include/libremidi/backends/coremidi_ump/midi_in.hpp include/libremidi/backends/coremidi_ump/midi_out.hpp include/libremidi/backends/coremidi_ump/observer.hpp + include/libremidi/backends/coremidi_ump.hpp + + include/libremidi/backends/dummy.hpp include/libremidi/backends/emscripten/config.hpp include/libremidi/backends/emscripten/helpers.hpp @@ -44,13 +52,22 @@ target_sources(libremidi PRIVATE include/libremidi/backends/emscripten/midi_out.hpp include/libremidi/backends/emscripten/observer.cpp include/libremidi/backends/emscripten/observer.hpp + include/libremidi/backends/emscripten.hpp include/libremidi/backends/jack/config.hpp + include/libremidi/backends/jack/error_domain.hpp include/libremidi/backends/jack/helpers.hpp include/libremidi/backends/jack/midi_in.hpp include/libremidi/backends/jack/midi_out.hpp include/libremidi/backends/jack/observer.hpp include/libremidi/backends/jack/shared_handler.hpp + include/libremidi/backends/jack.hpp + + include/libremidi/backends/jack_ump/config.hpp + include/libremidi/backends/jack_ump/midi_in.hpp + include/libremidi/backends/jack_ump/midi_out.hpp + include/libremidi/backends/jack_ump/observer.hpp + include/libremidi/backends/jack_ump.hpp include/libremidi/backends/keyboard/config.hpp include/libremidi/backends/keyboard/midi_in.hpp @@ -74,35 +91,28 @@ target_sources(libremidi PRIVATE include/libremidi/backends/pipewire/midi_out.hpp include/libremidi/backends/pipewire/observer.hpp include/libremidi/backends/pipewire/shared_handler.hpp + include/libremidi/backends/pipewire.hpp + include/libremidi/backends/pipewire_ump.hpp include/libremidi/backends/winmidi/config.hpp include/libremidi/backends/winmidi/helpers.hpp include/libremidi/backends/winmidi/midi_in.hpp include/libremidi/backends/winmidi/midi_out.hpp include/libremidi/backends/winmidi/observer.hpp + include/libremidi/backends/winmidi.hpp include/libremidi/backends/winmm/config.hpp include/libremidi/backends/winmm/helpers.hpp include/libremidi/backends/winmm/midi_in.hpp include/libremidi/backends/winmm/midi_out.hpp include/libremidi/backends/winmm/observer.hpp + include/libremidi/backends/winmm.hpp include/libremidi/backends/winuwp/config.hpp include/libremidi/backends/winuwp/helpers.hpp include/libremidi/backends/winuwp/midi_in.hpp include/libremidi/backends/winuwp/midi_out.hpp include/libremidi/backends/winuwp/observer.hpp - - include/libremidi/backends/alsa_raw.hpp - include/libremidi/backends/alsa_raw_ump.hpp - include/libremidi/backends/alsa_seq.hpp - include/libremidi/backends/alsa_seq_ump.hpp - include/libremidi/backends/coremidi.hpp - include/libremidi/backends/coremidi_ump.hpp - include/libremidi/backends/dummy.hpp - include/libremidi/backends/emscripten.hpp - include/libremidi/backends/jack.hpp - include/libremidi/backends/winmm.hpp include/libremidi/backends/winuwp.hpp include/libremidi/detail/memory.hpp diff --git a/examples/backends/midi2_in_jack.cpp b/examples/backends/midi2_in_jack.cpp new file mode 100644 index 00000000..28ec860c --- /dev/null +++ b/examples/backends/midi2_in_jack.cpp @@ -0,0 +1,43 @@ +#include "backend_test_utils.hpp" + +#include +#include + +using api = libremidi::jack_ump::backend; + +int main(void) +try +{ + std::cerr << "API: " << api::name << "\n"; + + libremidi::observer_configuration obs_config{.track_any = false}; + api::midi_observer_configuration obs_api_config; + libremidi::observer obs{obs_config, obs_api_config}; + + auto ports = obs.get_input_ports(); + for (const auto& port : ports) + { + std::cerr << "Port: " << port.display_name << " : " << port.port_name << "; " + << port.device_name << "\n"; + } + + { + libremidi::ump_input_configuration in_config; + in_config.on_message = [](const libremidi::ump& m) { std::cerr << m << "\n"; }; + api::midi_in_configuration in_api_config; + libremidi::midi_in midiin{in_config, in_api_config}; + + if (!ports.empty()) + { + midiin.open_port(ports[0]); + std::this_thread::sleep_for(std::chrono::seconds(50)); + } + } + + return 0; +} +catch (const std::exception& error) +{ + std::cerr << error.what() << std::endl; + exit(EXIT_FAILURE); +} diff --git a/examples/backends/midi2_in_pipewire.cpp b/examples/backends/midi2_in_pipewire.cpp new file mode 100644 index 00000000..d9cec6b9 --- /dev/null +++ b/examples/backends/midi2_in_pipewire.cpp @@ -0,0 +1,43 @@ +#include "backend_test_utils.hpp" + +#include +#include + +using api = libremidi::pipewire_ump::backend; + +int main(void) +try +{ + std::cerr << "API: " << api::name << "\n"; + + libremidi::observer_configuration obs_config{.track_any = false}; + api::midi_observer_configuration obs_api_config; + libremidi::observer obs{obs_config, obs_api_config}; + + auto ports = obs.get_input_ports(); + for (const auto& port : ports) + { + std::cerr << "Port: " << port.display_name << " : " << port.port_name << "; " + << port.device_name << "\n"; + } + + { + libremidi::ump_input_configuration in_config; + in_config.on_message = [](const libremidi::ump& m) { std::cerr << m << "\n"; }; + api::midi_in_configuration in_api_config; + libremidi::midi_in midiin{in_config, in_api_config}; + + if (!ports.empty()) + { + midiin.open_port(ports[0]); + std::this_thread::sleep_for(std::chrono::seconds(50)); + } + } + + return 0; +} +catch (const std::exception& error) +{ + std::cerr << error.what() << std::endl; + exit(EXIT_FAILURE); +} diff --git a/examples/backends/midi2_out_jack.cpp b/examples/backends/midi2_out_jack.cpp new file mode 100644 index 00000000..9453a4c6 --- /dev/null +++ b/examples/backends/midi2_out_jack.cpp @@ -0,0 +1,44 @@ +#include "backend_test_utils.hpp" + +#include +#include + +using api = libremidi::jack_ump::backend; + +int main(void) +try +{ + std::cerr << "API: " << api::name << "\n"; + + libremidi::observer_configuration obs_config{.track_any = true}; + api::midi_observer_configuration obs_api_config; + libremidi::observer obs{obs_config, obs_api_config}; + + auto ports = obs.get_output_ports(); + for (const auto& port : ports) + { + std::cerr << "Port: " << port.port_name << "\n"; + } + + libremidi::output_configuration out_config; + api::midi_out_configuration out_api_config; + libremidi::midi_out midiout{out_config, out_api_config}; + + { + midiout.open_virtual_port(); + + for (int n = 0; n < 100; n++) + { + for (int i = 81; i < 89; i++) + midiout.send_message(libremidi::channel_events::control_change(1, i, rand())); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + } + + return 0; +} +catch (const std::exception& error) +{ + std::cerr << error.what() << std::endl; + exit(EXIT_FAILURE); +} diff --git a/examples/backends/midi2_out_pipewire.cpp b/examples/backends/midi2_out_pipewire.cpp new file mode 100644 index 00000000..fbe125fe --- /dev/null +++ b/examples/backends/midi2_out_pipewire.cpp @@ -0,0 +1,44 @@ +#include "backend_test_utils.hpp" + +#include +#include + +using api = libremidi::pipewire_ump::backend; + +int main(void) +try +{ + std::cerr << "API: " << api::name << "\n"; + + libremidi::observer_configuration obs_config{.track_any = true}; + api::midi_observer_configuration obs_api_config; + libremidi::observer obs{obs_config, obs_api_config}; + + auto ports = obs.get_output_ports(); + for (const auto& port : ports) + { + std::cerr << "Port: " << port.port_name << "\n"; + } + + libremidi::output_configuration out_config; + api::midi_out_configuration out_api_config; + libremidi::midi_out midiout{out_config, out_api_config}; + + { + midiout.open_virtual_port(); + + for (int n = 0; n < 100; n++) + { + for (int i = 81; i < 89; i++) + midiout.send_message(libremidi::channel_events::control_change(1, i, rand())); + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + } + } + + return 0; +} +catch (const std::exception& error) +{ + std::cerr << error.what() << std::endl; + exit(EXIT_FAILURE); +} diff --git a/examples/midi2_echo.cpp b/examples/midi2_echo.cpp index 149d05cf..bcbd5f71 100644 --- a/examples/midi2_echo.cpp +++ b/examples/midi2_echo.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -13,7 +14,7 @@ try auto pi = obs.get_input_ports(); auto po = obs.get_output_ports(); if (pi.empty() || po.empty()) - throw std::runtime_error("No MIDI in / out available"); + throw std::runtime_error("No MIDI in / out pair available"); // Create a midi out midi_out midiout{{}, lm2::out_default_configuration()}; diff --git a/include/libremidi/api-c.h b/include/libremidi/api-c.h index db5f26ab..8822253f 100644 --- a/include/libremidi/api-c.h +++ b/include/libremidi/api-c.h @@ -30,6 +30,8 @@ typedef enum libremidi_api WINDOWS_MIDI_SERVICES, /*!< Windows API for MIDI 2.0. Requires Windows 11 */ KEYBOARD_UMP, /*!< Computer keyboard input */ NETWORK_UMP, /*!< MIDI2 over IP */ + JACK_UMP, /*!< MIDI2 over JACK, type "32 bit raw UMP". Requires PipeWire v1.4+. */ + PIPEWIRE_UMP, /*!< MIDI2 over PipeWire. Requires v1.4+. */ DUMMY = 0xFFFF /*!< A compilable but non-functional API. */ } libremidi_api; diff --git a/include/libremidi/backends.hpp b/include/libremidi/backends.hpp index 86b638a5..18c42a12 100644 --- a/include/libremidi/backends.hpp +++ b/include/libremidi/backends.hpp @@ -29,10 +29,14 @@ #if defined(LIBREMIDI_JACK) #include + #include #endif #if defined(LIBREMIDI_PIPEWIRE) #include + #if defined(LIBREMIDI_PIPEWIRE_UMP) + #include + #endif #endif #if defined(LIBREMIDI_COREMIDI) @@ -162,6 +166,14 @@ static constexpr auto available_backends = make_tl( #if defined(LIBREMIDI_NETWORK) , net_ump::backend{} +#endif +#if defined(LIBREMIDI_JACK) + , + jack_ump::backend{} +#endif +#if defined(LIBREMIDI_PIPEWIRE_UMP) + , + pipewire_ump::backend{} #endif , dummy_backend{}); @@ -172,7 +184,7 @@ static_assert(std::tuple_size_v >= 1); template auto for_all_backends(F&& f) { - std::apply([&](auto&&... x) { (f(x), ...); }, available_backends); + std::apply([&](auto&&... x) { ((x.available() && (f(x), true)), ...); }, available_backends); } template diff --git a/include/libremidi/backends/jack/helpers.hpp b/include/libremidi/backends/jack/helpers.hpp index 113d2b4d..4811ca5d 100644 --- a/include/libremidi/backends/jack/helpers.hpp +++ b/include/libremidi/backends/jack/helpers.hpp @@ -56,16 +56,16 @@ struct jack_client } template - static auto - get_ports(jack_client_t* client, const char* pattern, const JackPortFlags flags) noexcept - -> std::vector> + static auto get_ports( + jack_client_t* client, const char* pattern, const char* type, const JackPortFlags flags, + bool midi2) noexcept -> std::vector> { std::vector> ret; if (!client) return {}; - const char** ports = jack_get_ports(client, pattern, JACK_DEFAULT_MIDI_TYPE, flags); + const char** ports = jack_get_ports(client, pattern, type, flags); if (ports == nullptr) return {}; @@ -75,7 +75,11 @@ struct jack_client { // 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)); + if (port) + { + if (bool(midi2) == bool(jack_port_flags(port) & 0x20)) + ret.push_back(to_port_info(client, port)); + } i++; } @@ -194,8 +198,8 @@ struct jack_helpers : jack_client self.client_open_ = std::errc::not_connected; } - stdx::error - create_local_port(const auto& self, std::string_view portName, JackPortFlags flags) + stdx::error create_local_port( + const auto& self, std::string_view portName, const char* type, JackPortFlags flags) { // full name: "client_name:port_name\0" if (portName.empty()) @@ -211,8 +215,7 @@ struct jack_helpers : jack_client if (!this->port) { - this->port - = jack_port_register(this->client, portName.data(), JACK_DEFAULT_MIDI_TYPE, flags, 0); + this->port = jack_port_register(this->client, portName.data(), type, flags, 0); } if (!this->port) @@ -240,4 +243,77 @@ struct jack_helpers : jack_client return from_errc(err); } }; + +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); + } + + stdx::error write(const unsigned char* data, int64_t sz) const noexcept + { + if (static_cast(sz + size_sz) > ringbuffer_space) + return std::errc::no_buffer_space; + + 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); + + return stdx::error{}; + } + + 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 +}; + +struct jack_midi1 +{ + static constexpr const char* port_type = "8 bit raw midi"; +}; + +struct jack_midi2 +{ + static constexpr const char* port_type = "32 bit raw UMP"; +}; } diff --git a/include/libremidi/backends/jack/midi_in.hpp b/include/libremidi/backends/jack/midi_in.hpp index c051c157..f49e5822 100644 --- a/include/libremidi/backends/jack/midi_in.hpp +++ b/include/libremidi/backends/jack/midi_in.hpp @@ -9,6 +9,7 @@ namespace libremidi class midi_in_jack final : public midi1::in_api , public jack_helpers + , public jack_midi1 , public error_handler { public: @@ -44,7 +45,8 @@ class midi_in_jack final stdx::error open_port(const input_port& port, std::string_view portName) override { - if (auto err = create_local_port(*this, portName, JackPortIsInput); err != stdx::error{}) + if (auto err = create_local_port(*this, portName, port_type, JackPortIsInput); + err != stdx::error{}) return err; if (int err = jack_connect(this->client, port.port_name.c_str(), jack_port_name(this->port)); @@ -60,7 +62,7 @@ class midi_in_jack final stdx::error open_virtual_port(std::string_view portName) override { - return create_local_port(*this, portName, JackPortIsInput); + return create_local_port(*this, portName, port_type, JackPortIsInput); } stdx::error close_port() override { return do_close_port(); } diff --git a/include/libremidi/backends/jack/midi_out.hpp b/include/libremidi/backends/jack/midi_out.hpp index 1a2fd91b..544662a7 100644 --- a/include/libremidi/backends/jack/midi_out.hpp +++ b/include/libremidi/backends/jack/midi_out.hpp @@ -3,76 +3,12 @@ #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); - } - - stdx::error write(const unsigned char* data, int64_t sz) const noexcept - { - if (static_cast(sz + size_sz) > ringbuffer_space) - return std::errc::no_buffer_space; - - 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); - - return stdx::error{}; - } - - 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 jack_midi1 , public error_handler { public: @@ -94,7 +30,8 @@ class midi_out_jack stdx::error open_port(const output_port& port, std::string_view portName) override { - if (auto err = create_local_port(*this, portName, JackPortIsOutput); err != stdx::error{}) + if (auto err = create_local_port(*this, portName, port_type, JackPortIsOutput); + err != stdx::error{}) return err; // Connecting to the output @@ -111,7 +48,7 @@ class midi_out_jack stdx::error open_virtual_port(std::string_view portName) override { - return create_local_port(*this, portName, JackPortIsOutput); + return create_local_port(*this, portName, port_type, JackPortIsOutput); } stdx::error close_port() override { return do_close_port(); } diff --git a/include/libremidi/backends/jack/observer.hpp b/include/libremidi/backends/jack/observer.hpp index 01226b17..f1467e21 100644 --- a/include/libremidi/backends/jack/observer.hpp +++ b/include/libremidi/backends/jack/observer.hpp @@ -10,6 +10,7 @@ namespace libremidi class observer_jack final : public observer_api , private jack_client + , public jack_midi1 , private error_handler { public: @@ -48,8 +49,7 @@ class observer_jack final void initial_callback() { { - const char** ports - = jack_get_ports(client, nullptr, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput); + const char** ports = jack_get_ports(client, nullptr, port_type, JackPortIsOutput); if (ports != nullptr) { @@ -80,8 +80,7 @@ class observer_jack final } { - const char** ports - = jack_get_ports(client, nullptr, JACK_DEFAULT_MIDI_TYPE, JackPortIsInput); + const char** ports = jack_get_ports(client, nullptr, port_type, JackPortIsInput); if (ports != nullptr) { @@ -119,7 +118,7 @@ class observer_jack final if (reg) { std::string_view type = jack_port_type(port); - if (type != JACK_DEFAULT_MIDI_TYPE) + if (type != port_type) return; bool physical = flags & JackPortIsPhysical; @@ -199,12 +198,12 @@ class observer_jack final std::vector get_input_ports() const noexcept override { - return get_ports(this->client, nullptr, JackPortIsOutput); + return get_ports(this->client, nullptr, port_type, JackPortIsOutput, false); } std::vector get_output_ports() const noexcept override { - return get_ports(this->client, nullptr, JackPortIsInput); + return get_ports(this->client, nullptr, port_type, JackPortIsInput, false); } ~observer_jack() diff --git a/include/libremidi/backends/jack_ump.hpp b/include/libremidi/backends/jack_ump.hpp new file mode 100644 index 00000000..2246c240 --- /dev/null +++ b/include/libremidi/backends/jack_ump.hpp @@ -0,0 +1,34 @@ +#pragma once +#include +#include +#include + +#include + +namespace libremidi::jack_ump +{ +struct backend +{ + using midi_in = midi_in_jack; + using midi_out = midi_out_jack; + using midi_observer = observer_jack; + using midi_in_configuration = jack_ump::input_configuration; + using midi_out_configuration = jack_ump::output_configuration; + using midi_observer_configuration = jack_ump::observer_configuration; + static const constexpr auto API = libremidi::API::JACK_UMP; + static const constexpr std::string_view name = "jack_ump"; + static const constexpr std::string_view display_name = "JACK (UMP)"; + + static inline bool available() noexcept + { +#if LIBREMIDI_WEAKJACK + if (WeakJack::instance().available() != 0) + return false; +#endif + + int major, minor, micro, patch; + jack_get_version(&major, &minor, µ, &patch); + return (major >= 3 && minor >= 1 && micro >= 4); + } +}; +} diff --git a/include/libremidi/backends/jack_ump/config.hpp b/include/libremidi/backends/jack_ump/config.hpp new file mode 100644 index 00000000..e828e252 --- /dev/null +++ b/include/libremidi/backends/jack_ump/config.hpp @@ -0,0 +1,33 @@ +#pragma once +#include + +namespace libremidi::jack_ump +{ +struct input_configuration +{ + std::string client_name = "libremidi client"; + + jack_client_t* context{}; + std::function set_process_func{}; + std::function clear_process_func{}; +}; + +struct 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 observer_configuration +{ + std::string client_name = "libremidi client"; + jack_client_t* context{}; +}; + +} diff --git a/include/libremidi/backends/jack_ump/midi_in.hpp b/include/libremidi/backends/jack_ump/midi_in.hpp new file mode 100644 index 00000000..7aa60879 --- /dev/null +++ b/include/libremidi/backends/jack_ump/midi_in.hpp @@ -0,0 +1,119 @@ +#pragma once +#include +#include +#include +#include + +namespace libremidi::jack_ump +{ +class midi_in_jack final + : public midi2::in_api + , public jack_helpers + , private jack_midi1 + , public error_handler +{ +public: + using midi_api::client_open_; + struct + : libremidi::ump_input_configuration + , jack_ump::input_configuration + { + } configuration; + + explicit midi_in_jack( + libremidi::ump_input_configuration&& conf, jack_ump::input_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + auto status = connect(*this); + if (!this->client) + { + libremidi_handle_error(configuration, "Could not create JACK client"); + client_open_ = from_jack_status(status); + return; + } + + client_open_ = stdx::error{}; + } + + ~midi_in_jack() override + { + midi_in_jack::close_port(); + + disconnect(*this); + } + + libremidi::API get_current_api() const noexcept override { return libremidi::API::JACK_UMP; } + + stdx::error open_port(const input_port& port, std::string_view portName) override + { + if (auto err + = create_local_port(*this, portName, port_type, JackPortFlags(JackPortIsInput | 0x20)); + err != stdx::error{}) + return err; + + if (int err = jack_connect(this->client, port.port_name.c_str(), jack_port_name(this->port)); + err != 0 && err != EEXIST) + { + libremidi_handle_error( + configuration, + "could not connect to port: " + port.port_name + " -> " + jack_port_name(this->port)); + return from_errc(err); + } + return stdx::error{}; + } + + stdx::error open_virtual_port(std::string_view portName) override + { + return create_local_port(*this, portName, port_type, JackPortFlags(JackPortIsInput | 0x20)); + } + + stdx::error close_port() override { return do_close_port(); } + + stdx::error set_port_name(std::string_view portName) override + { + int ret = jack_port_rename(this->client, this->port, portName.data()); + return from_errc(ret); + } + + 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 ev_count = jack_midi_get_event_count(buff); + for (uint32_t j = 0; j < ev_count; 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( + {(uint32_t*)event.buffer, (uint32_t*)(event.buffer + event.size)}, + m_processing.timestamp(to_ns, event.time)); + } + + return 0; + } + + midi2::input_state_machine m_processing{this->configuration}; +}; +} diff --git a/include/libremidi/backends/jack_ump/midi_out.hpp b/include/libremidi/backends/jack_ump/midi_out.hpp new file mode 100644 index 00000000..643070ba --- /dev/null +++ b/include/libremidi/backends/jack_ump/midi_out.hpp @@ -0,0 +1,184 @@ +#pragma once +#include +#include +#include + +namespace libremidi::jack_ump +{ +class midi_out_jack + : public midi2::out_api + , public jack_helpers + , private jack_midi1 + , public error_handler +{ +public: + using midi_api::client_open_; + struct + : libremidi::output_configuration + , jack_ump::output_configuration + { + } configuration; + + midi_out_jack(libremidi::output_configuration&& conf, jack_ump::output_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + } + + ~midi_out_jack() override { } + + libremidi::API get_current_api() const noexcept override { return libremidi::API::JACK_UMP; } + + stdx::error open_port(const output_port& port, std::string_view portName) override + { + if (auto err + = create_local_port(*this, portName, port_type, JackPortFlags(JackPortIsOutput | 0x20)); + err != stdx::error{}) + return err; + + // Connecting to the output + if (int err = jack_connect(this->client, jack_port_name(this->port), port.port_name.c_str()); + err != 0 && err != EEXIST) + { + libremidi_handle_error(configuration, "could not connect to port" + port.port_name); + return from_errc(err); + } + + return stdx::error{}; + } + + stdx::error open_virtual_port(std::string_view portName) override + { + return create_local_port(*this, portName, port_type, JackPortFlags(JackPortIsOutput | 0x20)); + } + + stdx::error close_port() override { return do_close_port(); } + + stdx::error set_port_name(std::string_view portName) override + { + int ret = jack_port_rename(this->client, this->port, portName.data()); + return from_errc(ret); + } +}; + +class midi_out_jack_queued final : public midi_out_jack +{ +public: + midi_out_jack_queued( + libremidi::output_configuration&& conf, jack_ump::output_configuration&& apiconf) + : midi_out_jack{std::move(conf), std::move(apiconf)} + , m_queue{configuration.ringbuffer_size} + { + auto status = connect(*this); + if (!this->client) + { + libremidi_handle_error(configuration, "Could not create JACK client"); + client_open_ = from_jack_status(status); + return; + } + + client_open_ = stdx::error{}; + } + + ~midi_out_jack_queued() override + { + midi_out_jack::close_port(); + + disconnect(*this); + } + + stdx::error send_ump(const uint32_t* message, std::size_t size) override + { + return m_queue.write((unsigned char*)message, size * sizeof(uint32_t)); + } + + int process(jack_nframes_t nframes) + { + void* buff = jack_port_get_buffer(this->port, nframes); + jack_midi_clear_buffer(buff); + + this->m_queue.read(buff); + + return 0; + } + +private: + libremidi::jack_queue m_queue; +}; + +class midi_out_jack_direct final : public midi_out_jack +{ +public: + midi_out_jack_direct( + libremidi::output_configuration&& conf, jack_ump::output_configuration&& apiconf) + : midi_out_jack{std::move(conf), std::move(apiconf)} + { + auto status = connect(*this); + if (!this->client) + { + libremidi_handle_error(configuration, "Could not create JACK client"); + client_open_ = from_jack_status(status); + return; + } + + buffer_size = jack_get_buffer_size(this->client); + client_open_ = stdx::error{}; + } + + ~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; + } + + stdx::error send_ump(const uint32_t* message, std::size_t size) override + { + void* buff = jack_port_get_buffer(this->port, buffer_size); + int ret = jack_midi_event_write(buff, 0, (unsigned char*)message, size * sizeof(uint32_t)); + return from_errc(ret); + } + + int convert_timestamp(int64_t user) const noexcept + { + switch (configuration.timestamps) + { + case timestamp_mode::AudioFrame: + return static_cast(user); + + default: + // TODO + return 0; + } + } + + stdx::error schedule_ump(int64_t ts, const uint32_t* message, size_t size) override + { + void* buff = jack_port_get_buffer(this->port, buffer_size); + int ret = jack_midi_event_write( + buff, convert_timestamp(ts), (unsigned char*)message, size * sizeof(uint32_t)); + return from_errc(ret); + } + + int buffer_size{}; +}; +} + +namespace libremidi +{ +template <> +inline std::unique_ptr +make(output_configuration&& conf, jack_ump::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_ump/observer.hpp b/include/libremidi/backends/jack_ump/observer.hpp new file mode 100644 index 00000000..699166a2 --- /dev/null +++ b/include/libremidi/backends/jack_ump/observer.hpp @@ -0,0 +1,235 @@ +#pragma once +#include +#include +#include + +#include + +namespace libremidi::jack_ump +{ +class observer_jack final + : public observer_api + , private jack_client + , private jack_midi1 // seems like a bug in PipeWire? MIDI 2 flag is set but the MIDI 1 type string is used + , private error_handler +{ +public: + struct + : libremidi::observer_configuration + , jack_ump::observer_configuration + { + } configuration; + + explicit observer_jack( + libremidi::observer_configuration&& conf, jack_ump::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{}) + libremidi_handle_error(configuration, std::to_string((int)status)); + + if (this->client != nullptr) + { + set_callbacks(); + + jack_activate(this->client); + } + } + } + + void initial_callback() + { + { + const char** ports = jack_get_ports(client, nullptr, port_type, JackPortIsOutput); + + if (ports != nullptr) + { + int i = 0; + while (ports[i] != nullptr) + { + const auto port = jack_port_by_name(client, ports[i]); + const auto flags = jack_port_flags(port); + + if (!(flags & 0x20)) // midi 2 check + { + i++; + continue; + } + + 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, port_type, JackPortIsInput); + + if (ports != nullptr) + { + int i = 0; + while (ports[i] != nullptr) + { + const auto port = jack_port_by_name(client, ports[i]); + const auto flags = jack_port_flags(port); + + if (!(flags & 0x20)) // midi 2 check + { + i++; + continue; + } + + 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 != port_type) + return; + + if (!(flags & 0x20)) // midi 2 check + 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_UMP; } + + std::vector get_input_ports() const noexcept override + { + return get_ports(this->client, nullptr, port_type, JackPortIsOutput, true); + } + + std::vector get_output_ports() const noexcept override + { + return get_ports(this->client, nullptr, port_type, JackPortIsInput, true); + } + + ~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_ump/shared_handler.hpp b/include/libremidi/backends/jack_ump/shared_handler.hpp new file mode 100644 index 00000000..e69de29b diff --git a/include/libremidi/backends/linux/pipewire.hpp b/include/libremidi/backends/linux/pipewire.hpp index 4554e30b..4553cec5 100644 --- a/include/libremidi/backends/linux/pipewire.hpp +++ b/include/libremidi/backends/linux/pipewire.hpp @@ -12,6 +12,8 @@ namespace libremidi class libpipewire { public: + decltype(&::pw_get_library_version) get_library_version{}; + decltype(&::pw_init) init{}; decltype(&::pw_deinit) deinit{}; @@ -81,6 +83,9 @@ class libpipewire // in terms of regex: // decltype\(&::([a-z_]+)\) [a-z_]+{}; // \1 = library.symbol("\1"); + get_library_version + = m_library.symbol("pw_get_library_version"); + init = m_library.symbol("pw_init"); deinit = m_library.symbol("pw_deinit"); @@ -130,6 +135,8 @@ class libpipewire = m_library.symbol("pw_filter_queue_buffer"); filter_flush = m_library.symbol("pw_filter_flush"); + assert(get_library_version); + assert(init); assert(deinit); diff --git a/include/libremidi/backends/pipewire/context.hpp b/include/libremidi/backends/pipewire/context.hpp index b46459cc..8e2700d3 100644 --- a/include/libremidi/backends/pipewire/context.hpp +++ b/include/libremidi/backends/pipewire/context.hpp @@ -362,6 +362,8 @@ struct pipewire_context 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("UMP") != p.format.npos) + return &this->current_graph.physical_midi[nid]; } else { @@ -369,6 +371,8 @@ struct pipewire_context return &this->current_graph.software_audio[nid]; else if (p.format.find("midi") != p.format.npos) return &this->current_graph.software_midi[nid]; + else if (p.format.find("UMP") != p.format.npos) + return &this->current_graph.software_midi[nid]; } return nullptr; }; @@ -499,7 +503,8 @@ struct pipewire_filter pw.filter_destroy(this->filter); } - stdx::error create_local_port(std::string_view port_name, spa_direction direction) + stdx::error + create_local_port(std::string_view port_name, spa_direction direction, const char* format) { // clang-format off this->port = (struct port*)pw.filter_add_port( @@ -508,7 +513,7 @@ struct pipewire_filter PW_FILTER_PORT_FLAG_MAP_BUFFERS, sizeof(struct port), pw.properties_new( - PW_KEY_FORMAT_DSP, "8 bit raw midi", + PW_KEY_FORMAT_DSP, format, PW_KEY_PORT_NAME, port_name.data(), nullptr), nullptr, 0); diff --git a/include/libremidi/backends/pipewire/helpers.hpp b/include/libremidi/backends/pipewire/helpers.hpp index 5757d428..3de21c62 100644 --- a/include/libremidi/backends/pipewire/helpers.hpp +++ b/include/libremidi/backends/pipewire/helpers.hpp @@ -64,14 +64,14 @@ struct pipewire_helpers { this->filter = std::make_unique(this->global_context, configuration.filter); - pipewire_callback cbs{ + libremidi::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); + if (auto pt = p.lock()) + self.process(nf); - self.thread_lock.check_client_released(); - }}; + self.thread_lock.check_client_released(); + }}; configuration.set_process_func(cbs); } else @@ -195,7 +195,8 @@ struct pipewire_helpers } template - stdx::error create_local_port(Self& self, std::string_view portName, spa_direction direction) + stdx::error create_local_port( + Self& self, std::string_view portName, spa_direction direction, const char* format) { assert(this->global_context); assert(this->filter); @@ -205,7 +206,7 @@ struct pipewire_helpers if (!this->filter->port) { - auto ret = this->filter->create_local_port(portName.data(), direction); + auto ret = this->filter->create_local_port(portName.data(), direction, format); if (ret != stdx::error{}) { self.libremidi_handle_error(self.configuration, "error creating port"); @@ -216,11 +217,11 @@ struct pipewire_helpers return stdx::error{}; } - void add_callbacks(const observer_configuration& conf) + void add_callbacks(std::string format, 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) + global_context->on_port_added = [format, &conf](const pipewire_context::port_info& port) { + if (port.format.find(format) == std::string::npos) return; bool unfiltered = conf.track_any; @@ -241,8 +242,8 @@ struct pipewire_helpers } }; - global_context->on_port_removed = [&conf](const pipewire_context::port_info& port) { - if (port.format.find("midi") == std::string::npos) + global_context->on_port_removed = [format, &conf](const pipewire_context::port_info& port) { + if (port.format.find(format) == std::string::npos) return; bool unfiltered = conf.track_any; @@ -435,7 +436,9 @@ struct pipewire_helpers // 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 observer_configuration& conf, const pipewire_context& ctx) noexcept + static auto get_ports( + std::string_view format, const observer_configuration& conf, + const pipewire_context& ctx) noexcept -> std::vector< std::conditional_t> { @@ -450,7 +453,8 @@ struct pipewire_helpers for (auto& port : (Direction == SPA_DIRECTION_INPUT ? node.second.inputs : node.second.outputs)) { - ret.push_back(to_port_info(port)); + if (port.format.find(format) != std::string::npos) + ret.push_back(to_port_info(port)); } } @@ -460,7 +464,8 @@ struct pipewire_helpers for (auto& port : (Direction == SPA_DIRECTION_INPUT ? node.second.inputs : node.second.outputs)) { - ret.push_back(to_port_info(port)); + if (port.format.find(format) != std::string::npos) + ret.push_back(to_port_info(port)); } } } diff --git a/include/libremidi/backends/pipewire/midi_in.hpp b/include/libremidi/backends/pipewire/midi_in.hpp index a584b2d1..163ef0af 100644 --- a/include/libremidi/backends/pipewire/midi_in.hpp +++ b/include/libremidi/backends/pipewire/midi_in.hpp @@ -47,7 +47,8 @@ class midi_in_pipewire final stdx::error open_port(const input_port& in_port, std::string_view name) override { - if (auto err = create_local_port(*this, name, SPA_DIRECTION_INPUT); err != stdx::error{}) + if (auto err = create_local_port(*this, name, SPA_DIRECTION_INPUT, "8 bit raw midi"); + err != stdx::error{}) return err; if (auto err = link_ports(*this, in_port); err != stdx::error{}) @@ -59,7 +60,8 @@ class midi_in_pipewire final stdx::error open_virtual_port(std::string_view name) override { - if (auto err = create_local_port(*this, name, SPA_DIRECTION_INPUT); err != stdx::error{}) + if (auto err = create_local_port(*this, name, SPA_DIRECTION_INPUT, "8 bit raw midi"); + err != stdx::error{}) return err; start_thread(); diff --git a/include/libremidi/backends/pipewire/midi_out.hpp b/include/libremidi/backends/pipewire/midi_out.hpp index 9d33cc00..77b44f6f 100644 --- a/include/libremidi/backends/pipewire/midi_out.hpp +++ b/include/libremidi/backends/pipewire/midi_out.hpp @@ -48,7 +48,8 @@ class midi_out_pipewire stdx::error open_port(const output_port& out_port, std::string_view name) override { - if (auto err = create_local_port(*this, name, SPA_DIRECTION_OUTPUT); err != stdx::error{}) + if (auto err = create_local_port(*this, name, SPA_DIRECTION_OUTPUT, "8 bit raw midi"); + err != stdx::error{}) return err; this->filter->set_port_buffer(configuration.output_buffer_size); @@ -62,7 +63,8 @@ class midi_out_pipewire stdx::error open_virtual_port(std::string_view name) override { - if (auto err = create_local_port(*this, name, SPA_DIRECTION_OUTPUT); err != stdx::error{}) + if (auto err = create_local_port(*this, name, SPA_DIRECTION_OUTPUT, "8 bit raw midi"); + err != stdx::error{}) return err; this->filter->set_port_buffer(configuration.output_buffer_size); diff --git a/include/libremidi/backends/pipewire/observer.hpp b/include/libremidi/backends/pipewire/observer.hpp index 66eba74d..57d81279 100644 --- a/include/libremidi/backends/pipewire/observer.hpp +++ b/include/libremidi/backends/pipewire/observer.hpp @@ -24,19 +24,9 @@ class observer_pipewire final { 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->add_callbacks("midi", configuration); this->start_thread(); } @@ -56,27 +46,18 @@ class observer_pipewire final std::vector get_input_ports() const noexcept override { - return get_ports(this->configuration, *this->global_context); + return get_ports("midi", this->configuration, *this->global_context); } std::vector get_output_ports() const noexcept override { - return get_ports(this->configuration, *this->global_context); + return get_ports("midi", this->configuration, *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; diff --git a/include/libremidi/backends/pipewire_ump.hpp b/include/libremidi/backends/pipewire_ump.hpp new file mode 100644 index 00000000..3b1cb0fd --- /dev/null +++ b/include/libremidi/backends/pipewire_ump.hpp @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include + +namespace libremidi::pipewire_ump +{ +struct backend +{ + using midi_in = libremidi::pipewire_ump::midi_in_pipewire; + using midi_out = libremidi::pipewire_ump::midi_out_pipewire; + using midi_observer = libremidi::pipewire_ump::observer_pipewire; + using midi_in_configuration = libremidi::pipewire_ump::input_configuration; + using midi_out_configuration = libremidi::pipewire_ump::output_configuration; + using midi_observer_configuration = libremidi::pipewire_ump::observer_configuration; + static const constexpr auto API = libremidi::API::PIPEWIRE_UMP; + static const constexpr std::string_view name = "pipewire_ump"; + static const constexpr std::string_view display_name = "PipeWire (UMP)"; + + static inline bool available() noexcept + { + static const libpipewire& pw = libpipewire::instance(); + if (!pw.available) + return false; + const std::string_view version = pw.get_library_version(); + if (version.size() >= 3) + { + if (version[0] > '1' || version[2] >= '4') + return true; + } + return false; + } +}; +} diff --git a/include/libremidi/backends/pipewire_ump/config.hpp b/include/libremidi/backends/pipewire_ump/config.hpp new file mode 100644 index 00000000..a9dcb58b --- /dev/null +++ b/include/libremidi/backends/pipewire_ump/config.hpp @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +namespace libremidi::pipewire_ump +{ +struct 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 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 observer_configuration +{ + std::string client_name = "libremidi client"; + + pw_main_loop* context{}; +}; + +} diff --git a/include/libremidi/backends/pipewire_ump/midi_in.hpp b/include/libremidi/backends/pipewire_ump/midi_in.hpp new file mode 100644 index 00000000..129f1472 --- /dev/null +++ b/include/libremidi/backends/pipewire_ump/midi_in.hpp @@ -0,0 +1,135 @@ +#pragma once +#include +#include +#include +#include + +namespace libremidi::pipewire_ump +{ +class midi_in_pipewire final + : public midi2::in_api + , public pipewire_helpers + , public error_handler +{ +public: + struct + : ump_input_configuration + , libremidi::pipewire_ump::input_configuration + { + } configuration; + + explicit midi_in_pipewire( + ump_input_configuration&& conf, libremidi::pipewire_ump::input_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + if (auto ret = create_context(*this); ret != stdx::error{}) + { + client_open_ = ret; + return; + } + if (auto ret = create_filter(*this); ret != stdx::error{}) + { + client_open_ = ret; + return; + } + client_open_ = stdx::error{}; + } + + ~midi_in_pipewire() override + { + stop_thread(); + do_close_port(); + destroy_filter(*this); + destroy_context(); + client_open_ = std::errc::not_connected; + } + + libremidi::API get_current_api() const noexcept override { return libremidi::API::PIPEWIRE_UMP; } + + stdx::error open_port(const input_port& in_port, std::string_view name) override + { + if (auto err = create_local_port(*this, name, SPA_DIRECTION_INPUT, "32 bit raw UMP"); + err != stdx::error{}) + return err; + + if (auto err = link_ports(*this, in_port); err != stdx::error{}) + return err; + + start_thread(); + return stdx::error{}; + } + + stdx::error open_virtual_port(std::string_view name) override + { + if (auto err = create_local_port(*this, name, SPA_DIRECTION_INPUT, "32 bit raw UMP"); + err != stdx::error{}) + return err; + + start_thread(); + return stdx::error{}; + } + + stdx::error close_port() override + { + stop_thread(); + return do_close_port(); + } + + stdx::error set_port_name(std::string_view port_name) override + { + return 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_UMP) + 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( + {(uint32_t*)data, (uint32_t*)(data + size)}, + m_processing.timestamp(to_ns, c->offset)); + } + + pw.filter_queue_buffer(this->filter->port, b); + } + + midi2::input_state_machine m_processing{this->configuration}; +}; +} diff --git a/include/libremidi/backends/pipewire_ump/midi_out.hpp b/include/libremidi/backends/pipewire_ump/midi_out.hpp new file mode 100644 index 00000000..c56494e7 --- /dev/null +++ b/include/libremidi/backends/pipewire_ump/midi_out.hpp @@ -0,0 +1,185 @@ +#pragma once +#include +#include +#include + +#include + +namespace libremidi::pipewire_ump +{ +class midi_out_pipewire + : public midi2::out_api + , public pipewire_helpers + , public error_handler +{ +public: + struct + : libremidi::output_configuration + , libremidi::pipewire_ump::output_configuration + { + } configuration; + + midi_out_pipewire( + libremidi::output_configuration&& conf, + libremidi::pipewire_ump::output_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + if (auto ret = create_context(*this); ret != stdx::error{}) + { + client_open_ = ret; + return; + } + if (auto ret = create_filter(*this); ret != stdx::error{}) + { + client_open_ = ret; + return; + } + client_open_ = stdx::error{}; + } + + ~midi_out_pipewire() override + { + stop_thread(); + do_close_port(); + destroy_filter(*this); + destroy_context(); + client_open_ = std::errc::not_connected; + } + + libremidi::API get_current_api() const noexcept override { return libremidi::API::PIPEWIRE_UMP; } + + stdx::error open_port(const output_port& out_port, std::string_view name) override + { + if (auto err = create_local_port(*this, name, SPA_DIRECTION_OUTPUT, "32 bit raw UMP"); + err != stdx::error{}) + return err; + + this->filter->set_port_buffer(configuration.output_buffer_size); + + if (auto err = link_ports(*this, out_port); err != stdx::error{}) + return err; + + start_thread(); + return stdx::error{}; + } + + stdx::error open_virtual_port(std::string_view name) override + { + if (auto err = create_local_port(*this, name, SPA_DIRECTION_OUTPUT, "32 bit raw UMP"); + err != stdx::error{}) + return err; + + this->filter->set_port_buffer(configuration.output_buffer_size); + + start_thread(); + return stdx::error{}; + } + + stdx::error close_port() override + { + stop_thread(); + return do_close_port(); + } + + stdx::error set_port_name(std::string_view port_name) override + { + return 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; + + spa_pod_builder_control(&build, m.timestamp, SPA_CONTROL_UMP); + int res = spa_pod_builder_bytes(&build, m.data, cmidi2_ump_get_message_size_bytes(m.data)); + + // Try again next buffer + if (res == -ENOSPC) + break; + + // Recycle the memory + 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; + } + + stdx::error send_ump(const uint32_t* message, size_t size) override + { + // FIXME + if (size > 4) + return std::errc::message_size; + + libremidi::ump m; + std::copy_n(message, size, m.data); + m.timestamp = 0; + m_queue.enqueue(std::move(m)); + return stdx::error{}; + } + + int convert_timestamp(int64_t user) const noexcept + { + switch (configuration.timestamps) + { + case timestamp_mode::AudioFrame: + return static_cast(user); + + default: + // TODO + return 0; + } + } + + stdx::error schedule_ump(int64_t ts, const uint32_t* message, size_t size) override + { + // FIXME + if (size > 4) + return std::errc::message_size; + + libremidi::ump m; + std::copy_n(message, size, m.data); + m.timestamp = convert_timestamp(ts); + m_queue.enqueue(std::move(m)); // FIXME actually send them scheudled + return stdx::error{}; + } + + moodycamel::ReaderWriterQueue m_queue; + std::atomic_int64_t m_process_clock = 0; +}; +} diff --git a/include/libremidi/backends/pipewire_ump/observer.hpp b/include/libremidi/backends/pipewire_ump/observer.hpp new file mode 100644 index 00000000..7d52db10 --- /dev/null +++ b/include/libremidi/backends/pipewire_ump/observer.hpp @@ -0,0 +1,68 @@ +#pragma once +#include +#include +#include + +#include + +namespace libremidi::pipewire_ump +{ +class observer_pipewire final + : public observer_api + , private pipewire_helpers + , private error_handler +{ +public: + struct + : libremidi::observer_configuration + , libremidi::pipewire_ump::observer_configuration + { + } configuration; + + explicit observer_pipewire( + libremidi::observer_configuration&& conf, + libremidi::pipewire_ump::observer_configuration&& apiconf) + : configuration{std::move(conf), std::move(apiconf)} + { + create_context(*this); + + // FIXME port rename callback + { + this->add_callbacks("UMP", configuration); + this->start_thread(); + } + + 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); + } + } + + libremidi::API get_current_api() const noexcept override { return libremidi::API::PIPEWIRE_UMP; } + + std::vector get_input_ports() const noexcept override + { + return get_ports("UMP", this->configuration, *this->global_context); + } + + std::vector get_output_ports() const noexcept override + { + return get_ports("UMP", this->configuration, *this->global_context); + } + + ~observer_pipewire() + { + stop_thread(); + destroy_context(); + } + + std::unordered_set seen_input_ports; + std::unordered_set seen_output_ports; +}; +} diff --git a/include/libremidi/configurations.hpp b/include/libremidi/configurations.hpp index 75094103..6f04b7b2 100644 --- a/include/libremidi/configurations.hpp +++ b/include/libremidi/configurations.hpp @@ -9,9 +9,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -35,7 +37,7 @@ using input_api_configuration = std::variant< kbd_input_configuration, libremidi::net::dgram_input_configuration, libremidi::net_ump::dgram_input_configuration, pipewire_input_configuration, winmidi::input_configuration, winmm_input_configuration, winuwp_input_configuration, - libremidi::API>; + jack_ump::input_configuration, pipewire_ump::input_configuration, libremidi::API>; using output_api_configuration = std::variant< unspecified_configuration, dummy_configuration, alsa_raw_output_configuration, @@ -44,7 +46,8 @@ using output_api_configuration = std::variant< coremidi_ump::output_configuration, emscripten_output_configuration, jack_output_configuration, libremidi::net::dgram_output_configuration, libremidi::net_ump::dgram_output_configuration, pipewire_output_configuration, winmidi::output_configuration, winmm_output_configuration, - winuwp_output_configuration, libremidi::API>; + winuwp_output_configuration, jack_ump::output_configuration, + pipewire_ump::output_configuration, libremidi::API>; using observer_api_configuration = std::variant< unspecified_configuration, dummy_configuration, alsa_raw_observer_configuration, @@ -54,7 +57,7 @@ using observer_api_configuration = std::variant< jack_observer_configuration, libremidi::net::dgram_observer_configuration, libremidi::net_ump::dgram_observer_configuration, pipewire_observer_configuration, winmidi::observer_configuration, winmm_observer_configuration, winuwp_observer_configuration, - libremidi::API>; + jack_ump::observer_configuration, pipewire_ump::observer_configuration, libremidi::API>; LIBREMIDI_EXPORT libremidi::API midi_api(const input_api_configuration& conf);