7#include <pipewire/pipewire.h>
15 bool parse_alsa_seq_path(
const std::string& path,
int& client,
int& port)
17 auto c = path.rfind(
"client_");
18 auto p = path.rfind(
"capture_");
19 if (c == std::string::npos || p == std::string::npos)
21 client = std::stoi(path.substr(c + 7));
22 port = std::stoi(path.substr(p + 8));
31PipewireMidiBackend::PipewireMidiBackend()
32 : PipewireMidiBackend(Config {})
36PipewireMidiBackend::PipewireMidiBackend(Config config)
37 : m_config(
std::move(config))
41PipewireMidiBackend::~PipewireMidiBackend()
43 if (m_initialized.load()) {
52bool PipewireMidiBackend::initialize()
54 if (m_initialized.load()) {
55 MF_WARN(C, X,
"PipewireMidiBackend already initialized");
59 pw_init(
nullptr,
nullptr);
61 m_thread_loop = pw_thread_loop_new(
"mayaflux-midi",
nullptr);
63 MF_ERROR(C, X,
"pw_thread_loop_new failed");
67 m_context = pw_context_new(pw_thread_loop_get_loop(m_thread_loop),
nullptr, 0);
69 pw_thread_loop_destroy(m_thread_loop);
70 m_thread_loop =
nullptr;
71 MF_ERROR(C, X,
"pw_context_new failed");
75 pw_thread_loop_lock(m_thread_loop);
77 m_core = pw_context_connect(m_context,
nullptr, 0);
79 pw_thread_loop_unlock(m_thread_loop);
80 pw_context_destroy(m_context);
81 pw_thread_loop_destroy(m_thread_loop);
83 m_thread_loop =
nullptr;
84 MF_ERROR(C, X,
"pw_context_connect failed - is PipeWire running?");
88 m_registry = pw_core_get_registry(m_core, PW_VERSION_REGISTRY, 0);
90 pw_thread_loop_unlock(m_thread_loop);
91 pw_core_disconnect(m_core);
92 pw_context_destroy(m_context);
93 pw_thread_loop_destroy(m_thread_loop);
96 m_thread_loop =
nullptr;
97 MF_ERROR(C, X,
"pw_core_get_registry failed");
101 static const pw_registry_events k_registry_events {
102 .version = PW_VERSION_REGISTRY_EVENTS,
103 .global = on_registry_global,
104 .global_remove = on_registry_global_remove,
107 spa_zero(m_registry_listener);
108 pw_registry_add_listener(m_registry, &m_registry_listener, &k_registry_events,
this);
111 pw_thread_loop* loop;
113 } sync { .loop = m_thread_loop, .pending = 0 };
115 static const pw_core_events k_sync_events {
116 .version = PW_VERSION_CORE_EVENTS,
117 .done = [](
void* ud, uint32_t id, int) {
118 if (
id != PW_ID_CORE)
120 auto* s =
static_cast<SyncState*
>(ud);
121 pw_thread_loop_signal(s->loop,
false);
125 spa_hook sync_listener;
126 spa_zero(sync_listener);
127 pw_core_add_listener(m_core, &sync_listener, &k_sync_events, &sync);
128 sync.pending = pw_core_sync(m_core, PW_ID_CORE, 0);
130 pw_thread_loop_unlock(m_thread_loop);
132 if (pw_thread_loop_start(m_thread_loop) < 0) {
133 pw_thread_loop_destroy(m_thread_loop);
134 m_thread_loop =
nullptr;
135 MF_ERROR(C, X,
"pw_thread_loop_start failed");
139 pw_thread_loop_lock(m_thread_loop);
140 pw_thread_loop_wait(m_thread_loop);
141 spa_hook_remove(&sync_listener);
142 pw_thread_loop_unlock(m_thread_loop);
144 create_virtual_port_if_enabled();
146 m_initialized.store(
true);
148 MF_INFO(C, X,
"PipewireMidiBackend initialized with {} port(s) (PipeWire {})",
149 m_enumerated_devices.size(), pw_get_library_version());
151 for (
const auto& [
id, info] : m_enumerated_devices) {
152 MF_INFO(C, X,
" enumerated: id={} name='{}' pw_id={} path='{}'",
153 id, info.name, info.pw_global_id, info.object_path);
159void PipewireMidiBackend::start()
161 if (!m_initialized.load()) {
162 MF_ERROR(C, X,
"Cannot start PipewireMidiBackend: not initialized");
166 if (m_running.load()) {
167 MF_WARN(C, X,
"PipewireMidiBackend already running");
171 m_running.store(
true);
173 MF_INFO(C, X,
"PipewireMidiBackend started with {} open port(s)",
174 get_open_devices().size());
177void PipewireMidiBackend::stop()
179 if (!m_running.load())
182 m_running.store(
false);
184 std::vector<uint32_t> to_close;
186 std::lock_guard lock(m_devices_mutex);
187 for (
const auto& [
id, state] : m_open_devices)
188 to_close.push_back(id);
191 for (uint32_t
id : to_close)
194 MF_INFO(C, X,
"PipewireMidiBackend stopped");
197void PipewireMidiBackend::shutdown()
199 if (!m_initialized.load())
202 if (m_running.load())
206 pw_thread_loop_lock(m_thread_loop);
207 spa_hook_remove(&m_registry_listener);
208 pw_proxy_destroy(
reinterpret_cast<pw_proxy*
>(m_registry));
209 m_registry =
nullptr;
210 pw_thread_loop_unlock(m_thread_loop);
214 pw_thread_loop_stop(m_thread_loop);
217 std::lock_guard lock(m_devices_mutex);
218 m_open_devices.clear();
219 m_enumerated_devices.clear();
223 pw_core_disconnect(m_core);
228 pw_context_destroy(m_context);
233 pw_thread_loop_destroy(m_thread_loop);
234 m_thread_loop =
nullptr;
238 m_initialized.store(
false);
239 MF_INFO(C, X,
"PipewireMidiBackend shutdown complete");
246std::vector<InputDeviceInfo> PipewireMidiBackend::get_devices()
const
248 std::lock_guard lock(m_devices_mutex);
249 std::vector<InputDeviceInfo> result;
250 result.reserve(m_enumerated_devices.size());
251 for (
const auto& [
id, info] : m_enumerated_devices) {
252 result.push_back(info);
257size_t PipewireMidiBackend::refresh_devices()
259 std::lock_guard lock(m_devices_mutex);
260 return m_enumerated_devices.size();
263bool PipewireMidiBackend::open_device(uint32_t device_id)
268 std::lock_guard lock(m_devices_mutex);
269 if (m_open_devices.find(device_id) != m_open_devices.end()) {
270 MF_DEBUG(C, X,
"MIDI port {} already open", device_id);
273 auto it = m_enumerated_devices.find(device_id);
274 if (it == m_enumerated_devices.end()) {
275 MF_ERROR(C, X,
"MIDI port {} not found", device_id);
281 if (info.alsa_client < 0) {
282 MF_WARN(C, X,
"MIDI port '{}' is not an ALSA seq port, skipping", info.name);
286 auto state = std::make_shared<MIDIPortState>();
288 state->device_id = device_id;
290 std::lock_guard cb_lock(m_callback_mutex);
291 state->input_callback = m_input_callback;
294 if (snd_seq_open(&state->seq_handle,
"default", SND_SEQ_OPEN_INPUT, SND_SEQ_NONBLOCK) < 0) {
295 MF_ERROR(C, X,
"snd_seq_open failed for port '{}'", info.name);
299 snd_seq_set_client_name(state->seq_handle,
"MayaFlux");
301 state->seq_port = snd_seq_create_simple_port(
302 state->seq_handle,
"MayaFlux MIDI In",
303 SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
304 SND_SEQ_PORT_TYPE_APPLICATION);
306 if (state->seq_port < 0) {
307 snd_seq_close(state->seq_handle);
308 state->seq_handle =
nullptr;
309 MF_ERROR(C, X,
"snd_seq_create_simple_port failed for '{}'", info.name);
313 snd_seq_addr_t sender {
314 .client =
static_cast<unsigned char>(info.alsa_client),
315 .port =
static_cast<unsigned char>(info.alsa_port)
317 snd_seq_addr_t dest {
318 .client =
static_cast<unsigned char>(snd_seq_client_id(state->seq_handle)),
319 .port =
static_cast<unsigned char>(state->seq_port)
322 snd_seq_port_subscribe_t* sub {
nullptr };
323 snd_seq_port_subscribe_alloca(&sub);
324 snd_seq_port_subscribe_set_sender(sub, &sender);
325 snd_seq_port_subscribe_set_dest(sub, &dest);
327 if (snd_seq_subscribe_port(state->seq_handle, sub) < 0) {
328 snd_seq_close(state->seq_handle);
329 state->seq_handle =
nullptr;
330 MF_ERROR(C, X,
"snd_seq_subscribe_port failed for '{}'", info.name);
334 state->active.store(
true);
336 state->poll_thread = std::thread([state]() {
337 int npfds = snd_seq_poll_descriptors_count(state->seq_handle, POLLIN);
338 std::vector<struct pollfd> pfds(npfds);
339 snd_seq_poll_descriptors(state->seq_handle, pfds.data(), npfds, POLLIN);
341 while (state->active.load()) {
342 int ret = poll(pfds.data(), npfds, 100);
346 snd_seq_event_t* ev =
nullptr;
347 while (state->active.load()
348 && snd_seq_event_input(state->seq_handle, &ev) >= 0 && ev) {
349 uint8_t status = 0, d1 = 0, d2 = 0;
351 case SND_SEQ_EVENT_NOTEON:
352 status = 0x90 | (ev->data.note.channel & 0x0F);
353 d1 = ev->data.note.note;
354 d2 = ev->data.note.velocity;
356 case SND_SEQ_EVENT_NOTEOFF:
357 status = 0x80 | (ev->data.note.channel & 0x0F);
358 d1 = ev->data.note.note;
359 d2 = ev->data.note.velocity;
361 case SND_SEQ_EVENT_CONTROLLER:
362 status = 0xB0 | (ev->data.control.channel & 0x0F);
363 d1 =
static_cast<uint8_t
>(ev->data.control.param);
364 d2 =
static_cast<uint8_t
>(ev->data.control.value);
366 case SND_SEQ_EVENT_PITCHBEND:
367 status = 0xE0 | (ev->data.control.channel & 0x0F);
368 d1 =
static_cast<uint8_t
>((ev->data.control.value + 8192) & 0x7F);
369 d2 =
static_cast<uint8_t
>(((ev->data.control.value + 8192) >> 7) & 0x7F);
371 case SND_SEQ_EVENT_PGMCHANGE:
372 status = 0xC0 | (ev->data.control.channel & 0x0F);
373 d1 =
static_cast<uint8_t
>(ev->data.control.value);
375 case SND_SEQ_EVENT_CHANPRESS:
376 status = 0xD0 | (ev->data.control.channel & 0x0F);
377 d1 =
static_cast<uint8_t
>(ev->data.control.value);
383 if (state->input_callback) {
384 state->input_callback(
385 InputValue::make_midi(status, d1, d2, state->device_id));
392 std::lock_guard lock(m_devices_mutex);
393 m_open_devices.insert_or_assign(device_id, state);
396 MF_INFO(C, X,
"Opened MIDI port {}: '{}' (ALSA {}:{})",
397 device_id, info.name, info.alsa_client, info.alsa_port);
401void PipewireMidiBackend::register_midi_port(
402 uint32_t pw_id,
const std::string& name,
const std::string& object_path)
404 if (!port_matches_filter(name))
407 std::lock_guard lock(m_devices_mutex);
408 uint32_t dev_id = find_or_assign_device_id(pw_id);
409 bool is_new = (m_enumerated_devices.find(dev_id) == m_enumerated_devices.end());
411 MIDIPortInfo info {};
414 info.pw_global_id = pw_id;
415 info.object_path = object_path;
416 parse_alsa_seq_path(object_path, info.alsa_client, info.alsa_port);
417 info.backend_type = InputType::MIDI;
418 info.is_connected =
true;
419 info.is_input =
true;
420 info.is_output =
false;
421 info.port_number =
static_cast<uint8_t
>(dev_id & 0xFF);
423 m_enumerated_devices[dev_id] = info;
426 MF_INFO(C, X,
"MIDI port found: '{}' (pw_id={} alsa={}:{} path='{}')",
427 name, pw_id, info.alsa_client, info.alsa_port, object_path);
428 notify_device_change(info,
true);
432void PipewireMidiBackend::close_device(uint32_t device_id)
434 std::shared_ptr<MIDIPortState> state;
436 std::lock_guard lock(m_devices_mutex);
437 auto it = m_open_devices.find(device_id);
438 if (it == m_open_devices.end())
440 state = std::move(it->second);
441 m_open_devices.erase(it);
444 state->active.store(
false);
446 if (state->poll_thread.joinable())
447 state->poll_thread.join();
449 if (state->seq_handle) {
450 snd_seq_close(state->seq_handle);
451 state->seq_handle =
nullptr;
454 MF_INFO(C, X,
"Closed MIDI port {}: '{}'", device_id, state->info.name);
457bool PipewireMidiBackend::is_device_open(uint32_t device_id)
const
459 std::lock_guard lock(m_devices_mutex);
460 return m_open_devices.find(device_id) != m_open_devices.end();
463std::vector<uint32_t> PipewireMidiBackend::get_open_devices()
const
465 std::lock_guard lock(m_devices_mutex);
466 std::vector<uint32_t> result;
467 result.reserve(m_open_devices.size());
468 for (
const auto& [
id, state] : m_open_devices) {
469 result.push_back(
id);
474void PipewireMidiBackend::set_input_callback(InputCallback callback)
476 std::lock_guard lock(m_callback_mutex);
477 m_input_callback = std::move(callback);
480void PipewireMidiBackend::set_device_callback(DeviceCallback callback)
482 std::lock_guard lock(m_callback_mutex);
483 m_device_callback = std::move(callback);
486std::string PipewireMidiBackend::get_version()
const
488 return pw_get_library_version();
495bool PipewireMidiBackend::port_matches_filter(
const std::string& port_name)
const
497 if (m_config.input_port_filters.empty()) {
500 return std::ranges::any_of(m_config.input_port_filters,
501 [&port_name](
const std::string& filter) {
502 return port_name.find(filter) != std::string::npos;
506uint32_t PipewireMidiBackend::find_or_assign_device_id(uint32_t pw_global_id)
508 for (
const auto& [
id, info] : m_enumerated_devices) {
509 if (info.pw_global_id == pw_global_id) {
513 return m_next_device_id++;
516void PipewireMidiBackend::create_virtual_port_if_enabled()
518 if (!m_config.enable_virtual_port) {
522 pw_thread_loop_lock(m_thread_loop);
524 pw_properties* props = pw_properties_new(
525 PW_KEY_MEDIA_TYPE,
"Midi",
526 PW_KEY_MEDIA_CATEGORY,
"Playback",
527 PW_KEY_MEDIA_CLASS,
"Stream/Output/Midi",
528 PW_KEY_APP_NAME,
"MayaFlux",
529 PW_KEY_NODE_NAME, m_config.virtual_port_name.c_str(),
532 pw_stream* virt = pw_stream_new_simple(
533 pw_thread_loop_get_loop(m_thread_loop),
534 m_config.virtual_port_name.c_str(),
540 pw_stream_connect(virt, PW_DIRECTION_OUTPUT, PW_ID_ANY,
541 PW_STREAM_FLAG_AUTOCONNECT,
nullptr, 0);
542 MF_INFO(C, X,
"Created virtual MIDI port: {}", m_config.virtual_port_name);
544 MF_ERROR(C, X,
"Failed to create virtual MIDI port: {}",
545 m_config.virtual_port_name);
548 pw_thread_loop_unlock(m_thread_loop);
551void PipewireMidiBackend::notify_device_change(
const InputDeviceInfo& info,
bool connected)
553 std::lock_guard lock(m_callback_mutex);
554 if (m_device_callback) {
555 m_device_callback(info, connected);
563void PipewireMidiBackend::on_registry_global(
564 void* userdata, uint32_t
id, uint32_t,
565 const char* type, uint32_t,
const struct spa_dict* props)
567 auto* self =
static_cast<PipewireMidiBackend*
>(userdata);
571 if (std::strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
572 const char* media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
573 if (!media_class || std::strcmp(media_class,
"Midi/Source") != 0)
576 const char* name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
578 name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
582 self->register_midi_port(
id, name,
"");
586 if (std::strcmp(type, PW_TYPE_INTERFACE_Port) == 0) {
587 const char* fmt = spa_dict_lookup(props,
"format.dsp");
588 const char* dir = spa_dict_lookup(props,
"port.direction");
589 if (!fmt || std::strcmp(fmt,
"8 bit raw midi") != 0)
591 if (!dir || std::strcmp(dir,
"out") != 0)
594 const char* alias = spa_dict_lookup(props,
"port.alias");
595 const char* name = spa_dict_lookup(props,
"port.name");
596 const char* label = alias ? alias : name;
600 std::string display = label;
601 if (
auto pos = display.find(
':'); pos != std::string::npos)
602 display = display.substr(0, pos);
604 const char* obj_path = spa_dict_lookup(props,
"object.path");
605 std::string object_path = obj_path ? obj_path :
"";
607 self->register_midi_port(
id, display, object_path);
611void PipewireMidiBackend::on_registry_global_remove(
void* userdata, uint32_t
id)
613 auto* self =
static_cast<PipewireMidiBackend*
>(userdata);
615 std::lock_guard lock(self->m_devices_mutex);
617 for (
auto it = self->m_enumerated_devices.begin();
618 it != self->m_enumerated_devices.end(); ++it) {
619 if (it->second.pw_global_id ==
id) {
620 MF_INFO(C, X,
"MIDI port removed: {} (pw_id={})", it->second.name,
id);
621 self->notify_device_change(it->second,
false);
623 auto open_it = self->m_open_devices.find(it->first);
624 if (open_it != self->m_open_devices.end()) {
625 open_it->second->active.store(
false);
626 self->m_open_devices.erase(open_it);
629 self->m_enumerated_devices.erase(it);
635void PipewireMidiBackend::on_core_done(
void* , uint32_t ,
int )
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
@ InputBackend
Input device backend (HID, MIDI, OSC)
@ Core
Core engine, backend, subsystems.
void stop()
Stop all Portal::Graphics operations.