From b9ad7697b1000d532ebba7763ed2f43636ffd72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Wed, 27 May 2026 06:39:55 -0400 Subject: [PATCH 1/2] backends: introduce shared pipewire layer Deep rewrite of the whole pipewire layer, also to enable easier sharing with audio and video users --- cmake/libremidi.examples.cmake | 2 + cmake/libremidi.sources.cmake | 15 +- examples/backends/midi1_in_pipewire.cpp | 37 +- examples/backends/shared_pipewire_context.cpp | 127 ++ examples/example_virtual_ports.cpp | 50 + examples/pipewire_share.cpp | 143 +- include/libremidi/backends/linux/pipewire.hpp | 178 --- .../backends/linux/pipewire/context.cpp | 35 + .../backends/linux/pipewire/context.hpp | 1271 +++++++++++++++++ .../backends/linux/pipewire/drm_modifiers.hpp | 35 + .../backends/linux/pipewire/filter.hpp | 350 +++++ .../backends/linux/pipewire/format.hpp | 330 +++++ .../backends/linux/pipewire/instance.cpp | 31 + .../backends/linux/pipewire/instance.hpp | 53 + .../backends/linux/pipewire/loader.hpp | 274 ++++ .../backends/linux/pipewire/stream.hpp | 234 +++ .../backends/linux/pipewire/subscription.hpp | 56 + .../backends/linux/pipewire/types.hpp | 178 +++ include/libremidi/backends/pipewire.hpp | 6 +- .../libremidi/backends/pipewire/config.hpp | 35 +- .../libremidi/backends/pipewire/context.hpp | 620 -------- .../libremidi/backends/pipewire/helpers.hpp | 607 ++++---- .../libremidi/backends/pipewire/midi_in.hpp | 13 +- .../libremidi/backends/pipewire/midi_out.hpp | 17 +- .../libremidi/backends/pipewire/observer.hpp | 48 +- .../backends/pipewire/shared_handler.hpp | 150 -- include/libremidi/backends/pipewire_ump.hpp | 8 +- .../backends/pipewire_ump/config.hpp | 20 +- .../backends/pipewire_ump/midi_in.hpp | 23 +- .../backends/pipewire_ump/midi_out.hpp | 24 +- .../backends/pipewire_ump/observer.hpp | 52 +- include/libremidi/libremidi.hpp | 2 + src/libremidi.ixx | 6 + 33 files changed, 3537 insertions(+), 1493 deletions(-) create mode 100644 examples/backends/shared_pipewire_context.cpp create mode 100644 examples/example_virtual_ports.cpp delete mode 100644 include/libremidi/backends/linux/pipewire.hpp create mode 100644 include/libremidi/backends/linux/pipewire/context.cpp create mode 100644 include/libremidi/backends/linux/pipewire/context.hpp create mode 100644 include/libremidi/backends/linux/pipewire/drm_modifiers.hpp create mode 100644 include/libremidi/backends/linux/pipewire/filter.hpp create mode 100644 include/libremidi/backends/linux/pipewire/format.hpp create mode 100644 include/libremidi/backends/linux/pipewire/instance.cpp create mode 100644 include/libremidi/backends/linux/pipewire/instance.hpp create mode 100644 include/libremidi/backends/linux/pipewire/loader.hpp create mode 100644 include/libremidi/backends/linux/pipewire/stream.hpp create mode 100644 include/libremidi/backends/linux/pipewire/subscription.hpp create mode 100644 include/libremidi/backends/linux/pipewire/types.hpp delete mode 100644 include/libremidi/backends/pipewire/context.hpp delete mode 100644 include/libremidi/backends/pipewire/shared_handler.hpp diff --git a/cmake/libremidi.examples.cmake b/cmake/libremidi.examples.cmake index f293113b..b340a408 100644 --- a/cmake/libremidi.examples.cmake +++ b/cmake/libremidi.examples.cmake @@ -77,8 +77,10 @@ endif() if(LIBREMIDI_HAS_PIPEWIRE) add_example(pipewire_share) + add_example(example_virtual_ports) add_backend_example(midi1_in_pipewire) add_backend_example(midi1_out_pipewire) + add_backend_example(shared_pipewire_context) if(LIBREMIDI_HAS_PIPEWIRE_UMP) add_backend_example(midi2_in_pipewire) diff --git a/cmake/libremidi.sources.cmake b/cmake/libremidi.sources.cmake index 90d8da21..b9893658 100644 --- a/cmake/libremidi.sources.cmake +++ b/cmake/libremidi.sources.cmake @@ -78,7 +78,15 @@ target_sources(libremidi PRIVATE include/libremidi/backends/linux/alsa.hpp include/libremidi/backends/linux/dylib_loader.hpp include/libremidi/backends/linux/helpers.hpp - include/libremidi/backends/linux/pipewire.hpp + include/libremidi/backends/linux/pipewire/context.hpp + include/libremidi/backends/linux/pipewire/drm_modifiers.hpp + include/libremidi/backends/linux/pipewire/filter.hpp + include/libremidi/backends/linux/pipewire/format.hpp + include/libremidi/backends/linux/pipewire/instance.hpp + include/libremidi/backends/linux/pipewire/loader.hpp + include/libremidi/backends/linux/pipewire/stream.hpp + include/libremidi/backends/linux/pipewire/subscription.hpp + include/libremidi/backends/linux/pipewire/types.hpp include/libremidi/backends/linux/udev.hpp include/libremidi/backends/net/config.hpp @@ -88,12 +96,10 @@ target_sources(libremidi PRIVATE include/libremidi/backends/net/observer.hpp include/libremidi/backends/pipewire/config.hpp - include/libremidi/backends/pipewire/context.hpp include/libremidi/backends/pipewire/helpers.hpp include/libremidi/backends/pipewire/midi_in.hpp include/libremidi/backends/pipewire/midi_out.hpp include/libremidi/backends/pipewire/observer.hpp - include/libremidi/backends/pipewire/shared_handler.hpp include/libremidi/backends/pipewire.hpp include/libremidi/backends/pipewire_ump.hpp @@ -164,6 +170,9 @@ if(NOT LIBREMIDI_HEADER_ONLY AND NOT LIBREMIDI_MODULE_BUILD) include/libremidi/backends/emscripten/midi_out.cpp include/libremidi/backends/emscripten/observer.cpp + include/libremidi/backends/linux/pipewire/instance.cpp + include/libremidi/backends/linux/pipewire/context.cpp + include/libremidi/libremidi-c.cpp ) endif() diff --git a/examples/backends/midi1_in_pipewire.cpp b/examples/backends/midi1_in_pipewire.cpp index 3584ff35..3ddf664f 100644 --- a/examples/backends/midi1_in_pipewire.cpp +++ b/examples/backends/midi1_in_pipewire.cpp @@ -8,31 +8,28 @@ using api = libremidi::pipewire::backend; int main(void) try { - for (int i = 0; i < 100; i++) - { - std::cerr << "API: " << api::name << "\n"; + 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}; + 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_input_ports(); - for (const auto& port : ports) - { - std::cerr << "Port: " << port.display_name << "\n"; - } + auto ports = obs.get_input_ports(); + for (const auto& port : ports) + { + std::cerr << "Port: " << port.display_name << "\n"; + } - { - libremidi::input_configuration in_config; - in_config.timestamps = libremidi::timestamp_mode::Relative; - in_config.on_message = [](const libremidi::message& m) { std::cerr << m << "\n"; }; - api::midi_in_configuration in_api_config; - libremidi::midi_in midiin{in_config, in_api_config}; + { + libremidi::input_configuration in_config; + in_config.timestamps = libremidi::timestamp_mode::Relative; + in_config.on_message = [](const libremidi::message& m) { std::cerr << m << "\n"; }; + api::midi_in_configuration in_api_config; + libremidi::midi_in midiin{in_config, in_api_config}; - midiin.open_port(ports[1]); + midiin.open_port(ports[2]); - //std::this_thread::sleep_for(std::chrono::seconds(60)); - } + std::this_thread::sleep_for(std::chrono::seconds(5)); } return 0; } diff --git a/examples/backends/shared_pipewire_context.cpp b/examples/backends/shared_pipewire_context.cpp new file mode 100644 index 00000000..e7235f39 --- /dev/null +++ b/examples/backends/shared_pipewire_context.cpp @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: BSL-1.0 +// +// Connect to the pipewire daemon, print the initial graph, then watch +// live additions / removals for --duration seconds. + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace lpw = libremidi::pipewire; + +static const char* kind_name(lpw::media_class k) +{ + switch (k) + { + case lpw::media_class::audio: + return "audio"; + case lpw::media_class::midi: + return "midi"; + case lpw::media_class::ump: + return "ump"; + case lpw::media_class::video: + return "video"; + default: + return "other"; + } +} + +static void print_node(const lpw::node_info& n) +{ + std::printf( + " node id=%u kind=%-5s name=%s%s%s\n", n.id, kind_name(n.kind), + n.name.c_str(), n.description.empty() ? "" : " desc=", + n.description.c_str()); +} + +int main(int argc, char** argv) +{ + int seconds = 5; + for (int i = 1; i < argc - 1; ++i) + { + if (std::string{argv[i]} == "--duration") + seconds = std::atoi(argv[i + 1]); + } + + auto& pw = lpw::load(); + if (!pw.available) + { + std::fprintf( + stderr, + "libpipewire-0.3 not available at runtime; aborting.\n" + "(Install pipewire and re-run; the executable itself has no\n" + " DT_NEEDED for libpipewire, so it can start anywhere.)\n"); + return 1; + } + std::printf( + "loaded libpipewire-0.3 (%s)\n", + pw.get_library_version ? pw.get_library_version() : "version unknown"); + std::printf( + " core_available=%d thread_available=%d stream_available=%d " + "filter_available=%d\n", + pw.core_available, pw.thread_available, pw.stream_available, + pw.filter_available); + + auto ctx = lpw::shared_context(); + if (!ctx) + { + std::fprintf(stderr, "shared_context() returned empty\n"); + return 2; + } + if (!ctx->ok()) + { + std::fprintf( + stderr, + "context not in connected state (state=%d) — daemon down?\n", + static_cast(ctx->state())); + return 3; + } + std::printf("connected to pipewire daemon\n"); + + // Subscribe before snapshot or we miss late globals. + auto sub_state = ctx->on_state_changed([](lpw::connection_state s) { + const char* names[] = {"connecting", "connected", "broken", + "disconnected"}; + std::printf( + "[state] -> %s\n", + names[static_cast(s)]); + }); + auto sub_add = ctx->on_node_added([](const lpw::node_info& n) { + std::printf("[+node] id=%u %s (%s)\n", n.id, n.name.c_str(), + kind_name(n.kind)); + }); + auto sub_rm = ctx->on_node_removed([](std::uint32_t id) { + std::printf("[-node] id=%u\n", id); + }); + + auto snap = ctx->snapshot(); + std::printf("\n== Initial graph snapshot (%zu nodes) ==\n", snap.nodes.size()); + for (auto k : {lpw::media_class::audio, lpw::media_class::midi, + lpw::media_class::ump, lpw::media_class::video, + lpw::media_class::other}) + { + auto subset = snap.nodes_of(k); + if (subset.empty()) + continue; + std::printf("[%s] %zu nodes\n", kind_name(k), subset.size()); + for (const auto& n : subset) + print_node(n); + } + + std::printf("\n== Watching for live changes for %d seconds ==\n", seconds); + std::this_thread::sleep_for(std::chrono::seconds(seconds)); + + std::printf("\nclean shutdown\n"); + return 0; +} diff --git a/examples/example_virtual_ports.cpp b/examples/example_virtual_ports.cpp new file mode 100644 index 00000000..439b4a9c --- /dev/null +++ b/examples/example_virtual_ports.cpp @@ -0,0 +1,50 @@ +#include +#include "utils.hpp" +#include +#include +#include + +int main() +{ + try { + auto in_conf = libremidi::midi1::in_default_configuration(); + auto out_conf = libremidi::midi1::out_default_configuration(); + + libremidi::midi_in midi_in{ + { + .on_message = [](const libremidi::message& message) { + std::cout << "Received: " << message << std::endl; + } + }, + in_conf + }; + + libremidi::midi_out midi_out{ + libremidi::output_configuration{}, + out_conf + }; + + midi_in.open_virtual_port("my in"); + midi_out.open_virtual_port("my out"); + + std::cout << "MIDI virtual ports created:\n"; + std::cout << "Client name: my app\n"; + std::cout << "Input port: my in\n"; + std::cout << "Output port: my out\n"; + std::cout << "Press Ctrl+C to exit...\n"; + + libremidi::message test_note{0x90, 60, 127}; + midi_out.send_message(test_note); + std::cout << "Sent test note: " << test_note << std::endl; + + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} diff --git a/examples/pipewire_share.cpp b/examples/pipewire_share.cpp index 80968db2..812d9f04 100644 --- a/examples/pipewire_share.cpp +++ b/examples/pipewire_share.cpp @@ -1,92 +1,71 @@ +// Embed libremidi MIDI in a host that owns its pipewire setup; the +// host passes raw pointers via the borrow fields and libremidi +// adopts them without taking ownership. + #include "utils.hpp" -#include +#include #include -#include #include -#include +#include +#include +#include +#include #include #include -auto& pw = libremidi::libpipewire::instance(); +auto& pw = libremidi::pipewire::load(); struct my_app { - libremidi::unique_handle handle; - pw_filter* filter{}; + pw_thread_loop* tl{}; + pw_context* ctx{}; + pw_core* core{}; std::optional observer; - std::vector midiin; std::vector midiout; - std::vector midiin_callbacks; - std::vector midiout_callbacks; - my_app() { - auto callback = [&](int port, const libremidi::message& message) { - std::cerr << port << ": " << message << std::endl; + tl = pw.thread_loop_new("my-app", nullptr); + if (!tl) + throw std::runtime_error("thread_loop_new failed"); + + auto* lp = pw.thread_loop_get_loop(tl); + ctx = pw.context_new(lp, nullptr, 0); + if (!ctx) + throw std::runtime_error("context_new failed"); + core = pw.context_connect(ctx, nullptr, 0); + if (!core) + throw std::runtime_error("context_connect failed"); + + if (pw.thread_loop_start(tl) < 0) + throw std::runtime_error("thread_loop_start failed"); + + auto callback = [&](int port, const libremidi::message& message) { + std::cerr << port << ": " << message << '\n'; midiout[port].send_message(message); }; - // Create a PipeWire main loop which will be shared across objects - handle.reset(pw.main_loop_new(nullptr)); - if (!handle) - throw std::runtime_error("Could not initialize PipeWire"); + libremidi::pipewire_observer_configuration api_observer_config{}; + api_observer_config.thread_loop = tl; + api_observer_config.core = core; - auto main_loop = handle.get(); - // Create a PipeWire filter - { - // clang-format off -#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 = &my_app::pipewire_callback}; -#pragma GCC diagnostic pop - - filter = pw.filter_new_simple( - pw.main_loop_get_loop(main_loop), - "My filter", - pw.properties_new( - PW_KEY_MEDIA_TYPE, "Midi", - PW_KEY_MEDIA_CATEGORY, "Filter", - PW_KEY_MEDIA_ROLE, "DSP", - PW_KEY_MEDIA_NAME, "libremidi", - PW_KEY_NODE_ALWAYS_PROCESS, "true", - PW_KEY_NODE_PAUSE_ON_IDLE, "false", - nullptr), - &filter_events, - this); - // clang-format on - if (pw.filter_connect(this->filter, PW_FILTER_FLAG_RT_PROCESS, NULL, 0) < 0) - { - throw std::runtime_error("Could not connect PipeWire filter"); - } - } + observer + = libremidi::observer{libremidi::observer_configuration{}, api_observer_config}; + + libremidi::pipewire_input_configuration api_input_config{}; + api_input_config.thread_loop = tl; + api_input_config.core = core; + + libremidi::pipewire_output_configuration api_output_config{}; + api_output_config.thread_loop = tl; + api_output_config.core = core; - // Create an observer - observer = libremidi::observer{ - libremidi::observer_configuration{}, - libremidi::pipewire_observer_configuration{.context = handle.get()}}; - - // Create our configuration - auto api_input_config = libremidi::pipewire_input_configuration{ - .context = main_loop, - .filter = filter, - .set_process_func - = [this](libremidi::pipewire_callback cb) { midiin_callbacks.push_back(std::move(cb)); }}; - auto api_output_config = libremidi::pipewire_output_configuration{ - .context = main_loop, - .filter = filter, - .set_process_func - = [this](libremidi::pipewire_callback cb) { midiout_callbacks.push_back(std::move(cb)); }}; - - // Create 16 inputs and 16 outputs for (int i = 0; i < 16; i++) { midiin.emplace_back( @@ -100,23 +79,25 @@ struct my_app } } - ~my_app() { pw.filter_destroy(filter); } - - void run() { pw.main_loop_run(handle.get()); } - - static void pipewire_callback(void* ctx, spa_io_position* pos) + ~my_app() { - auto& self = *(my_app*)ctx; - - // Process the midi inputs - for (auto& cb : self.midiin_callbacks) - cb.callback(pos); - - // Do some other things + midiin.clear(); + midiout.clear(); + observer.reset(); + + if (tl) + pw.thread_loop_stop(tl); + if (core) + pw.core_disconnect(core); + if (ctx) + pw.context_destroy(ctx); + if (tl) + pw.thread_loop_destroy(tl); + } - // Process the midi outputs - for (auto& cb : self.midiout_callbacks) - cb.callback(pos); + void run() + { + std::this_thread::sleep_for(std::chrono::seconds(60)); } }; @@ -126,11 +107,11 @@ int main(int argc, char** argv) try { my_app app{}; - app.run(); } - catch (...) + catch (const std::exception& e) { + std::cerr << "error: " << e.what() << "\n"; } pw.deinit(); } diff --git a/include/libremidi/backends/linux/pipewire.hpp b/include/libremidi/backends/linux/pipewire.hpp deleted file mode 100644 index 171e20ce..00000000 --- a/include/libremidi/backends/linux/pipewire.hpp +++ /dev/null @@ -1,178 +0,0 @@ -#pragma once - -#include - -#include - -NAMESPACE_LIBREMIDI -{ - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -class libpipewire -{ -public: - decltype(&::pw_get_library_version) get_library_version{}; - - 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 m_library; - - libpipewire() - : m_library("libpipewire-0.3.so.0") - { - if (!m_library) - { - available = false; - return; - } - - // 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"); - - context_new = m_library.symbol("pw_context_new"); - context_connect = m_library.symbol("pw_context_connect"); - context_destroy = m_library.symbol("pw_context_destroy"); - - core_disconnect = m_library.symbol("pw_core_disconnect"); - - proxy_add_listener - = m_library.symbol("pw_proxy_add_listener"); - proxy_destroy = m_library.symbol("pw_proxy_destroy"); - - main_loop_new = m_library.symbol("pw_main_loop_new"); - main_loop_destroy - = m_library.symbol("pw_main_loop_destroy"); - main_loop_quit = m_library.symbol("pw_main_loop_quit"); - main_loop_run = m_library.symbol("pw_main_loop_run"); - main_loop_get_loop - = m_library.symbol("pw_main_loop_get_loop"); - - properties_new = m_library.symbol("pw_properties_new"); - properties_free = m_library.symbol("pw_properties_free"); - properties_get = m_library.symbol("pw_properties_get"); - - filter_new_simple - = m_library.symbol("pw_filter_new_simple"); - filter_get_node_id - = m_library.symbol("pw_filter_get_node_id"); - filter_get_properties - = m_library.symbol("pw_filter_get_properties"); - filter_add_port = m_library.symbol("pw_filter_add_port"); - filter_remove_port - = m_library.symbol("pw_filter_remove_port"); - filter_update_properties = m_library.symbol( - "pw_filter_update_properties"); - filter_update_params - = m_library.symbol("pw_filter_update_params"); - filter_get_time = m_library.symbol("pw_filter_get_time"); - filter_destroy = m_library.symbol("pw_filter_destroy"); - filter_connect = m_library.symbol("pw_filter_connect"); - filter_get_dsp_buffer - = m_library.symbol("pw_filter_get_dsp_buffer"); - filter_dequeue_buffer - = m_library.symbol("pw_filter_dequeue_buffer"); - filter_queue_buffer - = m_library.symbol("pw_filter_queue_buffer"); - filter_flush = m_library.symbol("pw_filter_flush"); - - assert(get_library_version); - - 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/pipewire/context.cpp b/include/libremidi/backends/linux/pipewire/context.cpp new file mode 100644 index 00000000..22a2dadd --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/context.cpp @@ -0,0 +1,35 @@ +#if defined(LIBREMIDI_PIPEWIRE) + + #if !defined(LIBREMIDI_HEADER_ONLY) + #include + #endif + + #include + +namespace libremidi::pipewire +{ + +LIBREMIDI_INLINE +std::shared_ptr shared_context(context::config cfg) noexcept +{ + static std::mutex mtx; + static std::weak_ptr weak; + + std::lock_guard lock{mtx}; + if (auto p = weak.lock()) + return p; + + auto inst = shared_instance(); + if (!inst) + return {}; + + auto ctx = context::make(std::move(inst), cfg); + if (!ctx) + return {}; + weak = ctx; + return ctx; +} + +} + +#endif diff --git a/include/libremidi/backends/linux/pipewire/context.hpp b/include/libremidi/backends/linux/pipewire/context.hpp new file mode 100644 index 00000000..dfdeec63 --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/context.hpp @@ -0,0 +1,1271 @@ +#pragma once + +#if defined(__clang__) + #pragma clang diagnostic push + // pw_loop_invoke macro expands to spa_callbacks_call_res which + // compares uint32_t version against int literal 0. + #pragma clang diagnostic ignored "-Wsign-compare" +#endif + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace libremidi::pipewire +{ + +enum class loop_kind : std::uint8_t +{ + thread, + main, +}; + +struct context_config +{ + loop_kind kind{loop_kind::thread}; + const char* loop_name{"libremidi-pw"}; + + std::chrono::milliseconds sync_deadline{std::chrono::seconds{2}}; + + // Reconnect path stops the thread loop from inside that thread + // — UB per pipewire docs. Off until reworked. + bool auto_reconnect{false}; + + std::chrono::milliseconds reconnect_backoff{std::chrono::seconds{1}}; + std::chrono::milliseconds reconnect_backoff_max{std::chrono::seconds{30}}; + + int fd{-1}; + + // Non-null pointers here are adopted without ownership; prefer the + // `borrow()` factories rather than setting these directly. + pw_thread_loop* borrow_thread_loop{}; + pw_main_loop* borrow_main_loop{}; + pw_core* borrow_core{}; +}; + +class context : public std::enable_shared_from_this +{ +public: + using config = context_config; + + context(std::shared_ptr inst, config cfg = {}) + : m_instance{std::move(inst)} + , m_cfg{cfg} + { + if (m_cfg.borrow_thread_loop) + { + m_cfg.kind = loop_kind::thread; + m_owns_loop = false; + } + else if (m_cfg.borrow_main_loop) + { + m_cfg.kind = loop_kind::main; + m_owns_loop = false; + } + if (m_cfg.borrow_core) + m_owns_core = false; + + if (!m_instance || !m_instance->initialized()) + { + m_state.store(connection_state::broken, std::memory_order_release); + return; + } + + if (!build_connection()) + m_state.store(connection_state::broken, std::memory_order_release); + + if (m_cfg.auto_reconnect) + start_supervisor(); + } + + ~context() + { + stop_supervisor(); + tear_down(true); + } + + context(const context&) = delete; + context& operator=(const context&) = delete; + context(context&&) = delete; + context& operator=(context&&) = delete; + + static std::shared_ptr make(std::shared_ptr inst, config cfg = {}) + { + auto p = std::make_shared(std::move(inst), cfg); + if (p->state() != connection_state::broken) + { + // Pass 1 drives the daemon's initial registry dump (binds the + // proxies + installs listeners); pass 2 drains the resulting + // info events so port.physical etc. is populated before return. + (void)p->synchronize(); + (void)p->synchronize(); + } + return p; + } + + static std::shared_ptr + borrow(pw_thread_loop* tl, pw_core* core, config cfg = {}) + { + if (!tl || !core) + return {}; + cfg.borrow_thread_loop = tl; + cfg.borrow_core = core; + auto inst = shared_instance(); + if (!inst) + return {}; + auto p = std::make_shared(std::move(inst), cfg); + if (p->state() != connection_state::broken) + { + (void)p->synchronize(); + (void)p->synchronize(); + } + return p; + } + + static std::shared_ptr + borrow(pw_main_loop* ml, pw_core* core, config cfg = {}) + { + if (!ml || !core) + return {}; + cfg.borrow_main_loop = ml; + cfg.borrow_core = core; + auto inst = shared_instance(); + if (!inst) + return {}; + auto p = std::make_shared(std::move(inst), cfg); + if (p->state() != connection_state::broken) + { + (void)p->synchronize(); + (void)p->synchronize(); + } + return p; + } + + bool ok() const noexcept { return state() == connection_state::connected; } + + connection_state state() const noexcept { return m_state.load(std::memory_order_acquire); } + + std::uint32_t generation() const noexcept + { + return m_generation.load(std::memory_order_acquire); + } + + bool synchronize() { return synchronize(m_cfg.sync_deadline); } + + bool synchronize(std::chrono::milliseconds deadline) + { + if (state() == connection_state::broken) + return false; + + auto& pw = load(); + if (!pw.core_available || !m_core || !m_loop) + return false; + + using clk = std::chrono::steady_clock; + const auto t_end = clk::now() + deadline; + + if (m_cfg.kind == loop_kind::thread) + { + pw.thread_loop_lock(m_thread_loop); + struct unlock_guard + { + const api& pw; + pw_thread_loop* loop; + ~unlock_guard() { pw.thread_loop_unlock(loop); } + } guard{pw, m_thread_loop}; + return do_sync_locked(t_end); + } + else + { + return do_sync_locked(t_end); + } + } + + bool reconnect() + { + invoke_sync([this] { + tear_down(/*final=*/false); + m_state.store(connection_state::connecting, std::memory_order_release); + if (!build_connection()) + m_state.store(connection_state::broken, std::memory_order_release); + }); + if (!synchronize()) + return false; + m_state.store(connection_state::connected, std::memory_order_release); + m_generation.fetch_add(1, std::memory_order_release); + dispatch_state(connection_state::connected); + return true; + } + bool is_in_loop_thread() const noexcept + { + if (m_cfg.kind != loop_kind::thread || !m_thread_loop) + return false; + auto& pw = load(); + return pw.thread_loop_in_thread && pw.thread_loop_in_thread(m_thread_loop); + } + + template + void with_lock(F&& fn) + { + if (m_cfg.kind != loop_kind::thread || !m_thread_loop || is_in_loop_thread()) + { + std::forward(fn)(); + return; + } + auto& pw = load(); + pw.thread_loop_lock(m_thread_loop); + struct unlock_guard + { + const api& pw; + pw_thread_loop* loop; + ~unlock_guard() { pw.thread_loop_unlock(loop); } + } guard{pw, m_thread_loop}; + std::forward(fn)(); + } + + template + void invoke_async(F&& fn) + { + if (!m_loop) + return; + if (is_in_loop_thread()) + { + std::forward(fn)(); + return; + } + using FR = std::remove_cvref_t; + auto* held = new FR{std::forward(fn)}; + constexpr auto trampoline + = +[](spa_loop* /*l*/, bool /*async*/, std::uint32_t /*seq*/, const void* /*data*/, + size_t /*size*/, void* user_data) noexcept -> int { + auto* p = static_cast(user_data); + try + { + (*p)(); + } + catch (...) + { + } + delete p; + return 0; + }; + pw_loop_invoke(m_loop, trampoline, 0, nullptr, 0, /*block=*/false, held); + } + + template + auto invoke_sync(F&& fn) -> std::invoke_result_t&> + { + using FR = std::remove_cvref_t; + using R = std::invoke_result_t; + if (!m_loop || is_in_loop_thread() || m_cfg.kind != loop_kind::thread) + { + return std::forward(fn)(); + } + if constexpr (std::is_void_v) + { + FR f{std::forward(fn)}; + constexpr auto trampoline = +[](spa_loop*, bool, std::uint32_t, const void*, size_t, + void* user_data) noexcept -> int { + try + { + (*static_cast(user_data))(); + } + catch (...) + { + } + return 0; + }; + pw_loop_invoke(m_loop, trampoline, 0, nullptr, 0, /*block=*/true, &f); + } + else + { + struct payload + { + FR fn; + std::optional result; + }; + payload p{std::forward(fn), std::nullopt}; + constexpr auto trampoline = +[](spa_loop*, bool, std::uint32_t, const void*, size_t, + void* user_data) noexcept -> int { + auto& pl = *static_cast(user_data); + try + { + pl.result.emplace(pl.fn()); + } + catch (...) + { + } + return 0; + }; + pw_loop_invoke(m_loop, trampoline, 0, nullptr, 0, /*block=*/true, &p); + return std::move(*p.result); + } + } + + graph_snapshot snapshot() const + { + graph_snapshot out; + std::lock_guard lock{m_graph_mtx}; + out.nodes.reserve(m_nodes.size()); + for (const auto& [id, n] : m_nodes) + out.nodes.push_back(n); + return out; + } + + [[nodiscard]] subscription on_state_changed(std::function fn) + { + return add_subscriber(m_subs_state, std::move(fn)); + } + + // Live events only; observer's notify_in_constructor handles the + // initial walk (matches alsa_seq / jack convention). + [[nodiscard]] subscription on_node_added(std::function fn) + { + return add_subscriber(m_subs_node_added, std::move(fn)); + } + + [[nodiscard]] subscription on_node_removed(std::function fn) + { + return add_subscriber(m_subs_node_removed, std::move(fn)); + } + + [[nodiscard]] subscription on_port_added(std::function fn) + { + return add_subscriber(m_subs_port_added, std::move(fn)); + } + + [[nodiscard]] subscription on_port_removed(std::function fn) + { + return add_subscriber(m_subs_port_removed, std::move(fn)); + } + + void unsubscribe(std::uint64_t id) noexcept + { + // Synchronous so a caller dropping its subscription can safely + // destroy state captured by the subscriber lambda. + invoke_sync([this, id] { + remove_subscriber(m_subs_state, id); + remove_subscriber(m_subs_node_added, id); + remove_subscriber(m_subs_node_removed, id); + remove_subscriber(m_subs_port_added, id); + remove_subscriber(m_subs_port_removed, id); + }); + } + + pw_thread_loop* thread_loop_handle() noexcept { return m_thread_loop; } + pw_main_loop* main_loop_handle() noexcept { return m_main_loop; } + pw_loop* bare_loop() noexcept { return m_loop; } + pw_context* pw_context_ptr() noexcept { return m_context; } + pw_core* pw_core_ptr() noexcept { return m_core; } + pw_registry* registry() noexcept { return m_registry; } + + int get_fd() const noexcept + { + if (m_cfg.kind != loop_kind::main || !m_loop) + return -1; + return pw_loop_get_fd(m_loop); + } + + int iterate(int timeout_ms) noexcept + { + if (m_cfg.kind != loop_kind::main || !m_loop) + return -EINVAL; + return pw_loop_iterate(m_loop, timeout_ms); + } + +private: + std::shared_ptr m_instance; + config m_cfg; + + // Exactly one of m_main_loop / m_thread_loop is non-null. + pw_main_loop* m_main_loop{}; + pw_thread_loop* m_thread_loop{}; + pw_loop* m_loop{}; + + pw_context* m_context{}; + pw_core* m_core{}; + pw_registry* m_registry{}; + spa_hook m_registry_listener{}; + spa_hook m_core_listener{}; + + // WirePlumber default-target metadata: JSON props + // `default.audio.sink` / `default.audio.source`. Strings guarded by m_graph_mtx. + pw_proxy* m_default_metadata_proxy{}; + spa_hook m_default_metadata_listener{}; + std::string m_default_sink_name; + std::string m_default_source_name; + + std::atomic m_sync_seq{0}; + std::atomic m_sync_done{0}; + std::atomic m_sync_error{0}; + + // FIXME lock-free? + mutable std::mutex m_graph_mtx; + + struct bound_node + { + pw_proxy* proxy{}; + std::unique_ptr hook; + bool info_seen{}; + bool emitted_added{}; + node_info info; + }; + struct bound_port + { + pw_proxy* proxy{}; + std::unique_ptr hook; + bool info_seen{}; + bool emitted_added{}; + port_info info; + }; + + std::vector> m_bound_nodes; + std::vector> m_bound_ports; + + std::unordered_map m_nodes; + + // False when the corresponding pointer is adopted via cfg.borrow_*. + bool m_owns_loop{true}; + bool m_owns_core{true}; + + std::atomic m_state{connection_state::connecting}; + std::atomic m_generation{0}; + + std::thread m_supervisor; + std::mutex m_supervisor_mtx; + std::condition_variable m_supervisor_cv; + std::atomic_bool m_supervisor_stop{false}; + + template + struct subscriber_slot + { + std::uint64_t id{}; + Fn fn; + }; + template + struct subscriber_list + { + std::vector> list; + }; + + subscriber_list> m_subs_state; + subscriber_list> m_subs_node_added; + subscriber_list> m_subs_node_removed; + subscriber_list> m_subs_port_added; + subscriber_list> m_subs_port_removed; + + std::atomic m_next_sub_id{1}; + + template + subscription add_subscriber(subscriber_list& list, Fn fn) + { + std::uint64_t id = m_next_sub_id.fetch_add(1, std::memory_order_relaxed); + // Under loop lock to serialize with dispatch. + with_lock([&] { list.list.push_back({id, std::move(fn)}); }); + return subscription{weak_from_this(), id}; + } + + template + void remove_subscriber(subscriber_list& list, std::uint64_t id) noexcept + { + auto it = std::find_if( + list.list.begin(), list.list.end(), [id](const auto& s) { return s.id == id; }); + if (it != list.list.end()) + list.list.erase(it); + } + + void dispatch_state(connection_state s) + { + for (const auto& sub : m_subs_state.list) + if (sub.fn) + sub.fn(s); + + notify_supervisor(); + } + void dispatch_node_added(const node_info& n) + { + for (const auto& sub : m_subs_node_added.list) + if (sub.fn) + sub.fn(n); + } + void dispatch_node_removed(std::uint32_t id) + { + for (const auto& sub : m_subs_node_removed.list) + if (sub.fn) + sub.fn(id); + } + void dispatch_port_added(const port_info& p) + { + for (const auto& sub : m_subs_port_added.list) + if (sub.fn) + sub.fn(p); + } + void dispatch_port_removed(std::uint32_t id) + { + for (const auto& sub : m_subs_port_removed.list) + if (sub.fn) + sub.fn(id); + } + + bool build_connection() + { + auto& pw = load(); + if (!pw.core_available) + return false; + if (m_cfg.kind == loop_kind::thread && !pw.thread_available) + return false; + + if (m_cfg.kind == loop_kind::thread) + { + if (m_cfg.borrow_thread_loop) + { + m_thread_loop = m_cfg.borrow_thread_loop; + } + else + { + m_thread_loop = pw.thread_loop_new(m_cfg.loop_name, nullptr); + if (!m_thread_loop) + return false; + } + m_loop = pw.thread_loop_get_loop(m_thread_loop); + } + else + { + if (m_cfg.borrow_main_loop) + { + m_main_loop = m_cfg.borrow_main_loop; + } + else + { + m_main_loop = pw.main_loop_new(nullptr); + if (!m_main_loop) + return false; + } + m_loop = pw.main_loop_get_loop(m_main_loop); + } + if (!m_loop) + return false; + + if (m_cfg.borrow_core) + { + m_core = m_cfg.borrow_core; + } + else + { + m_context = pw.context_new(m_loop, nullptr, 0); + if (!m_context) + return false; + + if (m_cfg.fd != -1) + { + // fd ownership transfers to pipewire (closed in pw_core_disconnect). + m_core = pw.context_connect_fd(m_context, m_cfg.fd, nullptr, 0); + m_cfg.fd = -1; + } + else + { + m_core = pw.context_connect(m_context, nullptr, 0); + } + if (!m_core) + return false; + } + + // Borrowed thread loop is already running — the worker iterates + // listener lists we are about to mutate, so take the lock. + const bool need_lock + = (m_cfg.kind == loop_kind::thread) && m_thread_loop && !m_owns_loop; + if (need_lock) + pw.thread_loop_lock(m_thread_loop); + struct unlock_guard + { + const api& pw; + pw_thread_loop* tl; + bool active; + ~unlock_guard() + { + if (active) + pw.thread_loop_unlock(tl); + } + } guard{pw, m_thread_loop, need_lock}; + + install_core_listener(); + + m_registry = pw_core_get_registry(m_core, PW_VERSION_REGISTRY, 0); + if (!m_registry) + return false; + + install_registry_listener(); + + // Start *after* listeners are installed or we race the event burst. + if (m_cfg.kind == loop_kind::thread && m_owns_loop) + { + if (pw.thread_loop_start(m_thread_loop) < 0) + return false; + } + + m_state.store(connection_state::connecting, std::memory_order_release); + return true; + } + + void start_supervisor() + { + m_supervisor = std::thread{[this] { supervisor_main(); }}; + } + + void stop_supervisor() noexcept + { + { + std::lock_guard lk{m_supervisor_mtx}; + m_supervisor_stop.store(true, std::memory_order_release); + } + m_supervisor_cv.notify_all(); + if (m_supervisor.joinable()) + m_supervisor.join(); + } + + void supervisor_main() + { + auto backoff = m_cfg.reconnect_backoff; + std::unique_lock lk{m_supervisor_mtx}; + while (!m_supervisor_stop.load(std::memory_order_acquire)) + { + m_supervisor_cv.wait_for( + lk, backoff, [this] { return m_supervisor_stop.load(std::memory_order_acquire); }); + if (m_supervisor_stop.load(std::memory_order_acquire)) + return; + + const auto s = m_state.load(std::memory_order_acquire); + if (s == connection_state::broken) + { + lk.unlock(); + bool ok = false; + try + { + ok = reconnect(); + } + catch (...) + { + ok = false; + } + lk.lock(); + if (m_supervisor_stop.load(std::memory_order_acquire)) + return; + if (ok) + { + backoff = m_cfg.reconnect_backoff; + } + else + { + auto next = backoff * 2; + if (next > m_cfg.reconnect_backoff_max) + next = m_cfg.reconnect_backoff_max; + backoff = next; + } + } + else + { + backoff = m_cfg.reconnect_backoff; + } + } + } + + void notify_supervisor() noexcept + { + if (!m_cfg.auto_reconnect) + return; + m_supervisor_cv.notify_all(); + } + + void tear_down(bool final) noexcept + { + auto& pw = load(); + + if (m_thread_loop && pw.thread_loop_stop && m_owns_loop) + pw.thread_loop_stop(m_thread_loop); + + { + // Loop stopped: no synchronisation needed. + for (auto& [id, b] : m_bound_nodes) + { + if (b.hook) + spa_hook_remove(b.hook.get()); + if (b.proxy && pw.proxy_destroy) + pw.proxy_destroy(b.proxy); + } + m_bound_nodes.clear(); + for (auto& [id, b] : m_bound_ports) + { + if (b.hook) + spa_hook_remove(b.hook.get()); + if (b.proxy && pw.proxy_destroy) + pw.proxy_destroy(b.proxy); + } + m_bound_ports.clear(); + std::lock_guard lock{m_graph_mtx}; + m_nodes.clear(); + } + + spa_hook_remove(&m_core_listener); + spa_hook_remove(&m_registry_listener); + if (m_default_metadata_proxy) + { + spa_hook_remove(&m_default_metadata_listener); + if (pw.proxy_destroy) + pw.proxy_destroy(m_default_metadata_proxy); + m_default_metadata_proxy = nullptr; + } + if (m_registry && pw.proxy_destroy) + pw.proxy_destroy(reinterpret_cast(m_registry)); + m_registry = nullptr; + + if (m_core && pw.core_disconnect && m_owns_core) + pw.core_disconnect(m_core); + m_core = nullptr; + + if (m_context && pw.context_destroy) + pw.context_destroy(m_context); + m_context = nullptr; + + if (m_thread_loop && pw.thread_loop_destroy && m_owns_loop) + pw.thread_loop_destroy(m_thread_loop); + m_thread_loop = nullptr; + + if (m_main_loop && pw.main_loop_destroy && m_owns_loop) + pw.main_loop_destroy(m_main_loop); + m_main_loop = nullptr; + + m_loop = nullptr; + + if (!final) + { + // Subscriber lists survive reconnect. + m_sync_seq.store(0, std::memory_order_relaxed); + m_sync_done.store(0, std::memory_order_relaxed); + m_sync_error.store(0, std::memory_order_relaxed); + } + else + { + m_state.store(connection_state::disconnected, std::memory_order_release); + } + } + + static void on_core_info(void* /*data*/, const pw_core_info* /*info*/) noexcept { } + + static void on_core_done(void* data, std::uint32_t id, int seq) noexcept + { + auto* self = static_cast(data); + if (id == PW_ID_CORE && seq == self->m_sync_seq.load(std::memory_order_acquire)) + self->m_sync_done.store(1, std::memory_order_release); + } + + static void on_core_error( + void* data, std::uint32_t /*id*/, int /*seq*/, int res, const char* /*message*/) noexcept + { + auto* self = static_cast(data); + self->m_sync_error.store(res, std::memory_order_release); + if (res == -EPIPE || res == -ECONNRESET || res == -ENOENT || res == -ENOTCONN) + { + self->m_state.store(connection_state::broken, std::memory_order_release); + } + } + + void install_core_listener() noexcept + { + spa_zero(m_core_listener); + static constexpr pw_core_events events = { + .version = PW_VERSION_CORE_EVENTS, + .info = &on_core_info, + .done = &on_core_done, + .ping = nullptr, + .error = &on_core_error, + .remove_id = nullptr, + .bound_id = nullptr, + .add_mem = nullptr, + .remove_mem = nullptr, +#if PW_VERSION_CORE_EVENTS >= 1 + .bound_props = nullptr, +#endif + }; + pw_core_add_listener(m_core, &m_core_listener, &events, this); + } + + // Lock must be held in thread-loop mode (drives pw_loop_iterate). + bool do_sync_locked(std::chrono::steady_clock::time_point deadline) + { + m_sync_done.store(0, std::memory_order_release); + m_sync_error.store(0, std::memory_order_release); + + int seq = pw_core_sync(m_core, PW_ID_CORE, 0); + m_sync_seq.store(seq, std::memory_order_release); + + using clk = std::chrono::steady_clock; + while (m_sync_done.load(std::memory_order_acquire) == 0 + && m_sync_error.load(std::memory_order_acquire) == 0) + { + auto now = clk::now(); + if (now >= deadline) + { + m_state.store(connection_state::broken, std::memory_order_release); + return false; + } + auto remaining_ms + = std::chrono::duration_cast(deadline - now).count(); + const int iter_ms = static_cast(remaining_ms < 8 ? remaining_ms : 8); + int r = pw_loop_iterate(m_loop, iter_ms); + if (r < 0) + { + m_state.store(connection_state::broken, std::memory_order_release); + return false; + } + } + + if (m_sync_error.load(std::memory_order_acquire) != 0) + { + m_state.store(connection_state::broken, std::memory_order_release); + return false; + } + + if (m_state.load(std::memory_order_acquire) == connection_state::connecting) + { + m_state.store(connection_state::connected, std::memory_order_release); + dispatch_state(connection_state::connected); + } + return true; + } + + static void on_registry_global( + void* data, std::uint32_t id, std::uint32_t /*permissions*/, const char* type, + std::uint32_t /*version*/, const spa_dict* props) noexcept + { + auto* self = static_cast(data); + if (!type) + return; + if (std::strcmp(type, PW_TYPE_INTERFACE_Node) == 0) + self->register_node(id, props); + else if (std::strcmp(type, PW_TYPE_INTERFACE_Port) == 0) + self->register_port(id, props); + else if (std::strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) + { + // Only the "default" metadata is WirePlumber's default-target + // marker (publishes default.audio.{sink,source}); ignore others. + const auto name = dict_get(props, PW_KEY_METADATA_NAME); + if (name == "default") + self->register_default_metadata(id); + } + } + + static void on_registry_global_remove(void* data, std::uint32_t id) noexcept + { + auto* self = static_cast(data); + self->unregister_global(id); + } + + void install_registry_listener() noexcept + { + spa_zero(m_registry_listener); + static constexpr pw_registry_events events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = &on_registry_global, + .global_remove = &on_registry_global_remove, + }; + pw_registry_add_listener(m_registry, &m_registry_listener, &events, this); + } + + void register_node(std::uint32_t id, const spa_dict* props) noexcept + { + auto* proxy = reinterpret_cast( + pw_registry_bind(m_registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0)); + if (!proxy) + return; + + bound_node b; + b.proxy = proxy; + b.hook = std::make_unique(); + spa_zero(*b.hook); + b.info.id = id; + + b.info.name = std::string{dict_get(props, PW_KEY_NODE_NAME)}; + b.info.description = std::string{dict_get(props, PW_KEY_NODE_DESCRIPTION)}; + b.info.media_class_str = std::string{dict_get(props, PW_KEY_MEDIA_CLASS)}; + b.info.media_role = std::string{dict_get(props, PW_KEY_MEDIA_ROLE)}; + b.info.kind = classify_media_class(b.info.media_class_str); + + m_bound_nodes.emplace_back(id, std::move(b)); + auto& slot = m_bound_nodes.back().second; + + static constexpr pw_node_events events = { + .version = PW_VERSION_NODE_EVENTS, + .info = &on_node_info, + .param = nullptr, + }; + auto* node_proxy = reinterpret_cast(slot.proxy); + pw_node_add_listener(node_proxy, slot.hook.get(), &events, this); + + if (!slot.info.name.empty() || !slot.info.media_class_str.empty()) + { + slot.info_seen = true; + mirror_node_to_snapshot(slot.info); + if (!slot.emitted_added) + { + slot.emitted_added = true; + dispatch_node_added(slot.info); + } + } + } + + static void on_node_info(void* data, const pw_node_info* info) noexcept + { + auto* self = static_cast(data); + if (!info) + return; + // info->props is only valid when PW_NODE_CHANGE_MASK_PROPS is set; + // otherwise re-reading clobbers cached values with empty placeholders. + const bool props_valid + = info->props && (info->change_mask & PW_NODE_CHANGE_MASK_PROPS); + for (auto& [k, slot] : self->m_bound_nodes) + { + if (k != info->id) + continue; + if (props_valid) + { + slot.info.name = std::string{dict_get(info->props, PW_KEY_NODE_NAME)}; + slot.info.description = std::string{dict_get(info->props, PW_KEY_NODE_DESCRIPTION)}; + slot.info.media_class_str = std::string{dict_get(info->props, PW_KEY_MEDIA_CLASS)}; + slot.info.media_role = std::string{dict_get(info->props, PW_KEY_MEDIA_ROLE)}; + slot.info.kind = classify_media_class(slot.info.media_class_str); + } + slot.info_seen = true; + self->mirror_node_to_snapshot(slot.info); + if (!slot.emitted_added) + { + slot.emitted_added = true; + self->dispatch_node_added(slot.info); + } + break; + } + } + + void mirror_node_to_snapshot(const node_info& n) + { + std::lock_guard lock{m_graph_mtx}; + auto& entry = m_nodes[n.id]; + // Preserve port lists populated by mirror_port_*. + auto inputs = std::move(entry.inputs); + auto outputs = std::move(entry.outputs); + bool phys = entry.physical; + entry = n; + entry.inputs = std::move(inputs); + entry.outputs = std::move(outputs); + entry.physical = phys; + } + + // Hand-parse Spa:String:JSON `{"name":"foo"}` (no JSON dep). + static std::string parse_default_target_json(const char* value) noexcept + { + if (!value) + return {}; + std::string_view v{value}; + constexpr std::string_view tag = "\"name\""; + auto kpos = v.find(tag); + if (kpos == std::string_view::npos) + return {}; + auto colon = v.find(':', kpos + tag.size()); + if (colon == std::string_view::npos) + return {}; + auto open = v.find('"', colon + 1); + if (open == std::string_view::npos) + return {}; + auto close = v.find('"', open + 1); + if (close == std::string_view::npos) + return {}; + return std::string{v.substr(open + 1, close - open - 1)}; + } + + void register_default_metadata(std::uint32_t id) noexcept + { + auto* proxy = reinterpret_cast(pw_registry_bind( + m_registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0)); + if (!proxy) + return; + if (m_default_metadata_proxy) + { + auto& pw = load(); + if (pw.proxy_destroy) + pw.proxy_destroy(proxy); + return; + } + m_default_metadata_proxy = proxy; + spa_zero(m_default_metadata_listener); + + static constexpr pw_metadata_events events = { + .version = PW_VERSION_METADATA_EVENTS, + .property = &on_default_metadata_property, + }; + pw_metadata_add_listener( + reinterpret_cast(proxy), &m_default_metadata_listener, + &events, this); + } + + static int on_default_metadata_property( + void* data, std::uint32_t /*subject*/, const char* key, + const char* /*type*/, const char* value) noexcept + { + if (!key) + return 0; + auto* self = static_cast(data); + const std::string parsed = parse_default_target_json(value); + std::lock_guard lock{self->m_graph_mtx}; + // `default.audio.*` (currently-routed) is what we want; + // `default.configured.audio.*` (sticky user choice) is ignored. + std::string_view k{key}; + if (k == "default.audio.sink") + self->m_default_sink_name = parsed; + else if (k == "default.audio.source") + self->m_default_source_name = parsed; + return 0; + } + +public: + std::string default_audio_sink_name() const + { + std::lock_guard lock{m_graph_mtx}; + return m_default_sink_name; + } + + std::string default_audio_source_name() const + { + std::lock_guard lock{m_graph_mtx}; + return m_default_source_name; + } + +private: + + void register_port(std::uint32_t id, const spa_dict* props) noexcept + { + auto* proxy = reinterpret_cast( + pw_registry_bind(m_registry, id, PW_TYPE_INTERFACE_Port, PW_VERSION_PORT, 0)); + if (!proxy) + return; + + bound_port b; + b.proxy = proxy; + b.hook = std::make_unique(); + spa_zero(*b.hook); + b.info.id = id; + if (props) + fill_port_from_props(b.info, props); + + m_bound_ports.emplace_back(id, std::move(b)); + auto& slot = m_bound_ports.back().second; + + static constexpr pw_port_events events = { + .version = PW_VERSION_PORT_EVENTS, + .info = &on_port_info, + .param = nullptr, + }; + auto* port_proxy = reinterpret_cast(slot.proxy); + pw_port_add_listener(port_proxy, slot.hook.get(), &events, this); + } + + static void on_port_info(void* data, const pw_port_info* info) noexcept + { + auto* self = static_cast(data); + if (!info) + return; + // See on_node_info: props only valid under PW_PORT_CHANGE_MASK_PROPS. + const bool props_valid + = info->props && (info->change_mask & PW_PORT_CHANGE_MASK_PROPS); + for (auto& [k, slot] : self->m_bound_ports) + { + if (k != info->id) + continue; + slot.info.id = info->id; + slot.info.direction = static_cast(info->direction); + if (props_valid) + self->fill_port_from_props(slot.info, info->props); + slot.info_seen = true; + self->mirror_port_to_snapshot(slot.info); + if (!slot.emitted_added && slot.info.node_id != 0) + { + slot.emitted_added = true; + self->dispatch_port_added(slot.info); + } + break; + } + } + + void fill_port_from_props(port_info& p, const spa_dict* props) noexcept + { + if (auto v = dict_get(props, "format.dsp"); !v.empty()) + { + p.format = std::string{v}; + p.kind = classify_format_dsp(p.format); + } + if (auto v = dict_get(props, PW_KEY_PORT_NAME); !v.empty()) + p.port_name = std::string{v}; + if (auto v = dict_get(props, PW_KEY_PORT_ALIAS); !v.empty()) + p.port_alias = std::string{v}; + if (auto v = dict_get(props, PW_KEY_OBJECT_PATH); !v.empty()) + p.object_path = std::string{v}; + if (auto v = dict_get(props, PW_KEY_PORT_ID); !v.empty()) + p.port_id = std::string{v}; + if (auto v = dict_get(props, PW_KEY_NODE_ID); !v.empty()) + { + // PW_KEY_NODE_ID is stringified. + try + { + p.node_id = static_cast(std::stoul(std::string{v})); + } + catch (...) + { + } + } + p.physical = dict_get(props, PW_KEY_PORT_PHYSICAL) == "true"; + p.terminal = dict_get(props, PW_KEY_PORT_TERMINAL) == "true"; + p.monitor = dict_get(props, PW_KEY_PORT_MONITOR) == "true"; + if (auto v = dict_get(props, PW_KEY_PORT_DIRECTION); v == "out") + p.direction = 1; // SPA_DIRECTION_OUTPUT + else if (v == "in") + p.direction = 0; // SPA_DIRECTION_INPUT + } + + void mirror_port_to_snapshot(const port_info& p) + { + if (p.node_id == 0) + return; + std::lock_guard lock{m_graph_mtx}; + auto& node = m_nodes[p.node_id]; + if (node.id == 0) + node.id = p.node_id; + auto& bucket = (p.direction == 1) ? node.outputs : node.inputs; + auto it + = std::find_if(bucket.begin(), bucket.end(), [&](const auto& q) { return q.id == p.id; }); + if (it == bucket.end()) + bucket.push_back(p); + else + *it = p; + if (p.physical) + node.physical = true; + } + + void unregister_global(std::uint32_t id) noexcept + { + auto& pw = load(); + { + auto it = std::find_if(m_bound_nodes.begin(), m_bound_nodes.end(), [id](const auto& e) { + return e.first == id; + }); + if (it != m_bound_nodes.end()) + { + if (it->second.hook) + spa_hook_remove(it->second.hook.get()); + if (it->second.proxy && pw.proxy_destroy) + pw.proxy_destroy(it->second.proxy); + m_bound_nodes.erase(it); + { + std::lock_guard lock{m_graph_mtx}; + m_nodes.erase(id); + } + dispatch_node_removed(id); + return; + } + } + { + auto it = std::find_if(m_bound_ports.begin(), m_bound_ports.end(), [id](const auto& e) { + return e.first == id; + }); + if (it != m_bound_ports.end()) + { + std::uint32_t parent_node = it->second.info.node_id; + if (it->second.hook) + spa_hook_remove(it->second.hook.get()); + if (it->second.proxy && pw.proxy_destroy) + pw.proxy_destroy(it->second.proxy); + m_bound_ports.erase(it); + if (parent_node != 0) + { + std::lock_guard lock{m_graph_mtx}; + auto nit = m_nodes.find(parent_node); + if (nit != m_nodes.end()) + { + auto& n = nit->second; + n.inputs.erase( + std::remove_if( + n.inputs.begin(), n.inputs.end(), [id](const auto& p) { return p.id == id; }), + n.inputs.end()); + n.outputs.erase( + std::remove_if( + n.outputs.begin(), n.outputs.end(), + [id](const auto& p) { return p.id == id; }), + n.outputs.end()); + } + } + dispatch_port_removed(id); + return; + } + } + } +}; + +inline void subscription::reset() noexcept +{ + if (m_id == 0) + return; + if (auto ctx = m_ctx.lock()) + ctx->unsubscribe(m_id); + m_ctx.reset(); + m_id = 0; +} + +inline subscription::~subscription() +{ + reset(); +} + +// Defined in context.cpp — see shared_instance() for the visibility +// rationale. +LIBREMIDI_EXPORT std::shared_ptr +shared_context(context::config cfg = context::config{}) noexcept; + +} + +#if defined(__clang__) + #pragma clang diagnostic pop +#endif + +#if defined(LIBREMIDI_HEADER_ONLY) + #include +#endif diff --git a/include/libremidi/backends/linux/pipewire/drm_modifiers.hpp b/include/libremidi/backends/linux/pipewire/drm_modifiers.hpp new file mode 100644 index 00000000..04ab53ce --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/drm_modifiers.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace libremidi::pipewire +{ +constexpr std::uint64_t DRM_FORMAT_MOD_LINEAR_ = 0; +constexpr std::uint64_t DRM_FORMAT_MOD_INVALID_ = 0x00FFFFFFFFFFFFFFLL; + +// DMA-BUF plane count for (fourcc, modifier). Vendor bits are +// modifier[56-63]. Conservatively returns 1 for unknown pairs; +// pipewire rejects mismatched counts so the producer can renegotiate. +constexpr std::uint32_t +drm_modifier_plane_count(std::uint32_t /*fourcc*/, std::uint64_t modifier) noexcept +{ + if (modifier == DRM_FORMAT_MOD_LINEAR_ || modifier == DRM_FORMAT_MOD_INVALID_) + return 1; + + const std::uint8_t vendor = static_cast((modifier >> 56) & 0xFFu); + switch (vendor) + { + case 0x02: // AMD + return 2; + case 0x01: // Intel + return 2; + case 0x08: // ARM (AFBC) + return 1; + case 0x03: // NVIDIA + return 1; + default: + return 1; + } +} + +} // namespace libremidi::pipewire diff --git a/include/libremidi/backends/linux/pipewire/filter.hpp b/include/libremidi/backends/linux/pipewire/filter.hpp new file mode 100644 index 00000000..52211298 --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/filter.hpp @@ -0,0 +1,350 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace libremidi::pipewire +{ + +struct filter_config +{ + std::string name; + std::string description; + std::string media_type{"Audio"}; + std::string media_category{"Filter"}; + + std::string media_role{"DSP"}; + + // Audio: "32 bit float mono audio"; MIDI 1.0: "8 bit raw midi"; + // MIDI 2.0: "32 bit raw UMP". + std::string format_dsp; + + int buffer_size{0}; + int rate{0}; + + bool force_quantum{false}; + bool lock_quantum{false}; + bool force_rate{false}; + bool lock_rate{false}; + + bool always_process{true}; + bool pause_on_idle{false}; + bool suspend_on_idle{false}; + + bool rt_process{true}; + std::uint32_t extra_flags{0}; +}; + +struct port_token +{ + void* opaque{}; + bool valid() const noexcept { return opaque != nullptr; } +}; + +class filter +{ +public: + using config = filter_config; + + filter(std::shared_ptr ctx, config cfg, const pw_filter_events& events, void* user_data) + : m_ctx{std::move(ctx)} + , m_cfg{std::move(cfg)} + { + if (!m_ctx || !m_ctx->ok()) + return; + auto& pw = load(); + if (!pw.filter_available) + return; + + m_ctx->with_lock([&] { + auto* props = build_filter_props(pw); + if (!props) + return; + // props ownership taken by pw_filter_new_simple. + m_filter = pw.filter_new_simple( + m_ctx->bare_loop(), m_cfg.name.c_str(), props, &events, user_data); + }); + } + + ~filter() { stop(); } + + filter(const filter&) = delete; + filter& operator=(const filter&) = delete; + filter(filter&&) = delete; + filter& operator=(filter&&) = delete; + + bool ok() const noexcept { return m_filter != nullptr; } + + port_token add_port( + pw_direction direction, std::string_view port_name, std::string_view format_dsp = {}, + pw_filter_port_flags port_flags = PW_FILTER_PORT_FLAG_MAP_BUFFERS, + const spa_pod* const* params = nullptr, std::uint32_t n_params = 0) + { + if (!ok()) + return {}; + auto& pw = load(); + void* result = nullptr; + m_ctx->with_lock([&] { + auto* props = pw.properties_new( + PW_KEY_FORMAT_DSP, + format_dsp.empty() ? m_cfg.format_dsp.c_str() : std::string{format_dsp}.c_str(), + PW_KEY_PORT_NAME, std::string{port_name}.c_str(), nullptr); + if (!props) + return; + // props ownership taken by pw_filter_add_port. + result = pw.filter_add_port( + m_filter, direction, port_flags, /*port_data_size=*/0, props, + const_cast(params), n_params); + }); + return port_token{result}; + } + + void remove_port(port_token token) + { + if (!ok() || !token.valid()) + return; + auto& pw = load(); + m_ctx->with_lock([&] { + if (pw.filter_remove_port) + pw.filter_remove_port(token.opaque); + }); + } + + int start() + { + if (!ok()) + return -ENOTCONN; + auto& pw = load(); + int rc = 0; + m_ctx->with_lock([&] { + pw_filter_flags flags = static_cast( + m_cfg.extra_flags | (m_cfg.rt_process ? PW_FILTER_FLAG_RT_PROCESS : 0)); + rc = pw.filter_connect(m_filter, flags, nullptr, 0); + }); + return rc; + } + + void stop() noexcept + { + if (!m_filter) + return; + auto& pw = load(); + if (!pw.filter_available) + { + m_filter = nullptr; + return; + } + if (m_ctx) + { + m_ctx->with_lock([&] { + if (pw.filter_disconnect) + pw.filter_disconnect(m_filter); + }); + // Let the disconnect propagate before destroy. + (void)m_ctx->synchronize(); + m_ctx->with_lock([&] { + if (pw.filter_destroy) + pw.filter_destroy(m_filter); + m_filter = nullptr; + }); + } + else + { + if (pw.filter_destroy) + pw.filter_destroy(m_filter); + m_filter = nullptr; + } + } + + std::uint32_t node_id() const noexcept + { + if (!m_filter) + return PW_ID_ANY; + auto& pw = load(); + return pw.filter_get_node_id ? pw.filter_get_node_id(m_filter) : PW_ID_ANY; + } + + bool synchronize_node(int max_attempts = 100) + { + for (int i = 0; i < max_attempts; ++i) + { + if (node_id() != PW_ID_ANY) + return true; + if (!m_ctx || !m_ctx->synchronize()) + return false; + } + return false; + } + + int update_params(port_token port, const spa_pod* const* params, std::uint32_t n_params) + { + if (!ok()) + return -ENOTCONN; + auto& pw = load(); + int rc = 0; + m_ctx->with_lock([&] { + rc = pw.filter_update_params( + m_filter, port.opaque, const_cast(params), n_params); + }); + return rc; + } + + int set_active(bool active) + { + if (!ok()) + return -ENOTCONN; + auto& pw = load(); + int rc = 0; + m_ctx->with_lock([&] { + if (pw.filter_set_active) + rc = pw.filter_set_active(m_filter, active); + }); + return rc; + } + + pw_filter* handle() noexcept { return m_filter; } + const config& cfg() const noexcept { return m_cfg; } + + // For use in DSP thread: + void* get_dsp_buffer(port_token port, std::uint32_t n_samples) noexcept + { + if (!m_filter || !port.valid()) + return nullptr; + auto& pw = load(); + return pw.filter_get_dsp_buffer ? pw.filter_get_dsp_buffer(port.opaque, n_samples) : nullptr; + } + + pw_buffer* dequeue_buffer(port_token port) noexcept + { + if (!m_filter || !port.valid()) + return nullptr; + auto& pw = load(); + return pw.filter_dequeue_buffer ? pw.filter_dequeue_buffer(port.opaque) : nullptr; + } + + void queue_buffer(port_token port, pw_buffer* buf) noexcept + { + if (!m_filter || !port.valid() || !buf) + return; + auto& pw = load(); + if (pw.filter_queue_buffer) + pw.filter_queue_buffer(port.opaque, buf); + } + +private: + pw_properties* build_filter_props(const api& pw) const + { + auto* props = pw.properties_new( + PW_KEY_MEDIA_TYPE, m_cfg.media_type.c_str(), PW_KEY_MEDIA_CATEGORY, + m_cfg.media_category.c_str(), PW_KEY_MEDIA_ROLE, m_cfg.media_role.c_str(), + PW_KEY_MEDIA_NAME, m_cfg.name.c_str(), PW_KEY_NODE_NAME, m_cfg.name.c_str(), nullptr); + if (!props) + return nullptr; + if (!m_cfg.description.empty()) + pw.properties_set(props, PW_KEY_NODE_DESCRIPTION, m_cfg.description.c_str()); + + if (m_cfg.buffer_size > 0 && m_cfg.rate > 0) + { + char latency[32]; + std::snprintf(latency, sizeof(latency), "%d/%d", m_cfg.buffer_size, m_cfg.rate); + pw.properties_set(props, PW_KEY_NODE_LATENCY, latency); + } + if (m_cfg.force_quantum && m_cfg.buffer_size > 0) + { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%d", m_cfg.buffer_size); + pw.properties_set(props, PW_KEY_NODE_FORCE_QUANTUM, buf); + } + if (m_cfg.lock_quantum) + pw.properties_set(props, PW_KEY_NODE_LOCK_QUANTUM, "true"); + if (m_cfg.force_rate && m_cfg.rate > 0) + { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%d", m_cfg.rate); + pw.properties_set(props, PW_KEY_NODE_FORCE_RATE, buf); + } + if (m_cfg.lock_rate) + pw.properties_set(props, PW_KEY_NODE_LOCK_RATE, "true"); + if (m_cfg.always_process) + pw.properties_set(props, PW_KEY_NODE_ALWAYS_PROCESS, "true"); + if (!m_cfg.pause_on_idle) + pw.properties_set(props, PW_KEY_NODE_PAUSE_ON_IDLE, "false"); + if (!m_cfg.suspend_on_idle) + pw.properties_set(props, PW_KEY_NODE_SUSPEND_ON_IDLE, "false"); + return props; + } + + std::shared_ptr m_ctx; + config m_cfg; + pw_filter* m_filter{}; +}; + +inline pw_proxy* link_ports(context& ctx, std::uint32_t out_port, std::uint32_t in_port) noexcept +{ + if (!ctx.ok()) + return nullptr; + auto& pw = load(); + pw_proxy* result = nullptr; + ctx.with_lock([&] { + 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); + if (!props) + return; + result = reinterpret_cast(pw_core_create_object( + ctx.pw_core_ptr(), "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, + 0)); + pw.properties_free(props); + }); + if (result) + (void)ctx.synchronize(); + return result; +} + +// Link by node ids; pipewire activates ports as needed. Required to +// reach a suspended sink that hasn't exposed ports (matches the +// `pw-link ` path). Release via unlink_ports. +inline pw_proxy* link_nodes(context& ctx, std::uint32_t out_node, std::uint32_t in_node) noexcept +{ + if (!ctx.ok()) + return nullptr; + auto& pw = load(); + pw_proxy* result = nullptr; + ctx.with_lock([&] { + auto* props = pw.properties_new( + PW_KEY_LINK_OUTPUT_NODE, std::to_string(out_node).c_str(), PW_KEY_LINK_INPUT_NODE, + std::to_string(in_node).c_str(), + nullptr); + if (!props) + return; + result = reinterpret_cast(pw_core_create_object( + ctx.pw_core_ptr(), "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, + 0)); + pw.properties_free(props); + }); + if (result) + (void)ctx.synchronize(); + return result; +} + +inline void unlink_ports(context& ctx, pw_proxy* link) noexcept +{ + if (!link) + return; + auto& pw = load(); + ctx.with_lock([&] { + if (pw.proxy_destroy) + pw.proxy_destroy(link); + }); +} + +} diff --git a/include/libremidi/backends/linux/pipewire/format.hpp b/include/libremidi/backends/linux/pipewire/format.hpp new file mode 100644 index 00000000..d83c7eb3 --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/format.hpp @@ -0,0 +1,330 @@ +#pragma once +// SPDX-License-Identifier: BSL-1.0 +// +// DMA-BUF / SHM video format negotiation for pw_stream consumers. +// See pipewire/doc/dox/internals/dma-buf.dox for the two-pass +// modifier protocol. + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace libremidi::pipewire +{ + +// DRM_FORMAT_MOD_* sentinels from , redeclared here +// so consumers don't need to pull in the full DRM headers. +constexpr std::uint64_t mod_linear = 0ULL; +constexpr std::uint64_t mod_invalid = 0x00ffffffffffffffULL; + +class format_negotiation +{ +public: + enum class result : std::uint8_t + { + // Param wasn't a SPA_PARAM_Format. + unrelated, + // Caller must test-allocate then re-announce. + needs_fixation, + // Single modifier chosen; use SPA_DATA_DmaBuf. + dmabuf_fixated, + // Use SPA_DATA_MemFd | SPA_DATA_MemPtr. + shm_fallback, + }; + + struct param_changed + { + result kind{result::unrelated}; + int width{}; + int height{}; + spa_video_format format{SPA_VIDEO_FORMAT_UNKNOWN}; + std::vector candidate_modifiers; + std::uint64_t chosen_modifier{}; + int n_planes{1}; + }; + + void set_size(int width, int height) noexcept + { + m_width = width; + m_height = height; + } + void set_framerate(int num, int den, int max_num = 0) noexcept + { + m_fps_num = num; + m_fps_den = den; + m_max_fps_num = max_num; + } + void set_video_format(spa_video_format f) noexcept { m_format = f; } + void set_dmabuf_modifiers(std::span mods) + { + m_consumer_modifiers.assign(mods.begin(), mods.end()); + } + void set_shm_fallback(bool enabled = true) noexcept { m_shm_fallback = enabled; } + + void set_producer_modifiers(std::span mods) + { + m_producer_modifiers.assign(mods.begin(), mods.end()); + } + + // `builder`'s underlying byte buffer must outlive pw_stream_connect. + std::vector + build_connect_params(spa_pod_builder& builder, pw_direction direction); + + param_changed on_param_changed(std::uint32_t id, const spa_pod* param); + + std::vector + build_fixation_params(spa_pod_builder& builder, std::uint64_t chosen); + + const spa_pod* + build_buffers_param(spa_pod_builder& builder, result kind, int blocks, int size, int stride); + + spa_video_format format() const noexcept { return m_format; } + int width() const noexcept { return m_width; } + int height() const noexcept { return m_height; } + +private: + int m_width{0}; + int m_height{0}; + int m_fps_num{30}; + int m_fps_den{1}; + int m_max_fps_num{60}; + spa_video_format m_format{SPA_VIDEO_FORMAT_RGBA}; + std::vector m_consumer_modifiers; + std::vector m_producer_modifiers; + bool m_shm_fallback{true}; +}; + +namespace detail +{ + +inline const spa_pod* build_video_enum_format( + spa_pod_builder& b, spa_video_format fmt, int width, int height, int fps_num, int fps_den, + int max_fps_num, std::span modifiers, bool dont_fixate) +{ + spa_pod_frame frame{}; + spa_pod_builder_push_object(&b, &frame, SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat); + + const spa_rectangle size_rect{ + static_cast(width), static_cast(height)}; + const spa_fraction fr{static_cast(fps_num), static_cast(fps_den)}; + const spa_fraction max_fr{ + static_cast(max_fps_num > 0 ? max_fps_num : fps_num), + static_cast(fps_den)}; + + spa_pod_builder_add( + &b, SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), SPA_FORMAT_mediaSubtype, + SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), SPA_FORMAT_VIDEO_format, SPA_POD_Id(fmt), + SPA_FORMAT_VIDEO_size, SPA_POD_Rectangle(&size_rect), SPA_FORMAT_VIDEO_framerate, + SPA_POD_Fraction(&fr), SPA_FORMAT_VIDEO_maxFramerate, SPA_POD_Fraction(&max_fr), 0); + + if (!modifiers.empty()) + { + // First pass uses MANDATORY | DONT_FIXATE ("pick one"); the + // fixation pass drops DONT_FIXATE and emits a single value. + std::uint32_t prop_flags = SPA_POD_PROP_FLAG_MANDATORY; + if (dont_fixate) + prop_flags |= SPA_POD_PROP_FLAG_DONT_FIXATE; + spa_pod_builder_prop(&b, SPA_FORMAT_VIDEO_modifier, prop_flags); + + if (modifiers.size() > 1 && dont_fixate) + { + spa_pod_frame choice_frame{}; + spa_pod_builder_push_choice(&b, &choice_frame, SPA_CHOICE_Enum, 0); + // SPA_CHOICE_Enum: first value is the default, then alternatives. + spa_pod_builder_long(&b, static_cast(modifiers[0])); + for (auto m : modifiers) + spa_pod_builder_long(&b, static_cast(m)); + spa_pod_builder_pop(&b, &choice_frame); + } + else + { + spa_pod_builder_long(&b, static_cast(modifiers[0])); + } + } + + return static_cast(spa_pod_builder_pop(&b, &frame)); +} + +// Returns false when no modifier prop is present (SHM fallback). +inline bool extract_modifier_choice( + const spa_pod* fmt_obj, std::uint64_t& out_single, std::vector& out_choices, + bool& out_dont_fixate) +{ + out_single = 0; + out_choices.clear(); + out_dont_fixate = false; + if (!fmt_obj || !spa_pod_is_object(fmt_obj)) + return false; + + const auto* obj = reinterpret_cast(fmt_obj); + const spa_pod_prop* prop = spa_pod_object_find_prop(obj, nullptr, SPA_FORMAT_VIDEO_modifier); + if (!prop) + return false; + + out_dont_fixate = (prop->flags & SPA_POD_PROP_FLAG_DONT_FIXATE) != 0; + const spa_pod* val = &prop->value; + + if (spa_pod_is_choice(val)) + { + const auto* choice = reinterpret_cast(val); + const spa_pod* child = SPA_POD_CHOICE_CHILD(reinterpret_cast(val)); + const std::uint32_t child_size = SPA_POD_BODY_SIZE(child); + const std::uint32_t n_values + = (SPA_POD_BODY_SIZE(val) - sizeof(spa_pod_choice_body)) / child_size; + const std::uint8_t* p = static_cast(SPA_POD_CONTENTS(spa_pod_choice, val)) + + sizeof(spa_pod_choice_body); + for (std::uint32_t i = 0; i < n_values; ++i) + { + std::int64_t v; + std::memcpy(&v, p, sizeof(v)); + out_choices.push_back(static_cast(v)); + p += child_size; + } + (void)choice; + return true; + } + + if (spa_pod_is_long(val)) + { + std::int64_t v = 0; + spa_pod_get_long(val, &v); + out_single = static_cast(v); + return true; + } + + return false; +} + +} // namespace detail + +inline std::vector +format_negotiation::build_connect_params(spa_pod_builder& builder, pw_direction /*direction*/) +{ + std::vector out; + out.reserve(2); + + if (!m_consumer_modifiers.empty()) + { + if (auto* p = detail::build_video_enum_format( + builder, m_format, m_width, m_height, m_fps_num, m_fps_den, m_max_fps_num, + std::span(m_consumer_modifiers), + /*dont_fixate=*/true)) + out.push_back(p); + } + + if (m_shm_fallback) + { + if (auto* p = detail::build_video_enum_format( + builder, m_format, m_width, m_height, m_fps_num, m_fps_den, m_max_fps_num, + std::span{}, // no modifiers → SHM + /*dont_fixate=*/false)) + out.push_back(p); + } + + // Unconfigured consumer: fall back to a LINEAR-only EnumFormat. + if (out.empty()) + { + static constexpr std::uint64_t linear_only[]{0ULL}; + if (auto* p = detail::build_video_enum_format( + builder, m_format, m_width, m_height, m_fps_num, m_fps_den, m_max_fps_num, + std::span(linear_only, 1), + /*dont_fixate=*/false)) + out.push_back(p); + } + + return out; +} + +inline format_negotiation::param_changed +format_negotiation::on_param_changed(std::uint32_t id, const spa_pod* param) +{ + param_changed pc; + if (id != SPA_PARAM_Format || !param) + return pc; + + spa_video_info_raw info{}; + if (spa_format_video_raw_parse(param, &info) < 0) + return pc; + pc.width = static_cast(info.size.width); + pc.height = static_cast(info.size.height); + pc.format = info.format; + + std::uint64_t single{}; + bool dont_fixate = false; + const bool has_modifier_prop + = detail::extract_modifier_choice(param, single, pc.candidate_modifiers, dont_fixate); + + if (!has_modifier_prop) + { + // No modifier prop -> producer wants SHM. + pc.kind = result::shm_fallback; + pc.chosen_modifier = mod_invalid; + pc.n_planes = 1; + return pc; + } + + if (dont_fixate && !pc.candidate_modifiers.empty()) + { + pc.kind = result::needs_fixation; + return pc; + } + + if (!pc.candidate_modifiers.empty()) + single = pc.candidate_modifiers.front(); + pc.chosen_modifier = single; + pc.kind = result::dmabuf_fixated; + pc.n_planes = static_cast(drm_modifier_plane_count(0, single)); + pc.candidate_modifiers.clear(); + return pc; +} + +inline std::vector +format_negotiation::build_fixation_params(spa_pod_builder& builder, std::uint64_t chosen) +{ + std::vector out; + const std::uint64_t mods[]{chosen}; + if (auto* p = detail::build_video_enum_format( + builder, m_format, m_width, m_height, m_fps_num, m_fps_den, m_max_fps_num, + std::span(mods, 1), + /*dont_fixate=*/false)) + out.push_back(p); + return out; +} + +inline const spa_pod* format_negotiation::build_buffers_param( + spa_pod_builder& builder, result kind, int blocks, int size, int stride) +{ + std::uint32_t data_type{}; + switch (kind) + { + case result::dmabuf_fixated: + data_type = (1u << SPA_DATA_DmaBuf); + break; + case result::shm_fallback: + data_type = (1u << SPA_DATA_MemFd) | (1u << SPA_DATA_MemPtr); + break; + default: + data_type = (1u << SPA_DATA_MemPtr); + break; + } + return reinterpret_cast(spa_pod_builder_add_object( + &builder, SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers, SPA_PARAM_BUFFERS_buffers, + SPA_POD_CHOICE_RANGE_Int(8, 2, 16), SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(blocks), + SPA_PARAM_BUFFERS_size, SPA_POD_Int(size), SPA_PARAM_BUFFERS_stride, SPA_POD_Int(stride), + SPA_PARAM_BUFFERS_dataType, SPA_POD_CHOICE_FLAGS_Int(data_type))); +} + +} // namespace libremidi::pipewire diff --git a/include/libremidi/backends/linux/pipewire/instance.cpp b/include/libremidi/backends/linux/pipewire/instance.cpp new file mode 100644 index 00000000..1dda9e46 --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/instance.cpp @@ -0,0 +1,31 @@ +#if defined(LIBREMIDI_PIPEWIRE) + + #if !defined(LIBREMIDI_HEADER_ONLY) + #include + #endif + + #include + +namespace libremidi::pipewire +{ + +LIBREMIDI_INLINE +std::shared_ptr shared_instance() noexcept +{ + static std::mutex mtx; + static std::weak_ptr weak; + + std::lock_guard lock{mtx}; + if (auto p = weak.lock()) + return p; + + auto p = std::make_shared(); + if (!p->initialized()) + return {}; + weak = p; + return p; +} + +} + +#endif diff --git a/include/libremidi/backends/linux/pipewire/instance.hpp b/include/libremidi/backends/linux/pipewire/instance.hpp new file mode 100644 index 00000000..fc35f960 --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/instance.hpp @@ -0,0 +1,53 @@ +#pragma once +#include +#include + +#include + +namespace libremidi::pipewire +{ +class instance +{ +public: + instance() noexcept + { + if (auto& pw = load(); pw.core_available) + { + int argc = 0; + char* argv[] = {nullptr}; + char** aa = argv; + pw.init(&argc, &aa); + m_initialized = true; + } + } + + ~instance() + { + if (m_initialized) + { + if (auto& pw = load(); pw.deinit) + pw.deinit(); + } + } + + instance(const instance&) = delete; + instance& operator=(const instance&) = delete; + instance(instance&&) = delete; + instance& operator=(instance&&) = delete; + + bool initialized() const noexcept { return m_initialized; } + +private: + bool m_initialized{}; +}; + +// Defined in instance.cpp: LIBREMIDI_EXPORT (default visibility) is +// required to dedup across plugin .so boundaries; inline would init +// pw_init per plugin and race libpipewire's globals. +LIBREMIDI_EXPORT std::shared_ptr shared_instance() noexcept; + +} + +#if defined(LIBREMIDI_HEADER_ONLY) + #include +#endif diff --git a/include/libremidi/backends/linux/pipewire/loader.hpp b/include/libremidi/backends/linux/pipewire/loader.hpp new file mode 100644 index 00000000..5659187e --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/loader.hpp @@ -0,0 +1,274 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace libremidi::pipewire +{ +class api +{ +public: + decltype(&::pw_init) init{}; + decltype(&::pw_deinit) deinit{}; + decltype(&::pw_get_library_version) get_library_version{}; + + decltype(&::pw_main_loop_new) main_loop_new{}; + decltype(&::pw_main_loop_destroy) main_loop_destroy{}; + decltype(&::pw_main_loop_run) main_loop_run{}; + decltype(&::pw_main_loop_quit) main_loop_quit{}; + decltype(&::pw_main_loop_get_loop) main_loop_get_loop{}; + + decltype(&::pw_thread_loop_new) thread_loop_new{}; + decltype(&::pw_thread_loop_new_full) thread_loop_new_full{}; + decltype(&::pw_thread_loop_destroy) thread_loop_destroy{}; + decltype(&::pw_thread_loop_start) thread_loop_start{}; + decltype(&::pw_thread_loop_stop) thread_loop_stop{}; + decltype(&::pw_thread_loop_get_loop) thread_loop_get_loop{}; + decltype(&::pw_thread_loop_lock) thread_loop_lock{}; + decltype(&::pw_thread_loop_unlock) thread_loop_unlock{}; + decltype(&::pw_thread_loop_signal) thread_loop_signal{}; + decltype(&::pw_thread_loop_wait) thread_loop_wait{}; + decltype(&::pw_thread_loop_timed_wait) thread_loop_timed_wait{}; + decltype(&::pw_thread_loop_in_thread) thread_loop_in_thread{}; + decltype(&::pw_thread_loop_add_listener) thread_loop_add_listener{}; + decltype(&::pw_thread_loop_accept) thread_loop_accept{}; + + decltype(&::pw_context_new) context_new{}; + decltype(&::pw_context_destroy) context_destroy{}; + decltype(&::pw_context_connect) context_connect{}; + decltype(&::pw_context_connect_fd) context_connect_fd{}; + decltype(&::pw_context_load_module) context_load_module{}; + decltype(&::pw_core_disconnect) core_disconnect{}; + + decltype(&::pw_proxy_add_listener) proxy_add_listener{}; + decltype(&::pw_proxy_destroy) proxy_destroy{}; + + decltype(&::pw_properties_new) properties_new{}; + decltype(&::pw_properties_new_dict) properties_new_dict{}; + decltype(&::pw_properties_set) properties_set{}; + decltype(&::pw_properties_setf) properties_setf{}; + decltype(&::pw_properties_get) properties_get{}; + decltype(&::pw_properties_copy) properties_copy{}; + decltype(&::pw_properties_free) properties_free{}; + + decltype(&::pw_stream_new) stream_new{}; + decltype(&::pw_stream_new_simple) stream_new_simple{}; + decltype(&::pw_stream_destroy) stream_destroy{}; + decltype(&::pw_stream_add_listener) stream_add_listener{}; + decltype(&::pw_stream_connect) stream_connect{}; + decltype(&::pw_stream_disconnect) stream_disconnect{}; + decltype(&::pw_stream_dequeue_buffer) stream_dequeue_buffer{}; + decltype(&::pw_stream_queue_buffer) stream_queue_buffer{}; + decltype(&::pw_stream_update_params) stream_update_params{}; + decltype(&::pw_stream_update_properties) stream_update_properties{}; + decltype(&::pw_stream_set_active) stream_set_active{}; +#if PW_CHECK_VERSION(0, 3, 70) + decltype(&::pw_stream_set_param) stream_set_param{}; +#else + int (*stream_set_param)(){}; // pipewire < 0.3.70: symbol absent +#endif + decltype(&::pw_stream_get_state) stream_get_state{}; + decltype(&::pw_stream_get_name) stream_get_name{}; + decltype(&::pw_stream_get_properties) stream_get_properties{}; + decltype(&::pw_stream_get_node_id) stream_get_node_id{}; + decltype(&::pw_stream_get_time_n) stream_get_time_n{}; + decltype(&::pw_stream_state_as_string) stream_state_as_string{}; + decltype(&::pw_stream_trigger_process) stream_trigger_process{}; + decltype(&::pw_stream_flush) stream_flush{}; + + decltype(&::pw_filter_new) filter_new{}; + decltype(&::pw_filter_new_simple) filter_new_simple{}; + decltype(&::pw_filter_destroy) filter_destroy{}; + decltype(&::pw_filter_add_listener) filter_add_listener{}; + decltype(&::pw_filter_connect) filter_connect{}; + decltype(&::pw_filter_disconnect) filter_disconnect{}; + decltype(&::pw_filter_get_state) filter_get_state{}; + decltype(&::pw_filter_get_node_id) filter_get_node_id{}; + decltype(&::pw_filter_get_properties) filter_get_properties{}; + decltype(&::pw_filter_update_properties) filter_update_properties{}; + decltype(&::pw_filter_update_params) filter_update_params{}; + decltype(&::pw_filter_add_port) filter_add_port{}; + decltype(&::pw_filter_remove_port) filter_remove_port{}; + decltype(&::pw_filter_dequeue_buffer) filter_dequeue_buffer{}; + decltype(&::pw_filter_queue_buffer) filter_queue_buffer{}; + decltype(&::pw_filter_get_dsp_buffer) filter_get_dsp_buffer{}; + + decltype(&::pw_filter_set_active) filter_set_active{}; + decltype(&::pw_filter_flush) filter_flush{}; +#if PW_CHECK_VERSION(0, 3, 66) + decltype(&::pw_filter_trigger_process) filter_trigger_process{}; +#else + int (*filter_trigger_process)(){}; // pipewire < 0.3.66: symbol absent +#endif + + bool available{}; + bool core_available{}; + bool thread_available{}; + bool stream_available{}; + bool filter_available{}; + + static const api& instance() noexcept + { + static const api self; + return self; + } + +private: + static void* try_open() noexcept + { + for (const char* name : {"libpipewire-0.3.so.0", "libpipewire-0.3.so"}) + { + if (void* h = ::dlopen(name, RTLD_LAZY | RTLD_LOCAL | RTLD_NODELETE)) + return h; + } + return nullptr; + } + + template + T sym(void* handle, const char* name) noexcept + { + return reinterpret_cast(::dlsym(handle, name)); + } + + api() noexcept + { + void* h = try_open(); + if (!h) + return; + + init = sym(h, "pw_init"); + deinit = sym(h, "pw_deinit"); + get_library_version = sym(h, "pw_get_library_version"); + + main_loop_new = sym(h, "pw_main_loop_new"); + main_loop_destroy = sym(h, "pw_main_loop_destroy"); + main_loop_run = sym(h, "pw_main_loop_run"); + main_loop_quit = sym(h, "pw_main_loop_quit"); + main_loop_get_loop = sym(h, "pw_main_loop_get_loop"); + + context_new = sym(h, "pw_context_new"); + context_destroy = sym(h, "pw_context_destroy"); + context_connect = sym(h, "pw_context_connect"); + context_connect_fd = sym(h, "pw_context_connect_fd"); + context_load_module = sym(h, "pw_context_load_module"); + core_disconnect = sym(h, "pw_core_disconnect"); + + proxy_add_listener = sym(h, "pw_proxy_add_listener"); + proxy_destroy = sym(h, "pw_proxy_destroy"); + + properties_new = sym(h, "pw_properties_new"); + properties_new_dict = sym(h, "pw_properties_new_dict"); + properties_set = sym(h, "pw_properties_set"); + properties_setf = sym(h, "pw_properties_setf"); + properties_get = sym(h, "pw_properties_get"); + properties_copy = sym(h, "pw_properties_copy"); + properties_free = sym(h, "pw_properties_free"); + + core_available = init && deinit && main_loop_new && main_loop_destroy && main_loop_run + && main_loop_quit && main_loop_get_loop && context_new && context_destroy + && context_connect && core_disconnect && proxy_destroy && properties_new + && properties_free; + available = core_available; + + thread_loop_new = sym(h, "pw_thread_loop_new"); + thread_loop_new_full = sym(h, "pw_thread_loop_new_full"); + thread_loop_destroy = sym(h, "pw_thread_loop_destroy"); + thread_loop_start = sym(h, "pw_thread_loop_start"); + thread_loop_stop = sym(h, "pw_thread_loop_stop"); + thread_loop_get_loop = sym(h, "pw_thread_loop_get_loop"); + thread_loop_lock = sym(h, "pw_thread_loop_lock"); + thread_loop_unlock = sym(h, "pw_thread_loop_unlock"); + thread_loop_signal = sym(h, "pw_thread_loop_signal"); + thread_loop_wait = sym(h, "pw_thread_loop_wait"); + thread_loop_timed_wait = sym(h, "pw_thread_loop_timed_wait"); + thread_loop_in_thread = sym(h, "pw_thread_loop_in_thread"); + thread_loop_add_listener + = sym(h, "pw_thread_loop_add_listener"); + thread_loop_accept = sym(h, "pw_thread_loop_accept"); + + thread_available = thread_loop_new && thread_loop_destroy && thread_loop_start + && thread_loop_stop && thread_loop_get_loop && thread_loop_lock + && thread_loop_unlock && thread_loop_signal && thread_loop_wait + && thread_loop_in_thread; + + stream_new = sym(h, "pw_stream_new"); + stream_new_simple = sym(h, "pw_stream_new_simple"); + stream_destroy = sym(h, "pw_stream_destroy"); + stream_add_listener = sym(h, "pw_stream_add_listener"); + stream_connect = sym(h, "pw_stream_connect"); + stream_disconnect = sym(h, "pw_stream_disconnect"); + stream_dequeue_buffer = sym(h, "pw_stream_dequeue_buffer"); + stream_queue_buffer = sym(h, "pw_stream_queue_buffer"); + stream_update_params = sym(h, "pw_stream_update_params"); + stream_update_properties + = sym(h, "pw_stream_update_properties"); + stream_set_active = sym(h, "pw_stream_set_active"); +#if PW_CHECK_VERSION(0, 3, 70) + stream_set_param = sym(h, "pw_stream_set_param"); +#endif + stream_get_state = sym(h, "pw_stream_get_state"); + stream_get_name = sym(h, "pw_stream_get_name"); + stream_get_properties = sym(h, "pw_stream_get_properties"); + stream_get_node_id = sym(h, "pw_stream_get_node_id"); + stream_get_time_n = sym(h, "pw_stream_get_time_n"); + stream_state_as_string = sym(h, "pw_stream_state_as_string"); + stream_trigger_process = sym(h, "pw_stream_trigger_process"); + stream_flush = sym(h, "pw_stream_flush"); + + stream_available = stream_new && stream_destroy && stream_add_listener && stream_connect + && stream_disconnect && stream_dequeue_buffer && stream_queue_buffer + && stream_update_params; + + filter_new = sym(h, "pw_filter_new"); + filter_new_simple = sym(h, "pw_filter_new_simple"); + filter_destroy = sym(h, "pw_filter_destroy"); + filter_add_listener = sym(h, "pw_filter_add_listener"); + filter_connect = sym(h, "pw_filter_connect"); + filter_disconnect = sym(h, "pw_filter_disconnect"); + filter_get_state = sym(h, "pw_filter_get_state"); + filter_get_node_id = sym(h, "pw_filter_get_node_id"); + filter_get_properties = sym(h, "pw_filter_get_properties"); + filter_update_properties + = sym(h, "pw_filter_update_properties"); + filter_update_params = sym(h, "pw_filter_update_params"); + filter_add_port = sym(h, "pw_filter_add_port"); + filter_remove_port = sym(h, "pw_filter_remove_port"); + filter_dequeue_buffer = sym(h, "pw_filter_dequeue_buffer"); + filter_queue_buffer = sym(h, "pw_filter_queue_buffer"); + filter_get_dsp_buffer = sym(h, "pw_filter_get_dsp_buffer"); + filter_set_active = sym(h, "pw_filter_set_active"); + filter_flush = sym(h, "pw_filter_flush"); +#if PW_CHECK_VERSION(0, 3, 66) + filter_trigger_process = sym(h, "pw_filter_trigger_process"); +#endif + + filter_available = filter_new_simple && filter_destroy && filter_connect && filter_disconnect + && filter_add_port && filter_get_node_id && filter_get_dsp_buffer; + } + + api(const api&) = delete; + api& operator=(const api&) = delete; + api(api&&) = delete; + api& operator=(api&&) = delete; +}; + +inline const api& load() noexcept +{ + return api::instance(); +} + +} diff --git a/include/libremidi/backends/linux/pipewire/stream.hpp b/include/libremidi/backends/linux/pipewire/stream.hpp new file mode 100644 index 00000000..6520b551 --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/stream.hpp @@ -0,0 +1,234 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace libremidi::pipewire +{ + +struct stream_config +{ + std::string name; + + pw_direction direction{PW_DIRECTION_INPUT}; + std::uint32_t target_id{PW_ID_ANY}; + pw_stream_flags flags{PW_STREAM_FLAG_AUTOCONNECT}; + std::unordered_map properties; +}; + +class stream +{ +public: + using config = stream_config; + + stream(std::shared_ptr ctx, config cfg, const pw_stream_events& events, void* user_data) + : m_ctx{std::move(ctx)} + , m_cfg{std::move(cfg)} + { + if (!m_ctx || !m_ctx->ok()) + return; + auto& pw = load(); + if (!pw.stream_available) + return; + + m_ctx->with_lock([&] { + auto* props = build_stream_props(pw); + if (!props) + return; + // props ownership taken by pw_stream_new_simple. + m_stream = pw.stream_new_simple( + m_ctx->bare_loop(), m_cfg.name.c_str(), props, &events, user_data); + }); + } + + ~stream() { destroy(); } + + stream(const stream&) = delete; + stream& operator=(const stream&) = delete; + stream(stream&&) = delete; + stream& operator=(stream&&) = delete; + + bool ok() const noexcept { return m_stream != nullptr; } + + int connect(std::span params) + { + if (!ok()) + return -ENOTCONN; + auto& pw = load(); + int rc = 0; + m_ctx->with_lock([&] { + rc = pw.stream_connect( + m_stream, m_cfg.direction, m_cfg.target_id, m_cfg.flags, params.data(), + static_cast(params.size())); + }); + return rc; + } + + int update_params(std::span params) + { + if (!ok()) + return -ENOTCONN; + auto& pw = load(); + int rc = 0; + m_ctx->with_lock([&] { + rc = pw.stream_update_params( + m_stream, params.data(), static_cast(params.size())); + }); + return rc; + } + + int set_active(bool active) + { + if (!ok()) + return -ENOTCONN; + auto& pw = load(); + int rc = 0; + m_ctx->with_lock([&] { + if (pw.stream_set_active) + rc = pw.stream_set_active(m_stream, active); + }); + return rc; + } + + pw_stream_state state(const char** error_out = nullptr) const noexcept + { + if (!m_stream) + return PW_STREAM_STATE_UNCONNECTED; + auto& pw = load(); + if (!pw.stream_get_state) + return PW_STREAM_STATE_UNCONNECTED; + return pw.stream_get_state(m_stream, error_out); + } + + std::uint32_t node_id() const noexcept + { + if (!m_stream) + return PW_ID_ANY; + auto& pw = load(); + return pw.stream_get_node_id ? pw.stream_get_node_id(m_stream) : PW_ID_ANY; + } + + void disconnect() noexcept + { + if (!m_stream) + return; + auto& pw = load(); + if (!pw.stream_available || !m_ctx) + return; + m_ctx->with_lock([&] { + if (pw.stream_set_active) + pw.stream_set_active(m_stream, false); + if (pw.stream_disconnect) + pw.stream_disconnect(m_stream); + }); + (void)m_ctx->synchronize(); + } + + void destroy() noexcept + { + if (!m_stream) + return; + auto& pw = load(); + if (!pw.stream_available) + { + m_stream = nullptr; + return; + } + if (m_ctx) + { + disconnect(); + m_ctx->with_lock([&] { + if (pw.stream_destroy) + pw.stream_destroy(m_stream); + m_stream = nullptr; + }); + } + else + { + if (pw.stream_destroy) + pw.stream_destroy(m_stream); + m_stream = nullptr; + } + } + + // RT-safe functions + pw_buffer* dequeue_buffer() noexcept + { + if (!m_stream) + return nullptr; + auto& pw = load(); + return pw.stream_dequeue_buffer ? pw.stream_dequeue_buffer(m_stream) : nullptr; + } + + int queue_buffer(pw_buffer* buf) noexcept + { + if (!m_stream || !buf) + return -EINVAL; + auto& pw = load(); + return pw.stream_queue_buffer ? pw.stream_queue_buffer(m_stream, buf) : -EINVAL; + } + + int trigger_process() noexcept + { + if (!m_stream) + return -ENOTCONN; + auto& pw = load(); + return pw.stream_trigger_process ? pw.stream_trigger_process(m_stream) : -ENOTSUP; + } + + pw_stream* handle() noexcept { return m_stream; } + const config& cfg() const noexcept { return m_cfg; } + + void queue_buffer_from_any_thread(pw_buffer* buf, bool block = true) noexcept + { + if (!m_stream || !buf || !m_ctx) + return; + auto& pw = load(); + if (!pw.stream_queue_buffer) + return; + + struct payload + { + pw_stream* stream; + pw_buffer* buf; + decltype(pw.stream_queue_buffer) qb; + }; + + if (block) + { + m_ctx->invoke_sync([&] { pw.stream_queue_buffer(m_stream, buf); }); + } + else + { + m_ctx->invoke_async([s = m_stream, buf, qb = pw.stream_queue_buffer] { qb(s, buf); }); + } + } + +private: + pw_properties* build_stream_props(const api& pw) const + { + auto* props = pw.properties_new(nullptr, nullptr); + if (!props) + return nullptr; + pw.properties_set(props, PW_KEY_NODE_NAME, m_cfg.name.c_str()); + for (const auto& [k, v] : m_cfg.properties) + pw.properties_set(props, k.c_str(), v.c_str()); + return props; + } + + std::shared_ptr m_ctx; + config m_cfg; + pw_stream* m_stream{}; +}; + +} diff --git a/include/libremidi/backends/linux/pipewire/subscription.hpp b/include/libremidi/backends/linux/pipewire/subscription.hpp new file mode 100644 index 00000000..b583e42c --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/subscription.hpp @@ -0,0 +1,56 @@ +#pragma once +#include + +#include +#include +#include + +namespace libremidi::pipewire +{ + +class context; + +class subscription +{ +public: + subscription() noexcept = default; + + subscription(std::weak_ptr ctx, std::uint64_t id) noexcept + : m_ctx{std::move(ctx)} + , m_id{id} + { + } + + subscription(const subscription&) = delete; + subscription& operator=(const subscription&) = delete; + + subscription(subscription&& other) noexcept + : m_ctx{std::move(other.m_ctx)} + , m_id{std::exchange(other.m_id, 0)} + { + } + + subscription& operator=(subscription&& other) noexcept + { + if (this != &other) + { + reset(); + m_ctx = std::move(other.m_ctx); + m_id = std::exchange(other.m_id, 0); + } + return *this; + } + + ~subscription(); + + void reset() noexcept; + bool empty() const noexcept { return m_id == 0; } + explicit operator bool() const noexcept { return !empty(); } + std::uint64_t id() const noexcept { return m_id; } + +private: + std::weak_ptr m_ctx; + std::uint64_t m_id{}; +}; + +} diff --git a/include/libremidi/backends/linux/pipewire/types.hpp b/include/libremidi/backends/linux/pipewire/types.hpp new file mode 100644 index 00000000..07243cba --- /dev/null +++ b/include/libremidi/backends/linux/pipewire/types.hpp @@ -0,0 +1,178 @@ +#pragma once +#include + +#include +#include +#include + +#include +#include +#include +#include + +namespace libremidi::pipewire +{ +enum class connection_state : std::uint8_t +{ + connecting, + connected, + broken, + disconnected, +}; +enum class media_class : std::uint8_t +{ + other = 0, + audio, + midi, + ump, + video, +}; + +constexpr bool is_audio(media_class c) noexcept +{ + return c == media_class::audio; +} +constexpr bool is_midi_like(media_class c) noexcept +{ + return c == media_class::midi || c == media_class::ump; +} +constexpr bool is_video(media_class c) noexcept +{ + return c == media_class::video; +} + +inline media_class classify_format_dsp(std::string_view fmt) noexcept +{ + if (fmt.empty()) + return media_class::other; + + if (fmt.find("UMP") != std::string_view::npos) + return media_class::ump; + if (fmt.find("ump") != std::string_view::npos) + return media_class::ump; + if (fmt.find("midi") != std::string_view::npos || fmt.find("Midi") != std::string_view::npos + || fmt.find("MIDI") != std::string_view::npos) + return media_class::midi; + + if (fmt.find("audio") != std::string_view::npos || fmt.find("Audio") != std::string_view::npos) + return media_class::audio; + + if (fmt.find("video") != std::string_view::npos || fmt.find("Video") != std::string_view::npos) + return media_class::video; + return media_class::other; +} + +inline media_class classify_media_class(std::string_view media_class_str) noexcept +{ + if (media_class_str.empty()) + return media_class::other; + + constexpr std::string_view stream_prefix = "Stream/"; + std::string_view rest = media_class_str; + if (rest.size() > stream_prefix.size() && rest.substr(0, stream_prefix.size()) == stream_prefix) + { + // Strip "Stream//". + rest.remove_prefix(stream_prefix.size()); + if (auto slash = rest.find('/'); slash != std::string_view::npos) + rest = rest.substr(slash + 1); + } + if (rest.find("Audio") != std::string_view::npos) + return media_class::audio; + + if (rest.find("Video") != std::string_view::npos) + return media_class::video; + + if (rest.find("UMP") != std::string_view::npos || rest.find("ump") != std::string_view::npos) + return media_class::ump; + + if (rest.find("Midi") != std::string_view::npos || rest.find("midi") != std::string_view::npos + || rest.find("MIDI") != std::string_view::npos) + return media_class::midi; + return media_class::other; +} + +inline std::string_view dict_get(const spa_dict* dict, const char* key) noexcept +{ + if (!dict || !key) + return {}; + const char* v = spa_dict_lookup(dict, key); + return v ? std::string_view{v} : std::string_view{}; +} + +inline media_class classify_node_props(const spa_dict* props) noexcept +{ + return classify_media_class(dict_get(props, PW_KEY_MEDIA_CLASS)); +} + +struct port_info +{ + std::uint32_t id{}; + + std::uint32_t node_id{}; + + std::string format; + + media_class kind{media_class::other}; + + std::string port_name; ///< `PW_KEY_PORT_NAME` + std::string port_alias; ///< `PW_KEY_PORT_ALIAS` + std::string object_path; ///< `PW_KEY_OBJECT_PATH` + std::string port_id; ///< `PW_KEY_PORT_ID` (stringified) + + bool physical{}; ///< `port.physical = true` + bool terminal{}; ///< `port.terminal = true` + bool monitor{}; ///< `port.monitor = true` + + int direction{}; /// < `SPA_DIRECTION_INPUT=0, _OUTPUT=1` +}; + +struct node_info +{ + std::uint32_t id{}; + + std::string name; ///< `PW_KEY_NODE_NAME` + std::string description; ///< `PW_KEY_NODE_DESCRIPTION` + std::string media_class_str; ///< `PW_KEY_MEDIA_CLASS` raw string + std::string media_role; ///< `PW_KEY_MEDIA_ROLE` + + media_class kind{media_class::other}; + + // True if any of this node's ports has `port.physical = true` + bool physical{}; + + std::vector inputs; + std::vector outputs; +}; + +struct graph_snapshot +{ + std::vector nodes; + + std::vector nodes_of(media_class c) const + { + std::vector out; + out.reserve(nodes.size()); + for (const auto& n : nodes) + if (n.kind == c) + out.push_back(n); + return out; + } + + const node_info* find_by_name(std::string_view name) const noexcept + { + for (const auto& n : nodes) + if (n.name == name) + return &n; + return nullptr; + } + + const node_info* find_by_id(std::uint32_t id) const noexcept + { + for (const auto& n : nodes) + if (n.id == id) + return &n; + return nullptr; + } +}; + +} diff --git a/include/libremidi/backends/pipewire.hpp b/include/libremidi/backends/pipewire.hpp index 914a44d4..f2c8346c 100644 --- a/include/libremidi/backends/pipewire.hpp +++ b/include/libremidi/backends/pipewire.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include #include @@ -25,8 +25,8 @@ struct backend static inline bool available() noexcept { - static const libpipewire& pw = libpipewire::instance(); - return pw.available; + auto& pw = libremidi::pipewire::load(); + return pw.filter_available && pw.thread_available && pw.core_available; } }; } diff --git a/include/libremidi/backends/pipewire/config.hpp b/include/libremidi/backends/pipewire/config.hpp index 69efb054..f081e543 100644 --- a/include/libremidi/backends/pipewire/config.hpp +++ b/include/libremidi/backends/pipewire/config.hpp @@ -1,44 +1,36 @@ #pragma once #include -#include #include -#include #include extern "C" { +struct pw_thread_loop; struct pw_main_loop; -struct pw_filter; -struct spa_io_position; +struct pw_core; } NAMESPACE_LIBREMIDI { -using pipewire_callback_function = std::function; -struct pipewire_callback -{ - int64_t token; - pipewire_callback_function callback; -}; - +// To embed in a host that owns the pipewire setup, supply `core` plus +// one of `thread_loop` / `main_loop` — libremidi adopts them without +// taking ownership. All-null falls back to the shared singleton. 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{}; + pw_thread_loop* thread_loop{}; + pw_main_loop* main_loop{}; + pw_core* core{}; }; 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{}; + pw_thread_loop* thread_loop{}; + pw_main_loop* main_loop{}; + pw_core* core{}; int64_t output_buffer_size{65536}; }; @@ -47,7 +39,8 @@ struct pipewire_observer_configuration { std::string client_name = "libremidi client"; - pw_main_loop* context{}; + pw_thread_loop* thread_loop{}; + pw_main_loop* main_loop{}; + pw_core* core{}; }; - } diff --git a/include/libremidi/backends/pipewire/context.hpp b/include/libremidi/backends/pipewire/context.hpp deleted file mode 100644 index 450ea04d..00000000 --- a/include/libremidi/backends/pipewire/context.hpp +++ /dev/null @@ -1,620 +0,0 @@ -#pragma once -#include -#include -#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{std::move(inst)} - , owns_main_loop{true} - { - this->main_loop = pw.main_loop_new(nullptr); - if (!this->main_loop) - { - // libremidi::logger().libremidi_handle_error("main_loop_new failed!"); - return; - } - initialize(); - } - - void initialize() - { - this->lp = pw.main_loop_get_loop(this->main_loop); - if (!lp) - { - // libremidi::logger().libremidi_handle_error("main_loop_get_loop failed!"); - return; - } - - this->context = pw.context_new(lp, nullptr, 0); - if (!this->context) - { - // libremidi::logger().libremidi_handle_error("context_new failed!"); - return; - } - - this->core = pw.context_connect(this->context, nullptr, 0); - if (!this->core) - { - // libremidi::logger().libremidi_handle_error("context_connect failed!"); - return; - } - - this->registry = pw_core_get_registry(this->core, PW_VERSION_REGISTRY, 0); - if (!this->registry) - { - // libremidi::logger().libremidi_handle_error("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_enter(this->lp); - pw_loop_iterate(this->lp, 1); - pw_loop_leave(this->lp); - } - - 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(uint64_t out_port, uint64_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) - { - 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("UMP") != 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]; - else if (p.format.find("UMP") != 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{std::move(loop)} - { - } - - explicit pipewire_filter(std::shared_ptr loop, pw_filter* filter) - : loop{std::move(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, "", -#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); - } - - 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( - this->filter, - direction, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - sizeof(struct port), - pw.properties_new( - PW_KEY_FORMAT_DSP, format, - PW_KEY_PORT_NAME, port_name.data(), - nullptr), - nullptr, 0); - // clang-format on - if (!port) - return std::errc::invalid_argument; - return stdx::error{}; - } - - void set_port_buffer(int64_t 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); - } - - stdx::error remove_port() - { - assert(this->port); - int ret = pw.filter_remove_port(this->port); - this->port = nullptr; - return from_errc(ret); - } - - stdx::error rename_port(std::string_view port_name) - { - if (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); - int ret = pw.filter_update_properties(this->filter, this->port, &properties); - return from_errc(ret); - } - else - { - return std::errc::not_connected; - } - } - - [[nodiscard]] stdx::error start_filter() - { - if (int ret = pw.filter_connect(this->filter, PW_FILTER_FLAG_RT_PROCESS, NULL, 0); ret < 0) - { - return from_errc(ret); - } - else - { - return stdx::error{}; - } - } - - 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 index 6e1edb43..5225ae23 100644 --- a/include/libremidi/backends/pipewire/helpers.hpp +++ b/include/libremidi/backends/pipewire/helpers.hpp @@ -1,15 +1,24 @@ #pragma once -#include -#include +#include +#include +#include +#include #include -#include -#include #include -#include -#include -#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include #if defined(__clang__) #pragma clang diagnostic push @@ -20,125 +29,58 @@ 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{}; + using context_t = libremidi::pipewire::context; + using filter_t = libremidi::pipewire::filter; + using filter_config_t = libremidi::pipewire::filter_config; + using port_token_t = libremidi::pipewire::port_token; - int64_t this_instance{}; + const libremidi::pipewire::api& pw = libremidi::pipewire::load(); - 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}; + std::shared_ptr ctx; + std::unique_ptr flt; + port_token_t port{}; + pw_proxy* link{}; - pipewire_helpers() - { - static std::atomic_int64_t instance{}; - this_instance = ++instance; + libremidi::pipewire::subscription sub_added; + libremidi::pipewire::subscription sub_removed; - fds[1] = termination_event; - } + // on_port_removed only delivers the id; cache the full info so the + // user-facing callback gets what it expects. Loop-thread only. + std::unordered_map port_cache; template - stdx::error create_filter(Self& self) + stdx::error create_context(Self& self) { - if (this->filter) + if (this->ctx) return stdx::error{}; auto& configuration = self.configuration; - if (configuration.context && configuration.filter && configuration.set_process_func) - { - this->filter = std::make_unique(this->global_context, configuration.filter); - - 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); - 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); - return this->filter->start_filter(); - } - return stdx::error{}; - } - - template - void destroy_filter(Self& self) - { - assert(global_context); - if (!global_context->owns_main_loop) + if (configuration.core) { - if (self.configuration.clear_process_func) + if (configuration.thread_loop) { - self.configuration.clear_process_func(this_instance); + this->ctx = context_t::borrow(configuration.thread_loop, configuration.core); } - } - else - { - if (this->filter) + else if (configuration.main_loop) { - this->filter->destroy(); + this->ctx = context_t::borrow(configuration.main_loop, configuration.core); + } + else + { + return std::errc::invalid_argument; } } - - this->filter.reset(); - } - - template - stdx::error create_context(Self& self) - { - if (this->global_context) - return stdx::error{}; - - // Initialize PipeWire client - auto& configuration = self.configuration; - if (configuration.context) + else if (configuration.thread_loop || configuration.main_loop) { - this->global_context = std::make_shared(configuration.context); + return std::errc::invalid_argument; } else { - this->global_instance = std::make_shared(); - this->global_context = std::make_shared(this->global_instance); + this->ctx = libremidi::pipewire::shared_context(); } - if (!this->global_context->main_loop) + + if (!this->ctx || !this->ctx->ok()) return std::errc::connection_refused; return stdx::error{}; @@ -146,289 +88,309 @@ struct pipewire_helpers void destroy_context() { - assert(this->global_context); - this->global_context.reset(); - this->global_instance.reset(); + sub_added.reset(); + sub_removed.reset(); + port_cache.clear(); + this->ctx.reset(); } - void run_poll_loop() - try + template + stdx::error create_filter(Self& self) { - // Note: called from a std::jthread. - assert(this->global_context); - auto lp = this->global_context->lp; - if (!lp) - return; + if (this->flt) + return stdx::error{}; + assert(this->ctx); + + static constexpr pw_filter_events filter_events = { + .version = PW_VERSION_FILTER_EVENTS, + .destroy = nullptr, + .state_changed = nullptr, + .io_changed = nullptr, + .param_changed = nullptr, + .add_buffer = nullptr, + .remove_buffer = nullptr, + .process = + +[](void* data, struct spa_io_position* position) -> void { + auto& s = *static_cast(data); + s.process(position); + }, + .drained = nullptr, +#if PW_VERSION_FILTER_EVENTS >= 1 + .command = nullptr, +#endif + }; - if (int fd = this->global_context->get_fd(); fd != -1) + filter_config_t cfg; + cfg.name = self.configuration.client_name; + cfg.media_type = "Midi"; + cfg.media_category = "Filter"; + cfg.media_role = "DSP"; + cfg.always_process = true; + cfg.pause_on_idle = false; + cfg.suspend_on_idle = false; + cfg.lock_rate = true; + cfg.rt_process = true; + + this->flt = std::make_unique( + this->ctx, std::move(cfg), filter_events, static_cast(&self)); + if (!this->flt->ok()) { - fds[0] = {.fd = fd, .events = POLLIN, .revents = 0}; - current_state = poll_state::in_poll; - - // pw_loop_iterate requires the loop to be entered first. - pw_loop_enter(lp); - - for (;;) - { - if (int err = poll(fds, 2, -1); err < 0) - { - if (err == -EAGAIN) - continue; - else - break; - } - - // Check pipewire fd: - if (fds[0].revents & POLLIN) - { - int result = pw_loop_iterate(lp, 0); - if (result < 0) - { - LIBREMIDI_LOG(spa_strerror(result)); - } - fds[0].revents = 0; - } - - // Check exit fd: - if (fds[1].revents & POLLIN) - { - break; - } - } + this->flt.reset(); + return std::errc::connection_refused; + } - pw_loop_leave(lp); + if (int rc = this->flt->start(); rc < 0) + { + this->flt.reset(); + return std::errc::connection_refused; } - current_state = poll_state::not_in_poll; + + return stdx::error{}; } - catch (...) + + template + void destroy_filter(Self&) { - current_state = poll_state::not_in_poll; + if (!this->flt) + return; + if (this->port.valid()) + { + this->flt->remove_port(this->port); + this->port = {}; + } + this->flt.reset(); } template stdx::error create_local_port( Self& self, std::string_view portName, spa_direction direction, const char* format) { - assert(this->global_context); - assert(this->filter); + assert(this->flt); + if (this->port.valid()) + return stdx::error{}; if (portName.empty()) portName = direction == SPA_DIRECTION_INPUT ? "i" : "o"; - if (!this->filter->port) + this->port = this->flt->add_port(direction, portName, format); + if (!this->port.valid()) { - auto ret = this->filter->create_local_port(portName.data(), direction, format); - if (ret != stdx::error{}) - { - self.libremidi_handle_error(self.configuration, "error creating port"); - return ret; - } + self.libremidi_handle_error(self.configuration, "error creating port"); + return std::errc::invalid_argument; } + // Wait for the daemon to assign our node an id. + this->flt->synchronize_node(); return stdx::error{}; } - template - void add_callbacks(std::string format, const observer_configuration& conf) + void set_port_buffer(std::int64_t bytes) { - assert(global_context); - 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; - 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 = [format, &conf](const pipewire_context::port_info& port) { - if (port.format.find(format) == std::string::npos) - return; + if (!this->flt || !this->port.valid()) + 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)); - } - } + std::uint8_t buffer[1024]; + spa_pod_builder builder; + spa_pod_builder_init(&builder, buffer, sizeof(buffer)); + + // clang-format off + const 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(static_cast(bytes), 4096, INT32_MAX), + SPA_PARAM_BUFFERS_stride, SPA_POD_Int(1) + ) }; - } - - void start_thread() - { - if (!this->global_context->owns_main_loop) - return; + // clang-format on - current_state = poll_state::start_poll; - main_loop_thread = std::jthread{[this]() { run_poll_loop(); }}; + this->flt->update_params(this->port, params, 1); } - void stop_thread() + template + void add_callbacks(std::string format, const observer_configuration& conf) { - 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(); - } + assert(this->ctx); + + sub_added = this->ctx->on_port_added( + [this, format, &conf](const libremidi::pipewire::port_info& port) { + if (port.format.find(format) == std::string::npos) + return; + this->port_cache[port.id] = port; + + bool unfiltered = conf.track_any; + unfiltered |= (port.physical && conf.track_hardware); + unfiltered |= (!port.physical && conf.track_virtual); + if (!unfiltered) + return; + 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)); + } + }); + + sub_removed = this->ctx->on_port_removed( + [this, format, &conf](std::uint32_t port_id) { + auto it = this->port_cache.find(port_id); + if (it == this->port_cache.end()) + return; + const auto port = it->second; + this->port_cache.erase(it); + + if (port.format.find(format) == std::string::npos) + return; + bool unfiltered = conf.track_any; + unfiltered |= (port.physical && conf.track_hardware); + unfiltered |= (!port.physical && conf.track_virtual); + if (!unfiltered) + return; + 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)); + } + }); } + // Shared context owns the thread loop; these are call-site shims. + void start_thread() noexcept { } + void stop_thread() noexcept { } + stdx::error do_close_port() { - if (!this->filter) + if (!this->flt || !this->port.valid()) return stdx::error{}; - if (!this->filter->port) - return stdx::error{}; - - if (!this->global_context->owns_main_loop) - { - this->canary.reset(); - this->thread_lock.prepare_release_client(); - } - unlink_ports(); - return this->filter->remove_port(); + this->flt->remove_port(this->port); + this->port = {}; + return stdx::error{}; } stdx::error rename_port(std::string_view port_name) { - if (this->filter) - { - return this->filter->rename_port(port_name); - } - else - { + if (!this->flt || !this->port.valid() || !this->ctx) return std::errc::not_connected; - } + + spa_dict_item items[1] = { + SPA_DICT_ITEM_INIT(PW_KEY_PORT_NAME, port_name.data()), + }; + auto properties = SPA_DICT_INIT(items, 1); + this->ctx->with_lock([&] { + if (pw.filter_update_properties) + pw.filter_update_properties(this->flt->handle(), this->port.opaque, &properties); + }); + return stdx::error{}; } void unlink_ports() { - if (link) + if (link && this->ctx) { - this->global_context->unlink_ports(link); + libremidi::pipewire::unlink_ports(*this->ctx, link); link = nullptr; } } + // Polls because the daemon may not have echoed our node + ports + // back yet; synchronize() drives it forward. Bounded for safety. stdx::error 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(); - if (this->filter->filter_node_id() != 4294967295) - break; - } + if (!this->flt || !this->ctx) + return std::errc::not_connected; - 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()) + this->flt->synchronize_node(); + const auto this_node_id = this->flt->node_id(); + if (this_node_id == PW_ID_ANY) return std::errc::invalid_argument; - // 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()) + libremidi::pipewire::port_info our_port{}; + bool found = false; + for (int i = 0; i < 200 && !found; ++i) + { + auto snap = this->ctx->snapshot(); + for (const auto& n : snap.nodes) + { + if (n.id == this_node_id && !n.inputs.empty()) + { + our_port = n.inputs.front(); + found = true; + break; + } + } + if (!found) + this->ctx->synchronize(); + } + if (!found) return std::errc::no_link; - // 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); + link = libremidi::pipewire::link_ports(*this->ctx, in_port.port, our_port.id); if (!link) { self.libremidi_handle_error( self.configuration, - "could not connect to port: " + in_port.port_name + " -> " + p.port_name); + "could not connect to port: " + in_port.port_name + " -> " + our_port.port_name); return std::errc::no_link; } - return stdx::error{}; } stdx::error 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(); - if (this->filter->filter_node_id() != 4294967295) - break; - } + if (!this->flt || !this->ctx) + return std::errc::not_connected; - 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()) - { + this->flt->synchronize_node(); + const auto this_node_id = this->flt->node_id(); + if (this_node_id == PW_ID_ANY) return std::errc::invalid_argument; - } - // 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()) + libremidi::pipewire::port_info our_port{}; + bool found = false; + for (int i = 0; i < 200 && !found; ++i) { - return std::errc::no_link; + auto snap = this->ctx->snapshot(); + for (const auto& n : snap.nodes) + { + if (n.id == this_node_id && !n.outputs.empty()) + { + our_port = n.outputs.front(); + found = true; + break; + } + } + if (!found) + this->ctx->synchronize(); } + if (!found) + return std::errc::no_link; - // 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); + link = libremidi::pipewire::link_ports(*this->ctx, our_port.id, out_port.port); if (!link) { self.libremidi_handle_error( self.configuration, - "could not connect to port: " + p.port_name + " -> " + out_port.port_name); + "could not connect to port: " + our_port.port_name + " -> " + out_port.port_name); return std::errc::no_link; } - return stdx::error{}; } template - static auto to_port_info(const pipewire_context::port_info& port) + static auto to_port_info(const libremidi::pipewire::port_info& port) -> std::conditional_t { std::string device_name, port_name; @@ -454,43 +416,32 @@ 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 + // The Direction template parameter is in pipewire's sense, so a + // user-facing input port carries Direction=OUTPUT. template static auto get_ports( - std::string_view format, const observer_configuration& conf, - const pipewire_context& ctx) noexcept + std::string_view format, const observer_configuration& conf, const context_t& ctx) noexcept -> std::vector< std::conditional_t> { std::vector> ret; + const auto snap = ctx.snapshot(); + for (const auto& node : snap.nodes) { - std::lock_guard _{ctx.current_graph.mtx}; - if (conf.track_any || conf.track_hardware) - for (auto& node : ctx.current_graph.physical_midi) - { - for (auto& port : - (Direction == SPA_DIRECTION_INPUT ? node.second.inputs : node.second.outputs)) - { - if (port.format.find(format) != std::string::npos) - ret.push_back(to_port_info(port)); - } - } - - if (conf.track_any || conf.track_virtual) - for (auto& node : ctx.current_graph.software_midi) - { - for (auto& port : - (Direction == SPA_DIRECTION_INPUT ? node.second.inputs : node.second.outputs)) - { - if (port.format.find(format) != std::string::npos) - ret.push_back(to_port_info(port)); - } - } + const auto& bucket = (Direction == SPA_DIRECTION_INPUT ? node.inputs : node.outputs); + for (const auto& port : bucket) + { + if (port.format.find(format) == std::string::npos) + continue; + bool unfiltered = conf.track_any; + unfiltered |= (port.physical && conf.track_hardware); + unfiltered |= (!port.physical && conf.track_virtual); + if (unfiltered) + ret.push_back(to_port_info(port)); + } } - return ret; } }; diff --git a/include/libremidi/backends/pipewire/midi_in.hpp b/include/libremidi/backends/pipewire/midi_in.hpp index 6d0ae97c..e0e13e24 100644 --- a/include/libremidi/backends/pipewire/midi_in.hpp +++ b/include/libremidi/backends/pipewire/midi_in.hpp @@ -4,6 +4,11 @@ #include #include +#include +#include +#include +#include + NAMESPACE_LIBREMIDI { class midi_in_pipewire final @@ -86,9 +91,9 @@ class midi_in_pipewire final .has_samples = true, }; - assert(this->filter); - assert(this->filter->port); - const auto b = pw.filter_dequeue_buffer(this->filter->port); + assert(this->flt); + assert(this->port.valid()); + const auto b = pw.filter_dequeue_buffer(this->port.opaque); if (!b) return; @@ -122,7 +127,7 @@ class midi_in_pipewire final {data, data + size}, m_processing.timestamp(to_ns, c->offset)); } - pw.filter_queue_buffer(this->filter->port, b); + pw.filter_queue_buffer(this->port.opaque, 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 index 38bf1c2c..cf6d5506 100644 --- a/include/libremidi/backends/pipewire/midi_out.hpp +++ b/include/libremidi/backends/pipewire/midi_out.hpp @@ -3,6 +3,10 @@ #include #include +#include +#include +#include + #include NAMESPACE_LIBREMIDI @@ -19,7 +23,8 @@ class midi_out_pipewire { } configuration; - midi_out_pipewire(output_configuration&& conf, pipewire_output_configuration&& apiconf) + midi_out_pipewire( + output_configuration&& conf, pipewire_output_configuration&& apiconf) : configuration{std::move(conf), std::move(apiconf)} { if (auto ret = create_context(*this); ret != stdx::error{}) @@ -52,7 +57,7 @@ class midi_out_pipewire err != stdx::error{}) return err; - this->filter->set_port_buffer(configuration.output_buffer_size); + this->set_port_buffer(configuration.output_buffer_size); if (auto err = link_ports(*this, out_port); err != stdx::error{}) return err; @@ -67,7 +72,7 @@ class midi_out_pipewire err != stdx::error{}) return err; - this->filter->set_port_buffer(configuration.output_buffer_size); + this->set_port_buffer(configuration.output_buffer_size); start_thread(); return stdx::error{}; @@ -84,7 +89,7 @@ class midi_out_pipewire 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); + const auto b = pw.filter_dequeue_buffer(this->port.opaque); if (!b) return 1; @@ -140,11 +145,11 @@ class midi_out_pipewire d->chunk->size = n_fill_frames; b->size = n_fill_frames; - pw.filter_queue_buffer(this->filter->port, b); + pw.filter_queue_buffer(this->port.opaque, b); return 0; } - pw.filter_flush(this->filter->filter, true); + pw.filter_flush(this->flt->handle(), true); return 0; } diff --git a/include/libremidi/backends/pipewire/observer.hpp b/include/libremidi/backends/pipewire/observer.hpp index acc87a3d..285d954b 100644 --- a/include/libremidi/backends/pipewire/observer.hpp +++ b/include/libremidi/backends/pipewire/observer.hpp @@ -3,8 +3,6 @@ #include #include -#include - NAMESPACE_LIBREMIDI { class observer_pipewire final @@ -23,47 +21,45 @@ class observer_pipewire final observer_configuration&& conf, pipewire_observer_configuration&& apiconf) : configuration{std::move(conf), std::move(apiconf)} { - create_context(*this); + if (auto err = create_context(*this); err != stdx::error{}) + return; - // FIXME port rename callback - { + // Hold thread_loop_lock so a port arriving between subscription + // install and the snapshot walk doesn't fire twice. + this->ctx->with_lock([this] { this->add_callbacks("midi", 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.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); - } + 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; } std::vector get_input_ports() const noexcept override { + if (!this->ctx) + return {}; return get_ports( - "midi", this->configuration, *this->global_context); + "midi", this->configuration, *this->ctx); } std::vector get_output_ports() const noexcept override { + if (!this->ctx) + return {}; return get_ports( - "midi", this->configuration, *this->global_context); - } - - ~observer_pipewire() - { - stop_thread(); - destroy_context(); + "midi", this->configuration, *this->ctx); } - std::unordered_set seen_input_ports; - std::unordered_set seen_output_ports; + ~observer_pipewire() { destroy_context(); } }; } diff --git a/include/libremidi/backends/pipewire/shared_handler.hpp b/include/libremidi/backends/pipewire/shared_handler.hpp deleted file mode 100644 index a6e531ac..00000000 --- a/include/libremidi/backends/pipewire/shared_handler.hpp +++ /dev/null @@ -1,150 +0,0 @@ -#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(*get_if(&ev.payload))); - break; - case in_callback_removed: { - auto idx = *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(*get_if(&ev.payload))); - break; - case out_callback_removed: - auto idx = *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; - libremidi_variant_alias::variant payload; - }; - - boost::lockfree::spsc_queue events{16}; - - std::vector midiin_callbacks; - std::vector midiout_callbacks; -}; -} -#endif diff --git a/include/libremidi/backends/pipewire_ump.hpp b/include/libremidi/backends/pipewire_ump.hpp index eed0a7ec..d45fb5f2 100644 --- a/include/libremidi/backends/pipewire_ump.hpp +++ b/include/libremidi/backends/pipewire_ump.hpp @@ -1,6 +1,6 @@ #pragma once #include -#include +#include #include #include #include @@ -24,9 +24,11 @@ struct backend static inline bool available() noexcept { - static const libpipewire& pw = libpipewire::instance(); - if (!pw.available) + auto& pw = libremidi::pipewire::load(); + if (!pw.filter_available || !pw.thread_available || !pw.core_available + || !pw.get_library_version) return false; + // UMP requires libpipewire >= 0.3.4x. const std::string_view version = pw.get_library_version(); if (version.size() >= 3) { diff --git a/include/libremidi/backends/pipewire_ump/config.hpp b/include/libremidi/backends/pipewire_ump/config.hpp index ab596840..3db6ad70 100644 --- a/include/libremidi/backends/pipewire_ump/config.hpp +++ b/include/libremidi/backends/pipewire_ump/config.hpp @@ -4,24 +4,23 @@ NAMESPACE_LIBREMIDI::pipewire_ump { +// Same borrow-mode contract as pipewire_input_configuration. 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{}; + pw_thread_loop* thread_loop{}; + pw_main_loop* main_loop{}; + pw_core* core{}; }; 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{}; + pw_thread_loop* thread_loop{}; + pw_main_loop* main_loop{}; + pw_core* core{}; int64_t output_buffer_size{65536}; }; @@ -30,7 +29,8 @@ struct observer_configuration { std::string client_name = "libremidi client"; - pw_main_loop* context{}; + pw_thread_loop* thread_loop{}; + pw_main_loop* main_loop{}; + pw_core* core{}; }; - } diff --git a/include/libremidi/backends/pipewire_ump/midi_in.hpp b/include/libremidi/backends/pipewire_ump/midi_in.hpp index 05b112b3..8c345dbc 100644 --- a/include/libremidi/backends/pipewire_ump/midi_in.hpp +++ b/include/libremidi/backends/pipewire_ump/midi_in.hpp @@ -4,6 +4,11 @@ #include #include +#include +#include +#include +#include + NAMESPACE_LIBREMIDI::pipewire_ump { class midi_in_pipewire final @@ -44,7 +49,10 @@ class midi_in_pipewire final client_open_ = std::errc::not_connected; } - libremidi::API get_current_api() const noexcept override { return libremidi::API::PIPEWIRE_UMP; } + 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 { @@ -75,7 +83,10 @@ class midi_in_pipewire final return do_close_port(); } - stdx::error set_port_name(std::string_view port_name) override { return rename_port(port_name); } + 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(); } @@ -87,9 +98,9 @@ class midi_in_pipewire final .has_samples = true, }; - assert(this->filter); - assert(this->filter->port); - const auto b = pw.filter_dequeue_buffer(this->filter->port); + assert(this->flt); + assert(this->port.valid()); + const auto b = pw.filter_dequeue_buffer(this->port.opaque); if (!b) return; @@ -124,7 +135,7 @@ class midi_in_pipewire final m_processing.timestamp(to_ns, c->offset)); } - pw.filter_queue_buffer(this->filter->port, b); + pw.filter_queue_buffer(this->port.opaque, 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 index 1c2bbffa..69558427 100644 --- a/include/libremidi/backends/pipewire_ump/midi_out.hpp +++ b/include/libremidi/backends/pipewire_ump/midi_out.hpp @@ -3,6 +3,10 @@ #include #include +#include +#include +#include + #include NAMESPACE_LIBREMIDI::pipewire_ump @@ -46,7 +50,10 @@ class midi_out_pipewire client_open_ = std::errc::not_connected; } - libremidi::API get_current_api() const noexcept override { return libremidi::API::PIPEWIRE_UMP; } + 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 { @@ -54,7 +61,7 @@ class midi_out_pipewire err != stdx::error{}) return err; - this->filter->set_port_buffer(configuration.output_buffer_size); + this->set_port_buffer(configuration.output_buffer_size); if (auto err = link_ports(*this, out_port); err != stdx::error{}) return err; @@ -69,7 +76,7 @@ class midi_out_pipewire err != stdx::error{}) return err; - this->filter->set_port_buffer(configuration.output_buffer_size); + this->set_port_buffer(configuration.output_buffer_size); start_thread(); return stdx::error{}; @@ -81,12 +88,15 @@ class midi_out_pipewire return do_close_port(); } - stdx::error set_port_name(std::string_view port_name) override { return rename_port(port_name); } + 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); + const auto b = pw.filter_dequeue_buffer(this->port.opaque); if (!b) return 1; @@ -128,11 +138,11 @@ class midi_out_pipewire d->chunk->size = n_fill_frames; b->size = n_fill_frames; - pw.filter_queue_buffer(this->filter->port, b); + pw.filter_queue_buffer(this->port.opaque, b); return 0; } - pw.filter_flush(this->filter->filter, true); + pw.filter_flush(this->flt->handle(), true); return 0; } diff --git a/include/libremidi/backends/pipewire_ump/observer.hpp b/include/libremidi/backends/pipewire_ump/observer.hpp index e3b1bd87..c21c21e9 100644 --- a/include/libremidi/backends/pipewire_ump/observer.hpp +++ b/include/libremidi/backends/pipewire_ump/observer.hpp @@ -3,8 +3,6 @@ #include #include -#include - NAMESPACE_LIBREMIDI::pipewire_ump { class observer_pipewire final @@ -24,47 +22,47 @@ class observer_pipewire final libremidi::pipewire_ump::observer_configuration&& apiconf) : configuration{std::move(conf), std::move(apiconf)} { - create_context(*this); + if (auto err = create_context(*this); err != stdx::error{}) + return; - // FIXME port rename callback - { + // Atomic subscribe + initial walk (see pipewire/observer.hpp). + this->ctx->with_lock([this] { 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.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); - } + 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; } + libremidi::API get_current_api() const noexcept override + { + return libremidi::API::PIPEWIRE_UMP; + } std::vector get_input_ports() const noexcept override { + if (!this->ctx) + return {}; return get_ports( - "UMP", this->configuration, *this->global_context); + "UMP", this->configuration, *this->ctx); } std::vector get_output_ports() const noexcept override { + if (!this->ctx) + return {}; return get_ports( - "UMP", this->configuration, *this->global_context); - } - - ~observer_pipewire() - { - stop_thread(); - destroy_context(); + "UMP", this->configuration, *this->ctx); } - std::unordered_set seen_input_ports; - std::unordered_set seen_output_ports; + ~observer_pipewire() { destroy_context(); } }; } diff --git a/include/libremidi/libremidi.hpp b/include/libremidi/libremidi.hpp index 40e6885f..3196ea38 100644 --- a/include/libremidi/libremidi.hpp +++ b/include/libremidi/libremidi.hpp @@ -273,6 +273,8 @@ class LIBREMIDI_EXPORT midi_out #include #include #include + // pipewire {instance,context}.cpp are pulled by their .hpp under + // HEADER_ONLY so any TU touching them gets the definitions. #if defined(__EMSCRIPTEN__) #include diff --git a/src/libremidi.ixx b/src/libremidi.ixx index dbf6739c..5472cc6c 100644 --- a/src/libremidi.ixx +++ b/src/libremidi.ixx @@ -137,6 +137,7 @@ module; #include #include #include +#include #include #include #include @@ -246,3 +247,8 @@ module :private; #include #include // #include + +#if defined(LIBREMIDI_PIPEWIRE) +#include +#include +#endif From 424d5de99e4135b99f8ce54979235cad41a46c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Mon, 1 Jun 2026 17:00:08 -0400 Subject: [PATCH 2/2] ci: bump clang-20 matrix entry to clang-22 --- .github/workflows/build_cmake.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_cmake.yml b/.github/workflows/build_cmake.yml index 7040bdcb..38b872bb 100644 --- a/.github/workflows/build_cmake.yml +++ b/.github/workflows/build_cmake.yml @@ -65,10 +65,10 @@ jobs: examples: 1 } - { - name: "Ubuntu clang-20 libstdc++", + name: "Ubuntu clang-22 libstdc++", os: ubuntu-latest, generator: "", - cmakeflags: "-DCMAKE_CXX_COMPILER=clang++-20 -DCMAKE_CXX_FLAGS='-Werror=return-type -fsanitize=address -fsanitize=undefined' -DBOOST_ROOT=$PWD/boost_1_90_0", + cmakeflags: "-DCMAKE_CXX_COMPILER=clang++-22 -DCMAKE_CXX_FLAGS='-Werror=return-type -fsanitize=address -fsanitize=undefined' -DBOOST_ROOT=$PWD/boost_1_90_0", test_target: "test", tests: 1, examples: 1