MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
PipewireMidiBackend.cpp
Go to the documentation of this file.
2
4
5#ifdef PIPEWIRE_BACKEND
6
7#include <pipewire/pipewire.h>
8
9namespace MayaFlux::Core {
10
11namespace {
12 constexpr auto C = Journal::Component::Core;
13 constexpr auto X = Journal::Context::InputBackend;
14
15 bool parse_alsa_seq_path(const std::string& path, int& client, int& port)
16 {
17 auto c = path.rfind("client_");
18 auto p = path.rfind("capture_");
19 if (c == std::string::npos || p == std::string::npos)
20 return false;
21 client = std::stoi(path.substr(c + 7));
22 port = std::stoi(path.substr(p + 8));
23 return true;
24 }
25}
26
27// ===================================================================================
28// Construction / Destruction
29// ===================================================================================
30
31PipewireMidiBackend::PipewireMidiBackend()
32 : PipewireMidiBackend(Config {})
33{
34}
35
36PipewireMidiBackend::PipewireMidiBackend(Config config)
37 : m_config(std::move(config))
38{
39}
40
41PipewireMidiBackend::~PipewireMidiBackend()
42{
43 if (m_initialized.load()) {
44 shutdown();
45 }
46}
47
48// ===================================================================================
49// IInputBackend: Lifecycle
50// ===================================================================================
51
52bool PipewireMidiBackend::initialize()
53{
54 if (m_initialized.load()) {
55 MF_WARN(C, X, "PipewireMidiBackend already initialized");
56 return true;
57 }
58
59 pw_init(nullptr, nullptr);
60
61 m_thread_loop = pw_thread_loop_new("mayaflux-midi", nullptr);
62 if (!m_thread_loop) {
63 MF_ERROR(C, X, "pw_thread_loop_new failed");
64 return false;
65 }
66
67 m_context = pw_context_new(pw_thread_loop_get_loop(m_thread_loop), nullptr, 0);
68 if (!m_context) {
69 pw_thread_loop_destroy(m_thread_loop);
70 m_thread_loop = nullptr;
71 MF_ERROR(C, X, "pw_context_new failed");
72 return false;
73 }
74
75 pw_thread_loop_lock(m_thread_loop);
76
77 m_core = pw_context_connect(m_context, nullptr, 0);
78 if (!m_core) {
79 pw_thread_loop_unlock(m_thread_loop);
80 pw_context_destroy(m_context);
81 pw_thread_loop_destroy(m_thread_loop);
82 m_context = nullptr;
83 m_thread_loop = nullptr;
84 MF_ERROR(C, X, "pw_context_connect failed - is PipeWire running?");
85 return false;
86 }
87
88 m_registry = pw_core_get_registry(m_core, PW_VERSION_REGISTRY, 0);
89 if (!m_registry) {
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);
94 m_core = nullptr;
95 m_context = nullptr;
96 m_thread_loop = nullptr;
97 MF_ERROR(C, X, "pw_core_get_registry failed");
98 return false;
99 }
100
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,
105 };
106
107 spa_zero(m_registry_listener);
108 pw_registry_add_listener(m_registry, &m_registry_listener, &k_registry_events, this);
109
110 struct SyncState {
111 pw_thread_loop* loop;
112 int pending;
113 } sync { .loop = m_thread_loop, .pending = 0 };
114
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)
119 return;
120 auto* s = static_cast<SyncState*>(ud);
121 pw_thread_loop_signal(s->loop, false);
122 },
123 };
124
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);
129
130 pw_thread_loop_unlock(m_thread_loop);
131
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");
136 return false;
137 }
138
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);
143
144 create_virtual_port_if_enabled();
145
146 m_initialized.store(true);
147
148 MF_INFO(C, X, "PipewireMidiBackend initialized with {} port(s) (PipeWire {})",
149 m_enumerated_devices.size(), pw_get_library_version());
150
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);
154 }
155
156 return true;
157}
158
159void PipewireMidiBackend::start()
160{
161 if (!m_initialized.load()) {
162 MF_ERROR(C, X, "Cannot start PipewireMidiBackend: not initialized");
163 return;
164 }
165
166 if (m_running.load()) {
167 MF_WARN(C, X, "PipewireMidiBackend already running");
168 return;
169 }
170
171 m_running.store(true);
172
173 MF_INFO(C, X, "PipewireMidiBackend started with {} open port(s)",
174 get_open_devices().size());
175}
176
177void PipewireMidiBackend::stop()
178{
179 if (!m_running.load())
180 return;
181
182 m_running.store(false);
183
184 std::vector<uint32_t> to_close;
185 {
186 std::lock_guard lock(m_devices_mutex);
187 for (const auto& [id, state] : m_open_devices) {
188 if (id != m_virtual_device_id)
189 to_close.push_back(id);
190 }
191 }
192
193 for (uint32_t id : to_close)
194 close_device(id);
195
196 MF_INFO(C, X, "PipewireMidiBackend stopped");
197}
198
199void PipewireMidiBackend::shutdown()
200{
201 if (!m_initialized.load())
202 return;
203
204 if (m_running.load())
205 stop();
206
207 if (m_registry) {
208 pw_thread_loop_lock(m_thread_loop);
209 spa_hook_remove(&m_registry_listener);
210 pw_proxy_destroy(reinterpret_cast<pw_proxy*>(m_registry));
211 m_registry = nullptr;
212 pw_thread_loop_unlock(m_thread_loop);
213 }
214
215 if (m_thread_loop)
216 pw_thread_loop_stop(m_thread_loop);
217
218 if (m_virtual_device_id != 0) {
219 close_device(m_virtual_device_id);
220 m_virtual_device_id = 0;
221 }
222
223 {
224 std::lock_guard lock(m_devices_mutex);
225 m_open_devices.clear();
226 m_enumerated_devices.clear();
227 }
228
229 if (m_core) {
230 pw_core_disconnect(m_core);
231 m_core = nullptr;
232 }
233
234 if (m_context) {
235 pw_context_destroy(m_context);
236 m_context = nullptr;
237 }
238
239 if (m_thread_loop) {
240 pw_thread_loop_destroy(m_thread_loop);
241 m_thread_loop = nullptr;
242 }
243
244 pw_deinit();
245 m_initialized.store(false);
246 MF_INFO(C, X, "PipewireMidiBackend shutdown complete");
247}
248
249// ===================================================================================
250// IInputBackend: Device Management
251// ===================================================================================
252
253std::vector<InputDeviceInfo> PipewireMidiBackend::get_devices() const
254{
255 std::lock_guard lock(m_devices_mutex);
256 std::vector<InputDeviceInfo> result;
257 result.reserve(m_enumerated_devices.size());
258 for (const auto& [id, info] : m_enumerated_devices) {
259 result.push_back(info);
260 }
261 return result;
262}
263
264size_t PipewireMidiBackend::refresh_devices()
265{
266 std::lock_guard lock(m_devices_mutex);
267 return m_enumerated_devices.size();
268}
269
270void PipewireMidiBackend::run_seq_poll(const std::shared_ptr<MIDIPortState>& state)
271{
272 int npfds = snd_seq_poll_descriptors_count(state->seq_handle, POLLIN);
273 std::vector<struct pollfd> pfds(npfds);
274 snd_seq_poll_descriptors(state->seq_handle, pfds.data(), npfds, POLLIN);
275
276 while (state->active.load()) {
277 int ret = poll(pfds.data(), npfds, 100);
278 if (ret <= 0)
279 continue;
280
281 snd_seq_event_t* ev = nullptr;
282 while (state->active.load()
283 && snd_seq_event_input(state->seq_handle, &ev) >= 0 && ev) {
284 uint8_t status = 0, d1 = 0, d2 = 0;
285 switch (ev->type) {
286 case SND_SEQ_EVENT_NOTEON:
287 status = 0x90 | (ev->data.note.channel & 0x0F);
288 d1 = ev->data.note.note;
289 d2 = ev->data.note.velocity;
290 break;
291 case SND_SEQ_EVENT_NOTEOFF:
292 status = 0x80 | (ev->data.note.channel & 0x0F);
293 d1 = ev->data.note.note;
294 d2 = ev->data.note.velocity;
295 break;
296 case SND_SEQ_EVENT_CONTROLLER:
297 status = 0xB0 | (ev->data.control.channel & 0x0F);
298 d1 = static_cast<uint8_t>(ev->data.control.param);
299 d2 = static_cast<uint8_t>(ev->data.control.value);
300 break;
301 case SND_SEQ_EVENT_PITCHBEND:
302 status = 0xE0 | (ev->data.control.channel & 0x0F);
303 d1 = static_cast<uint8_t>((ev->data.control.value + 8192) & 0x7F);
304 d2 = static_cast<uint8_t>(((ev->data.control.value + 8192) >> 7) & 0x7F);
305 break;
306 case SND_SEQ_EVENT_PGMCHANGE:
307 status = 0xC0 | (ev->data.control.channel & 0x0F);
308 d1 = static_cast<uint8_t>(ev->data.control.value);
309 break;
310 case SND_SEQ_EVENT_CHANPRESS:
311 status = 0xD0 | (ev->data.control.channel & 0x0F);
312 d1 = static_cast<uint8_t>(ev->data.control.value);
313 break;
314 default:
315 continue;
316 }
317
318 if (state->input_callback) {
319 state->input_callback(
320 InputValue::make_midi(status, d1, d2, state->device_id));
321 }
322 }
323 }
324}
325
326bool PipewireMidiBackend::open_device(uint32_t device_id)
327{
328 MIDIPortInfo info;
329
330 {
331 std::lock_guard lock(m_devices_mutex);
332 if (m_open_devices.find(device_id) != m_open_devices.end()) {
333 MF_DEBUG(C, X, "MIDI port {} already open", device_id);
334 return true;
335 }
336 auto it = m_enumerated_devices.find(device_id);
337 if (it == m_enumerated_devices.end()) {
338 MF_ERROR(C, X, "MIDI port {} not found", device_id);
339 return false;
340 }
341 info = it->second;
342 }
343
344 if (info.alsa_client < 0) {
345 MF_WARN(C, X, "MIDI port '{}' is not an ALSA seq port, skipping", info.name);
346 return false;
347 }
348
349 auto state = std::make_shared<MIDIPortState>();
350 state->info = info;
351 state->device_id = device_id;
352 {
353 std::lock_guard cb_lock(m_callback_mutex);
354 state->input_callback = m_input_callback;
355 }
356
357 if (snd_seq_open(&state->seq_handle, "default", SND_SEQ_OPEN_INPUT, SND_SEQ_NONBLOCK) < 0) {
358 MF_ERROR(C, X, "snd_seq_open failed for port '{}'", info.name);
359 return false;
360 }
361
362 snd_seq_set_client_name(state->seq_handle, "MayaFlux");
363
364 state->seq_port = snd_seq_create_simple_port(
365 state->seq_handle, "MayaFlux MIDI In",
366 SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
367 SND_SEQ_PORT_TYPE_APPLICATION);
368
369 if (state->seq_port < 0) {
370 snd_seq_close(state->seq_handle);
371 state->seq_handle = nullptr;
372 MF_ERROR(C, X, "snd_seq_create_simple_port failed for '{}'", info.name);
373 return false;
374 }
375
376 snd_seq_addr_t sender {
377 .client = static_cast<unsigned char>(info.alsa_client),
378 .port = static_cast<unsigned char>(info.alsa_port)
379 };
380 snd_seq_addr_t dest {
381 .client = static_cast<unsigned char>(snd_seq_client_id(state->seq_handle)),
382 .port = static_cast<unsigned char>(state->seq_port)
383 };
384
385 snd_seq_port_subscribe_t* sub { nullptr };
386 snd_seq_port_subscribe_alloca(&sub);
387 snd_seq_port_subscribe_set_sender(sub, &sender);
388 snd_seq_port_subscribe_set_dest(sub, &dest);
389
390 if (snd_seq_subscribe_port(state->seq_handle, sub) < 0) {
391 snd_seq_close(state->seq_handle);
392 state->seq_handle = nullptr;
393 MF_ERROR(C, X, "snd_seq_subscribe_port failed for '{}'", info.name);
394 return false;
395 }
396
397 state->active.store(true);
398
399 state->poll_thread = std::thread([state]() { run_seq_poll(state); });
400
401 {
402 std::lock_guard lock(m_devices_mutex);
403 m_open_devices.insert_or_assign(device_id, state);
404 }
405
406 MF_INFO(C, X, "Opened MIDI port {}: '{}' (ALSA {}:{})",
407 device_id, info.name, info.alsa_client, info.alsa_port);
408 return true;
409}
410
411void PipewireMidiBackend::register_midi_port(
412 uint32_t pw_id, const std::string& name, const std::string& object_path)
413{
414 if (!port_matches_filter(name))
415 return;
416
417 std::lock_guard lock(m_devices_mutex);
418 uint32_t dev_id = find_or_assign_device_id(pw_id);
419 bool is_new = (m_enumerated_devices.find(dev_id) == m_enumerated_devices.end());
420
421 MIDIPortInfo info {};
422 info.id = dev_id;
423 info.name = name;
424 info.pw_global_id = pw_id;
425 info.object_path = object_path;
426 parse_alsa_seq_path(object_path, info.alsa_client, info.alsa_port);
427 info.backend_type = InputType::MIDI;
428 info.is_connected = true;
429 info.is_input = true;
430 info.is_output = false;
431 info.port_number = static_cast<uint8_t>(dev_id & 0xFF);
432
433 m_enumerated_devices[dev_id] = info;
434
435 if (is_new) {
436 MF_INFO(C, X, "MIDI port found: '{}' (pw_id={} alsa={}:{} path='{}')",
437 name, pw_id, info.alsa_client, info.alsa_port, object_path);
438 notify_device_change(info, true);
439 }
440}
441
442void PipewireMidiBackend::close_device(uint32_t device_id)
443{
444 std::shared_ptr<MIDIPortState> state;
445 {
446 std::lock_guard lock(m_devices_mutex);
447 auto it = m_open_devices.find(device_id);
448 if (it == m_open_devices.end())
449 return;
450 state = std::move(it->second);
451 m_open_devices.erase(it);
452 }
453
454 state->active.store(false);
455
456 if (state->poll_thread.joinable())
457 state->poll_thread.join();
458
459 if (state->seq_handle) {
460 snd_seq_close(state->seq_handle);
461 state->seq_handle = nullptr;
462 }
463
464 MF_INFO(C, X, "Closed MIDI port {}: '{}'", device_id, state->info.name);
465}
466
467bool PipewireMidiBackend::is_device_open(uint32_t device_id) const
468{
469 std::lock_guard lock(m_devices_mutex);
470 return m_open_devices.find(device_id) != m_open_devices.end();
471}
472
473std::vector<uint32_t> PipewireMidiBackend::get_open_devices() const
474{
475 std::lock_guard lock(m_devices_mutex);
476 std::vector<uint32_t> result;
477 result.reserve(m_open_devices.size());
478 for (const auto& [id, state] : m_open_devices) {
479 result.push_back(id);
480 }
481 return result;
482}
483
484void PipewireMidiBackend::set_input_callback(InputCallback callback)
485{
486 std::lock_guard lock(m_callback_mutex);
487 m_input_callback = std::move(callback);
488}
489
490void PipewireMidiBackend::set_device_callback(DeviceCallback callback)
491{
492 std::lock_guard lock(m_callback_mutex);
493 m_device_callback = std::move(callback);
494}
495
496std::string PipewireMidiBackend::get_version() const
497{
498 return pw_get_library_version();
499}
500
501// ===================================================================================
502// Private helpers
503// ===================================================================================
504
505bool PipewireMidiBackend::port_matches_filter(const std::string& port_name) const
506{
507 if (m_config.input_port_filters.empty()) {
508 return true;
509 }
510 return std::ranges::any_of(m_config.input_port_filters,
511 [&port_name](const std::string& filter) {
512 return port_name.find(filter) != std::string::npos;
513 });
514}
515
516uint32_t PipewireMidiBackend::find_or_assign_device_id(uint32_t pw_global_id)
517{
518 for (const auto& [id, info] : m_enumerated_devices) {
519 if (info.pw_global_id == pw_global_id) {
520 return id;
521 }
522 }
523 return m_next_device_id++;
524}
525
526void PipewireMidiBackend::create_virtual_port_if_enabled()
527{
528 if (!m_config.enable_virtual_port) {
529 return;
530 }
531
532 auto state = std::make_shared<MIDIPortState>();
533 {
534 std::lock_guard cb_lock(m_callback_mutex);
535 state->input_callback = m_input_callback;
536 }
537
538 if (snd_seq_open(&state->seq_handle, "default", SND_SEQ_OPEN_INPUT, SND_SEQ_NONBLOCK) < 0) {
539 MF_ERROR(C, X, "snd_seq_open failed for virtual MIDI port '{}'", m_config.virtual_port_name);
540 return;
541 }
542
543 snd_seq_set_client_name(state->seq_handle, m_config.virtual_port_name.c_str());
544
545 state->seq_port = snd_seq_create_simple_port(
546 state->seq_handle, m_config.virtual_port_name.c_str(),
547 SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE,
548 SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION);
549
550 if (state->seq_port < 0) {
551 snd_seq_close(state->seq_handle);
552 state->seq_handle = nullptr;
553 MF_ERROR(C, X, "Failed to create virtual MIDI port: {}", m_config.virtual_port_name);
554 return;
555 }
556
557 {
558 std::lock_guard lock(m_devices_mutex);
559 const uint32_t dev_id = m_next_device_id++;
560
561 state->device_id = dev_id;
562 state->info.id = dev_id;
563 state->info.name = m_config.virtual_port_name;
564 state->info.pw_global_id = UINT32_MAX;
565 state->info.alsa_client = snd_seq_client_id(state->seq_handle);
566 state->info.alsa_port = state->seq_port;
567 state->info.backend_type = InputType::MIDI;
568 state->info.is_connected = true;
569 state->info.is_input = true;
570 state->info.is_output = false;
571 state->info.port_number = static_cast<uint8_t>(dev_id & 0xFF);
572
573 state->active.store(true);
574 state->poll_thread = std::thread([state]() { run_seq_poll(state); });
575
576 m_enumerated_devices[dev_id] = state->info;
577 m_open_devices.insert_or_assign(dev_id, state);
578 m_virtual_device_id = dev_id;
579 }
580
581 MF_INFO(C, X, "Created virtual MIDI port: {}", m_config.virtual_port_name);
582}
583
584void PipewireMidiBackend::notify_device_change(const InputDeviceInfo& info, bool connected)
585{
586 std::lock_guard lock(m_callback_mutex);
587 if (m_device_callback) {
588 m_device_callback(info, connected);
589 }
590}
591
592// ===================================================================================
593// PipeWire callbacks (called on pw_thread_loop thread)
594// ===================================================================================
595
596void PipewireMidiBackend::on_registry_global(
597 void* userdata, uint32_t id, uint32_t,
598 const char* type, uint32_t, const struct spa_dict* props)
599{
600 auto* self = static_cast<PipewireMidiBackend*>(userdata);
601 if (!props)
602 return;
603
604 if (std::strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
605 const char* media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
606 if (!media_class || std::strcmp(media_class, "Midi/Source") != 0)
607 return;
608
609 const char* name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
610 if (!name)
611 name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
612 if (!name)
613 name = "(unknown)";
614
615 self->register_midi_port(id, name, "");
616 return;
617 }
618
619 if (std::strcmp(type, PW_TYPE_INTERFACE_Port) == 0) {
620 const char* fmt = spa_dict_lookup(props, "format.dsp");
621 const char* dir = spa_dict_lookup(props, "port.direction");
622 if (!fmt || std::strcmp(fmt, "8 bit raw midi") != 0)
623 return;
624 if (!dir || std::strcmp(dir, "out") != 0)
625 return;
626
627 const char* alias = spa_dict_lookup(props, "port.alias");
628 const char* name = spa_dict_lookup(props, "port.name");
629 const char* label = alias ? alias : name;
630 if (!label)
631 label = "(unknown)";
632
633 std::string display = label;
634 if (auto pos = display.find(':'); pos != std::string::npos)
635 display = display.substr(0, pos);
636
637 const char* obj_path = spa_dict_lookup(props, "object.path");
638 std::string object_path = obj_path ? obj_path : "";
639
640 self->register_midi_port(id, display, object_path);
641 }
642}
643
644void PipewireMidiBackend::on_registry_global_remove(void* userdata, uint32_t id)
645{
646 auto* self = static_cast<PipewireMidiBackend*>(userdata);
647
648 std::lock_guard lock(self->m_devices_mutex);
649
650 for (auto it = self->m_enumerated_devices.begin();
651 it != self->m_enumerated_devices.end(); ++it) {
652 if (it->second.pw_global_id == id) {
653 MF_INFO(C, X, "MIDI port removed: {} (pw_id={})", it->second.name, id);
654 self->notify_device_change(it->second, false);
655
656 auto open_it = self->m_open_devices.find(it->first);
657 if (open_it != self->m_open_devices.end()) {
658 open_it->second->active.store(false);
659 self->m_open_devices.erase(open_it);
660 }
661
662 self->m_enumerated_devices.erase(it);
663 return;
664 }
665 }
666}
667
668void PipewireMidiBackend::on_core_done(void* /*userdata*/, uint32_t /*id*/, int /*seq*/)
669{
670 // Intentionally empty: sync signalling is handled inline in initialize()
671 // via a local SyncState. This stub satisfies the registry pattern.
672}
673
674} // namespace MayaFlux::Core
675
676#endif // PIPEWIRE_MIDI_BACKEND
#define MF_INFO(comp, ctx,...)
#define MF_ERROR(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
#define MF_DEBUG(comp, ctx,...)
uint32_t label
Definition Contours.cpp:16
bool size
PickCallback callback
Definition Picker.cpp:22
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
std::string name
Definition VKDevice.cpp:123
@ InputBackend
Input device backend (HID, MIDI, OSC)
@ Core
Core engine, backend, subsystems.
void shutdown()
Release stored references.
Definition Forma.cpp:58
void stop()
Stop all Portal::Graphics operations.
Definition Graphics.cpp:69
void move(EditableText &s, int n)
Move the cursor by n codepoints.
Definition TextEdit.cpp:20