MayaFlux 0.4.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 to_close.push_back(id);
189 }
190
191 for (uint32_t id : to_close)
192 close_device(id);
193
194 MF_INFO(C, X, "PipewireMidiBackend stopped");
195}
196
197void PipewireMidiBackend::shutdown()
198{
199 if (!m_initialized.load())
200 return;
201
202 if (m_running.load())
203 stop();
204
205 if (m_registry) {
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);
211 }
212
213 if (m_thread_loop)
214 pw_thread_loop_stop(m_thread_loop);
215
216 {
217 std::lock_guard lock(m_devices_mutex);
218 m_open_devices.clear();
219 m_enumerated_devices.clear();
220 }
221
222 if (m_core) {
223 pw_core_disconnect(m_core);
224 m_core = nullptr;
225 }
226
227 if (m_context) {
228 pw_context_destroy(m_context);
229 m_context = nullptr;
230 }
231
232 if (m_thread_loop) {
233 pw_thread_loop_destroy(m_thread_loop);
234 m_thread_loop = nullptr;
235 }
236
237 pw_deinit();
238 m_initialized.store(false);
239 MF_INFO(C, X, "PipewireMidiBackend shutdown complete");
240}
241
242// ===================================================================================
243// IInputBackend: Device Management
244// ===================================================================================
245
246std::vector<InputDeviceInfo> PipewireMidiBackend::get_devices() const
247{
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);
253 }
254 return result;
255}
256
257size_t PipewireMidiBackend::refresh_devices()
258{
259 std::lock_guard lock(m_devices_mutex);
260 return m_enumerated_devices.size();
261}
262
263bool PipewireMidiBackend::open_device(uint32_t device_id)
264{
265 MIDIPortInfo info;
266
267 {
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);
271 return true;
272 }
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);
276 return false;
277 }
278 info = it->second;
279 }
280
281 if (info.alsa_client < 0) {
282 MF_WARN(C, X, "MIDI port '{}' is not an ALSA seq port, skipping", info.name);
283 return false;
284 }
285
286 auto state = std::make_shared<MIDIPortState>();
287 state->info = info;
288 state->device_id = device_id;
289 {
290 std::lock_guard cb_lock(m_callback_mutex);
291 state->input_callback = m_input_callback;
292 }
293
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);
296 return false;
297 }
298
299 snd_seq_set_client_name(state->seq_handle, "MayaFlux");
300
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);
305
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);
310 return false;
311 }
312
313 snd_seq_addr_t sender {
314 .client = static_cast<unsigned char>(info.alsa_client),
315 .port = static_cast<unsigned char>(info.alsa_port)
316 };
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)
320 };
321
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);
326
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);
331 return false;
332 }
333
334 state->active.store(true);
335
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);
340
341 while (state->active.load()) {
342 int ret = poll(pfds.data(), npfds, 100);
343 if (ret <= 0)
344 continue;
345
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;
350 switch (ev->type) {
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;
355 break;
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;
360 break;
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);
365 break;
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);
370 break;
371 case SND_SEQ_EVENT_PGMCHANGE:
372 status = 0xC0 | (ev->data.control.channel & 0x0F);
373 d1 = static_cast<uint8_t>(ev->data.control.value);
374 break;
375 case SND_SEQ_EVENT_CHANPRESS:
376 status = 0xD0 | (ev->data.control.channel & 0x0F);
377 d1 = static_cast<uint8_t>(ev->data.control.value);
378 break;
379 default:
380 continue;
381 }
382
383 if (state->input_callback) {
384 state->input_callback(
385 InputValue::make_midi(status, d1, d2, state->device_id));
386 }
387 }
388 }
389 });
390
391 {
392 std::lock_guard lock(m_devices_mutex);
393 m_open_devices.insert_or_assign(device_id, state);
394 }
395
396 MF_INFO(C, X, "Opened MIDI port {}: '{}' (ALSA {}:{})",
397 device_id, info.name, info.alsa_client, info.alsa_port);
398 return true;
399}
400
401void PipewireMidiBackend::register_midi_port(
402 uint32_t pw_id, const std::string& name, const std::string& object_path)
403{
404 if (!port_matches_filter(name))
405 return;
406
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());
410
411 MIDIPortInfo info {};
412 info.id = dev_id;
413 info.name = name;
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);
422
423 m_enumerated_devices[dev_id] = info;
424
425 if (is_new) {
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);
429 }
430}
431
432void PipewireMidiBackend::close_device(uint32_t device_id)
433{
434 std::shared_ptr<MIDIPortState> state;
435 {
436 std::lock_guard lock(m_devices_mutex);
437 auto it = m_open_devices.find(device_id);
438 if (it == m_open_devices.end())
439 return;
440 state = std::move(it->second);
441 m_open_devices.erase(it);
442 }
443
444 state->active.store(false);
445
446 if (state->poll_thread.joinable())
447 state->poll_thread.join();
448
449 if (state->seq_handle) {
450 snd_seq_close(state->seq_handle);
451 state->seq_handle = nullptr;
452 }
453
454 MF_INFO(C, X, "Closed MIDI port {}: '{}'", device_id, state->info.name);
455}
456
457bool PipewireMidiBackend::is_device_open(uint32_t device_id) const
458{
459 std::lock_guard lock(m_devices_mutex);
460 return m_open_devices.find(device_id) != m_open_devices.end();
461}
462
463std::vector<uint32_t> PipewireMidiBackend::get_open_devices() const
464{
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);
470 }
471 return result;
472}
473
474void PipewireMidiBackend::set_input_callback(InputCallback callback)
475{
476 std::lock_guard lock(m_callback_mutex);
477 m_input_callback = std::move(callback);
478}
479
480void PipewireMidiBackend::set_device_callback(DeviceCallback callback)
481{
482 std::lock_guard lock(m_callback_mutex);
483 m_device_callback = std::move(callback);
484}
485
486std::string PipewireMidiBackend::get_version() const
487{
488 return pw_get_library_version();
489}
490
491// ===================================================================================
492// Private helpers
493// ===================================================================================
494
495bool PipewireMidiBackend::port_matches_filter(const std::string& port_name) const
496{
497 if (m_config.input_port_filters.empty()) {
498 return true;
499 }
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;
503 });
504}
505
506uint32_t PipewireMidiBackend::find_or_assign_device_id(uint32_t pw_global_id)
507{
508 for (const auto& [id, info] : m_enumerated_devices) {
509 if (info.pw_global_id == pw_global_id) {
510 return id;
511 }
512 }
513 return m_next_device_id++;
514}
515
516void PipewireMidiBackend::create_virtual_port_if_enabled()
517{
518 if (!m_config.enable_virtual_port) {
519 return;
520 }
521
522 pw_thread_loop_lock(m_thread_loop);
523
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(),
530 nullptr);
531
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(),
535 props,
536 nullptr,
537 nullptr);
538
539 if (virt) {
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);
543 } else {
544 MF_ERROR(C, X, "Failed to create virtual MIDI port: {}",
545 m_config.virtual_port_name);
546 }
547
548 pw_thread_loop_unlock(m_thread_loop);
549}
550
551void PipewireMidiBackend::notify_device_change(const InputDeviceInfo& info, bool connected)
552{
553 std::lock_guard lock(m_callback_mutex);
554 if (m_device_callback) {
555 m_device_callback(info, connected);
556 }
557}
558
559// ===================================================================================
560// PipeWire callbacks (called on pw_thread_loop thread)
561// ===================================================================================
562
563void PipewireMidiBackend::on_registry_global(
564 void* userdata, uint32_t id, uint32_t,
565 const char* type, uint32_t, const struct spa_dict* props)
566{
567 auto* self = static_cast<PipewireMidiBackend*>(userdata);
568 if (!props)
569 return;
570
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)
574 return;
575
576 const char* name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
577 if (!name)
578 name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
579 if (!name)
580 name = "(unknown)";
581
582 self->register_midi_port(id, name, "");
583 return;
584 }
585
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)
590 return;
591 if (!dir || std::strcmp(dir, "out") != 0)
592 return;
593
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;
597 if (!label)
598 label = "(unknown)";
599
600 std::string display = label;
601 if (auto pos = display.find(':'); pos != std::string::npos)
602 display = display.substr(0, pos);
603
604 const char* obj_path = spa_dict_lookup(props, "object.path");
605 std::string object_path = obj_path ? obj_path : "";
606
607 self->register_midi_port(id, display, object_path);
608 }
609}
610
611void PipewireMidiBackend::on_registry_global_remove(void* userdata, uint32_t id)
612{
613 auto* self = static_cast<PipewireMidiBackend*>(userdata);
614
615 std::lock_guard lock(self->m_devices_mutex);
616
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);
622
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);
627 }
628
629 self->m_enumerated_devices.erase(it);
630 return;
631 }
632 }
633}
634
635void PipewireMidiBackend::on_core_done(void* /*userdata*/, uint32_t /*id*/, int /*seq*/)
636{
637 // Intentionally empty: sync signalling is handled inline in initialize()
638 // via a local SyncState. This stub satisfies the registry pattern.
639}
640
641} // namespace MayaFlux::Core
642
643#endif // PIPEWIRE_MIDI_BACKEND
#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 shutdown()
Release stored references.
Definition Forma.cpp:168
void stop()
Stop all Portal::Graphics operations.
Definition Graphics.cpp:69