very, very simple WP with midi CC out

This commit is contained in:
Sebastian
2024-10-09 02:01:23 +02:00
commit 3d6fc151f1
126 changed files with 19539 additions and 0 deletions
@@ -0,0 +1,464 @@
#pragma once
#include <libremidi/backends/linux/helpers.hpp>
#include <libremidi/backends/linux/pipewire.hpp>
#include <libremidi/backends/pipewire/context.hpp>
#include <libremidi/detail/memory.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/semaphore.hpp>
#include <atomic>
#include <semaphore>
#include <stop_token>
#include <thread>
namespace libremidi
{
struct pipewire_helpers
{
struct port
{
void* data{};
};
// All pipewire operations have to happen in the same thread
// - and pipewire checks that internally.
std::jthread main_loop_thread;
const libpipewire& pw = libpipewire::instance();
std::shared_ptr<pipewire_instance> global_instance;
std::shared_ptr<pipewire_context> global_context;
std::unique_ptr<pipewire_filter> filter;
pw_proxy* link{};
int64_t this_instance{};
eventfd_notifier termination_event{};
pollfd fds[2]{};
semaphore_pair_lock thread_lock;
std::shared_ptr<void> canary = std::make_shared<int>();
enum poll_state
{
start_poll,
in_poll,
not_in_poll
};
std::atomic<poll_state> current_state{not_in_poll};
pipewire_helpers()
{
static std::atomic_int64_t instance{};
this_instance = ++instance;
fds[1] = termination_event;
}
template <typename Self>
void create_filter(Self& self)
{
if (this->filter)
return;
auto& configuration = self.configuration;
if (configuration.context && configuration.filter && configuration.set_process_func)
{
this->filter = std::make_unique<pipewire_filter>(this->global_context, configuration.filter);
pipewire_callback cbs{
.token = this_instance,
.callback = [&self, p = std::weak_ptr{canary}](spa_io_position* nf) -> void {
if (auto pt = p.lock())
self.process(nf);
self.thread_lock.check_client_released();
}};
configuration.set_process_func(cbs);
}
else
{
this->filter = std::make_unique<pipewire_filter>(this->global_context);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
static constexpr struct pw_filter_events filter_events
= {.version = PW_VERSION_FILTER_EVENTS,
.process = +[](void* _data, struct spa_io_position* position) -> void {
// FIXME likely we need the thread_lock check here too
Self& self = *static_cast<Self*>(_data);
self.process(position);
}};
#pragma GCC diagnostic pop
this->filter->create_filter(self.configuration.client_name, filter_events, &self);
this->filter->start_filter();
}
}
template <typename Self>
void destroy_filter(Self& self)
{
assert(global_context);
if (!global_context->owns_main_loop)
{
if (self.configuration.clear_process_func)
{
self.configuration.clear_process_func(this_instance);
}
}
else
{
if (this->filter)
{
this->filter->destroy();
}
}
this->filter.reset();
}
template <typename Self>
int create_context(Self& self)
{
if (this->global_context)
return 0;
// Initialize PipeWire client
auto& configuration = self.configuration;
if (configuration.context)
{
this->global_context = std::make_shared<pipewire_context>(configuration.context);
}
else
{
this->global_instance = std::make_shared<pipewire_instance>();
this->global_context = std::make_shared<pipewire_context>(this->global_instance);
}
return 0;
}
void destroy_context()
{
assert(this->global_context);
this->global_context.reset();
this->global_instance.reset();
}
void run_poll_loop()
try
{
// Note: called from a std::jthread.
assert(this->global_context);
if (int fd = this->global_context->get_fd(); fd != -1)
{
fds[0] = {.fd = fd, .events = POLLIN, .revents = 0};
current_state = poll_state::in_poll;
for (;;)
{
if (int err = poll(fds, 2, -1); err < 0)
{
if (err == -EAGAIN)
continue;
else
break;
}
// Check pipewire fd:
if (fds[0].revents & POLLIN)
{
if (auto lp = this->global_context->lp)
{
int result = pw_loop_iterate(lp, 0);
if (result < 0)
std::cerr << "pw_loop_iterate: " << spa_strerror(result) << "\n";
}
fds[0].revents = 0;
}
// Check exit fd:
if (fds[1].revents & POLLIN)
{
break;
}
}
}
current_state = poll_state::not_in_poll;
}
catch (...)
{
current_state = poll_state::not_in_poll;
}
template <typename Self>
bool create_local_port(Self& self, std::string_view portName, spa_direction direction)
{
assert(this->global_context);
assert(this->filter);
if (portName.empty())
portName = direction == SPA_DIRECTION_INPUT ? "i" : "o";
if (!this->filter->port)
{
this->filter->create_local_port(portName.data(), direction);
}
if (!this->filter->port)
{
self.template error<driver_error>(self.configuration, "PipeWire: error creating port");
return false;
}
return true;
}
void add_callbacks(const observer_configuration& conf)
{
assert(global_context);
global_context->on_port_added = [&conf](const pipewire_context::port_info& port) {
if (port.format.find("midi") == std::string::npos)
return;
bool unfiltered = conf.track_any;
unfiltered |= (port.physical && conf.track_hardware);
unfiltered |= (!port.physical && conf.track_virtual);
if (unfiltered)
{
if (port.direction == SPA_DIRECTION_INPUT)
{
if (conf.output_added)
conf.output_added(to_port_info<SPA_DIRECTION_INPUT>(port));
}
else
{
if (conf.input_added)
conf.input_added(to_port_info<SPA_DIRECTION_OUTPUT>(port));
}
}
};
global_context->on_port_removed = [&conf](const pipewire_context::port_info& port) {
if (port.format.find("midi") == std::string::npos)
return;
bool unfiltered = conf.track_any;
unfiltered |= (port.physical && conf.track_hardware);
unfiltered |= (!port.physical && conf.track_virtual);
if (unfiltered)
{
if (port.direction == SPA_DIRECTION_INPUT)
{
if (conf.output_removed)
conf.output_removed(to_port_info<SPA_DIRECTION_INPUT>(port));
}
else
{
if (conf.input_removed)
conf.input_removed(to_port_info<SPA_DIRECTION_OUTPUT>(port));
}
}
};
}
void start_thread()
{
if (!this->global_context->owns_main_loop)
return;
current_state = poll_state::start_poll;
main_loop_thread = std::jthread{[this]() { run_poll_loop(); }};
}
void stop_thread()
{
assert(this->global_context);
if (!this->global_context->owns_main_loop)
return;
if (main_loop_thread.joinable() || current_state != poll_state::not_in_poll)
{
termination_event.notify();
main_loop_thread.request_stop();
termination_event.notify();
for (int i = 0; i < 100; i++)
{
if (current_state == poll_state::not_in_poll)
break;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
termination_event.notify();
}
if (main_loop_thread.joinable())
main_loop_thread.join();
}
}
void do_close_port()
{
if (!this->filter)
return;
if (!this->filter->port)
return;
if (!this->global_context->owns_main_loop)
{
this->canary.reset();
this->thread_lock.prepare_release_client();
}
unlink_ports();
this->filter->remove_port();
}
void rename_port(std::string_view port_name)
{
if (this->filter)
this->filter->rename_port(port_name);
}
void unlink_ports()
{
if (link)
{
this->global_context->unlink_ports(link);
link = nullptr;
}
}
bool link_ports(auto& self, const input_port& in_port)
{
// Wait for the pipewire server to send us back our node's info
for (int i = 0; i < 1000; i++)
this->filter->synchronize_node();
auto this_node = this->filter->filter_node_id();
auto& midi = this->global_context->current_graph.software_midi;
auto node_it = midi.find(this_node);
if (node_it == midi.end())
{
std::cerr << "Node " << this_node << " not found! \n";
return false;
}
// Wait for the pipewire server to send us back our node's ports
this->filter->synchronize_ports(node_it->second);
if (node_it->second.inputs.empty())
{
std::cerr << "Node " << this_node << " has no ports! \n";
return false;
}
// Link ports
const auto& p = node_it->second.inputs.front();
link = this->global_context->link_ports(in_port.port, p.id);
pw_loop_iterate(this->global_context->lp, 1);
if (!link)
{
self.template error<invalid_parameter_error>(
self.configuration,
"PipeWire: could not connect to port: " + in_port.port_name + " -> " + p.port_name);
return false;
}
return true;
}
bool link_ports(auto& self, const output_port& out_port)
{
// Wait for the pipewire server to send us back our node's info
for (int i = 0; i < 1000; i++)
this->filter->synchronize_node();
auto this_node = this->filter->filter_node_id();
auto& midi = this->global_context->current_graph.software_midi;
auto node_it = midi.find(this_node);
if (node_it == midi.end())
{
std::cerr << "Node " << this_node << " not found! \n";
return false;
}
// Wait for the pipewire server to send us back our node's ports
this->filter->synchronize_ports(node_it->second);
if (node_it->second.outputs.empty())
{
std::cerr << "Node " << this_node << " has no ports! \n";
return false;
}
// Link ports
const auto& p = node_it->second.outputs.front();
link = this->global_context->link_ports(p.id, out_port.port);
pw_loop_iterate(this->global_context->lp, 1);
if (!link)
{
self.template error<invalid_parameter_error>(
self.configuration,
"PipeWire: could not connect to port: " + p.port_name + " -> " + out_port.port_name);
return false;
}
return true;
}
template <spa_direction Direction>
static auto to_port_info(const pipewire_context::port_info& port)
-> std::conditional_t<Direction == SPA_DIRECTION_OUTPUT, input_port, output_port>
{
std::string device_name, port_name;
auto name_colon = port.port_alias.find(':');
if (name_colon != std::string::npos)
{
device_name = port.port_alias.substr(0, name_colon);
port_name = port.port_alias.substr(name_colon + 1);
}
else
{
port_name = port.port_alias;
}
return {{
.client = 0,
.port = port.id,
.manufacturer = "",
.device_name = device_name,
.port_name = port.port_name,
.display_name = port_name,
}};
}
// Note: keep in mind that an "input" port for us (e.g. a keyboard that goes to the computer)
// is an "output" port from the point of view of pipewire as data will come out of it
template <spa_direction Direction>
static auto get_ports(const pipewire_context& ctx) noexcept -> std::vector<
std::conditional_t<Direction == SPA_DIRECTION_OUTPUT, input_port, output_port>>
{
std::vector<std::conditional_t<Direction == SPA_DIRECTION_OUTPUT, input_port, output_port>>
ret;
{
std::lock_guard _{ctx.current_graph.mtx};
for (auto& node : ctx.current_graph.physical_midi)
{
for (auto& p :
(Direction == SPA_DIRECTION_INPUT ? node.second.inputs : node.second.outputs))
{
ret.push_back(to_port_info<Direction>(p));
}
}
for (auto& node : ctx.current_graph.software_midi)
{
for (auto& p :
(Direction == SPA_DIRECTION_INPUT ? node.second.inputs : node.second.outputs))
{
ret.push_back(to_port_info<Direction>(p));
}
}
}
return ret;
}
};
}