#pragma once #include #include #include #include #include #include #include #include #include #if defined(__clang__) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wsign-compare" #endif NAMESPACE_LIBREMIDI { struct pipewire_helpers { struct port { void* data{}; }; // All pipewire operations have to happen in the same thread // - and pipewire checks that internally. std::jthread main_loop_thread; const libpipewire& pw = libpipewire::instance(); std::shared_ptr global_instance; std::shared_ptr global_context; std::unique_ptr filter; pw_proxy* link{}; int64_t this_instance{}; eventfd_notifier termination_event{}; pollfd fds[2]{}; semaphore_pair_lock thread_lock; std::shared_ptr canary = std::make_shared(); enum poll_state { start_poll, in_poll, not_in_poll }; std::atomic current_state{not_in_poll}; pipewire_helpers() { static std::atomic_int64_t instance{}; this_instance = ++instance; fds[1] = termination_event; } template stdx::error create_filter(Self& self) { if (this->filter) 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 (self.configuration.clear_process_func) { self.configuration.clear_process_func(this_instance); } } else { if (this->filter) { this->filter->destroy(); } } 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) { this->global_context = std::make_shared(configuration.context); } else { this->global_instance = std::make_shared(); this->global_context = std::make_shared(this->global_instance); } if (!this->global_context->main_loop) return std::errc::connection_refused; return stdx::error{}; } void destroy_context() { assert(this->global_context); this->global_context.reset(); this->global_instance.reset(); } void run_poll_loop() try { // Note: called from a std::jthread. assert(this->global_context); if (int fd = this->global_context->get_fd(); fd != -1) { fds[0] = {.fd = fd, .events = POLLIN, .revents = 0}; current_state = poll_state::in_poll; for (;;) { if (int err = poll(fds, 2, -1); err < 0) { if (err == -EAGAIN) continue; else break; } // Check pipewire fd: if (fds[0].revents & POLLIN) { if (auto lp = this->global_context->lp) { int result = pw_loop_iterate(lp, 0); if (result < 0) { LIBREMIDI_LOG(spa_strerror(result)); } } fds[0].revents = 0; } // Check exit fd: if (fds[1].revents & POLLIN) { break; } } } current_state = poll_state::not_in_poll; } catch (...) { current_state = poll_state::not_in_poll; } template stdx::error create_local_port( Self& self, std::string_view portName, spa_direction direction, const char* format) { assert(this->global_context); assert(this->filter); if (portName.empty()) portName = direction == SPA_DIRECTION_INPUT ? "i" : "o"; if (!this->filter->port) { 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; } } return stdx::error{}; } template void add_callbacks(std::string format, const observer_configuration& conf) { 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; bool unfiltered = conf.track_any; unfiltered |= (port.physical && conf.track_hardware); unfiltered |= (!port.physical && conf.track_virtual); if (unfiltered) { if (port.direction == SPA_DIRECTION_INPUT) { if (conf.output_removed) conf.output_removed(to_port_info(port)); } else { if (conf.input_removed) conf.input_removed(to_port_info(port)); } } }; } void start_thread() { if (!this->global_context->owns_main_loop) return; current_state = poll_state::start_poll; main_loop_thread = std::jthread{[this]() { run_poll_loop(); }}; } void stop_thread() { assert(this->global_context); if (!this->global_context->owns_main_loop) return; if (main_loop_thread.joinable() || current_state != poll_state::not_in_poll) { termination_event.notify(); main_loop_thread.request_stop(); termination_event.notify(); for (int i = 0; i < 100; i++) { if (current_state == poll_state::not_in_poll) break; std::this_thread::sleep_for(std::chrono::milliseconds(10)); termination_event.notify(); } if (main_loop_thread.joinable()) main_loop_thread.join(); } } stdx::error do_close_port() { if (!this->filter) 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(); } stdx::error rename_port(std::string_view port_name) { if (this->filter) { return this->filter->rename_port(port_name); } else { return std::errc::not_connected; } } void unlink_ports() { if (link) { this->global_context->unlink_ports(link); link = nullptr; } } 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(); 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()) 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()) 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); if (!link) { self.libremidi_handle_error( self.configuration, "could not connect to port: " + in_port.port_name + " -> " + p.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(); 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()) { 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()) { 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); if (!link) { self.libremidi_handle_error( self.configuration, "could not connect to port: " + p.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) -> std::conditional_t { std::string device_name, port_name; auto name_colon = port.port_alias.find(':'); if (name_colon != std::string::npos) { device_name = port.port_alias.substr(0, name_colon); port_name = port.port_alias.substr(name_colon + 1); } else { port_name = port.port_alias; } return {{ .api = Api, .client = 0, .port = port.id, .manufacturer = "", .device_name = device_name, .port_name = port.port_name, .display_name = port_name, }}; } // Note: keep in mind that an "input" port for us (e.g. a keyboard that goes to the computer) // is an "output" port from the point of view of pipewire as data will come out of it template static auto get_ports( std::string_view format, const observer_configuration& conf, const pipewire_context& ctx) noexcept -> std::vector< std::conditional_t> { std::vector> ret; { 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)); } } } return ret; } }; } #if defined(__clang__) #pragma clang diagnostic pop #endif