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)
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 if (
id != m_virtual_device_id)
189 to_close.push_back(
id);
193 for (uint32_t
id : to_close)
196 MF_INFO(C, X,
"PipewireMidiBackend stopped");
199void PipewireMidiBackend::shutdown()
201 if (!m_initialized.load())
204 if (m_running.load())
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);
216 pw_thread_loop_stop(m_thread_loop);
218 if (m_virtual_device_id != 0) {
219 close_device(m_virtual_device_id);
220 m_virtual_device_id = 0;
224 std::lock_guard lock(m_devices_mutex);
225 m_open_devices.clear();
226 m_enumerated_devices.clear();
230 pw_core_disconnect(m_core);
235 pw_context_destroy(m_context);
240 pw_thread_loop_destroy(m_thread_loop);
241 m_thread_loop =
nullptr;
245 m_initialized.store(
false);
246 MF_INFO(C, X,
"PipewireMidiBackend shutdown complete");
253std::vector<InputDeviceInfo> PipewireMidiBackend::get_devices()
const
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);
264size_t PipewireMidiBackend::refresh_devices()
266 std::lock_guard lock(m_devices_mutex);
267 return m_enumerated_devices.size();
270void PipewireMidiBackend::run_seq_poll(
const std::shared_ptr<MIDIPortState>& state)
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);
276 while (state->active.load()) {
277 int ret = poll(pfds.data(), npfds, 100);
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;
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;
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;
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);
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);
306 case SND_SEQ_EVENT_PGMCHANGE:
307 status = 0xC0 | (ev->data.control.channel & 0x0F);
308 d1 =
static_cast<uint8_t
>(ev->data.control.value);
310 case SND_SEQ_EVENT_CHANPRESS:
311 status = 0xD0 | (ev->data.control.channel & 0x0F);
312 d1 =
static_cast<uint8_t
>(ev->data.control.value);
318 if (state->input_callback) {
319 state->input_callback(
320 InputValue::make_midi(status, d1, d2, state->device_id));
326bool PipewireMidiBackend::open_device(uint32_t device_id)
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);
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);
344 if (info.alsa_client < 0) {
345 MF_WARN(C, X,
"MIDI port '{}' is not an ALSA seq port, skipping", info.name);
349 auto state = std::make_shared<MIDIPortState>();
351 state->device_id = device_id;
353 std::lock_guard cb_lock(m_callback_mutex);
354 state->input_callback = m_input_callback;
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);
362 snd_seq_set_client_name(state->seq_handle,
"MayaFlux");
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);
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);
376 snd_seq_addr_t sender {
377 .client =
static_cast<unsigned char>(info.alsa_client),
378 .port =
static_cast<unsigned char>(info.alsa_port)
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)
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);
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);
397 state->active.store(
true);
399 state->poll_thread = std::thread([state]() { run_seq_poll(state); });
402 std::lock_guard lock(m_devices_mutex);
403 m_open_devices.insert_or_assign(device_id, state);
406 MF_INFO(C, X,
"Opened MIDI port {}: '{}' (ALSA {}:{})",
407 device_id, info.name, info.alsa_client, info.alsa_port);
411void PipewireMidiBackend::register_midi_port(
412 uint32_t pw_id,
const std::string&
name,
const std::string& object_path)
414 if (!port_matches_filter(
name))
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());
421 MIDIPortInfo info {};
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);
433 m_enumerated_devices[dev_id] = info;
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);
442void PipewireMidiBackend::close_device(uint32_t device_id)
444 std::shared_ptr<MIDIPortState> state;
446 std::lock_guard lock(m_devices_mutex);
447 auto it = m_open_devices.find(device_id);
448 if (it == m_open_devices.end())
450 state = std::move(it->second);
451 m_open_devices.erase(it);
454 state->active.store(
false);
456 if (state->poll_thread.joinable())
457 state->poll_thread.join();
459 if (state->seq_handle) {
460 snd_seq_close(state->seq_handle);
461 state->seq_handle =
nullptr;
464 MF_INFO(C, X,
"Closed MIDI port {}: '{}'", device_id, state->info.name);
467bool PipewireMidiBackend::is_device_open(uint32_t device_id)
const
469 std::lock_guard lock(m_devices_mutex);
470 return m_open_devices.find(device_id) != m_open_devices.end();
473std::vector<uint32_t> PipewireMidiBackend::get_open_devices()
const
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);
484void PipewireMidiBackend::set_input_callback(InputCallback
callback)
486 std::lock_guard lock(m_callback_mutex);
487 m_input_callback = std::move(
callback);
490void PipewireMidiBackend::set_device_callback(DeviceCallback
callback)
492 std::lock_guard lock(m_callback_mutex);
493 m_device_callback = std::move(
callback);
496std::string PipewireMidiBackend::get_version()
const
498 return pw_get_library_version();
505bool PipewireMidiBackend::port_matches_filter(
const std::string& port_name)
const
507 if (m_config.input_port_filters.empty()) {
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;
516uint32_t PipewireMidiBackend::find_or_assign_device_id(uint32_t pw_global_id)
518 for (
const auto& [
id, info] : m_enumerated_devices) {
519 if (info.pw_global_id == pw_global_id) {
523 return m_next_device_id++;
526void PipewireMidiBackend::create_virtual_port_if_enabled()
528 if (!m_config.enable_virtual_port) {
532 auto state = std::make_shared<MIDIPortState>();
534 std::lock_guard cb_lock(m_callback_mutex);
535 state->input_callback = m_input_callback;
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);
543 snd_seq_set_client_name(state->seq_handle, m_config.virtual_port_name.c_str());
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);
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);
558 std::lock_guard lock(m_devices_mutex);
559 const uint32_t dev_id = m_next_device_id++;
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);
573 state->active.store(
true);
574 state->poll_thread = std::thread([state]() { run_seq_poll(state); });
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;
581 MF_INFO(C, X,
"Created virtual MIDI port: {}", m_config.virtual_port_name);
584void PipewireMidiBackend::notify_device_change(
const InputDeviceInfo& info,
bool connected)
586 std::lock_guard lock(m_callback_mutex);
587 if (m_device_callback) {
588 m_device_callback(info, connected);
596void PipewireMidiBackend::on_registry_global(
597 void* userdata, uint32_t
id, uint32_t,
598 const char*
type, uint32_t,
const struct spa_dict* props)
600 auto* self =
static_cast<PipewireMidiBackend*
>(userdata);
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)
609 const char*
name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
611 name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
615 self->register_midi_port(
id,
name,
"");
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)
624 if (!dir || std::strcmp(dir,
"out") != 0)
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;
633 std::string display =
label;
634 if (
auto pos = display.find(
':'); pos != std::string::npos)
635 display = display.substr(0, pos);
637 const char* obj_path = spa_dict_lookup(props,
"object.path");
638 std::string object_path = obj_path ? obj_path :
"";
640 self->register_midi_port(
id, display, object_path);
644void PipewireMidiBackend::on_registry_global_remove(
void* userdata, uint32_t
id)
646 auto* self =
static_cast<PipewireMidiBackend*
>(userdata);
648 std::lock_guard lock(self->m_devices_mutex);
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);
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);
662 self->m_enumerated_devices.erase(it);
668void 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,...)
vk::PhysicalDeviceType type
@ InputBackend
Input device backend (HID, MIDI, OSC)
@ Core
Core engine, backend, subsystems.
void stop()
Stop all Portal::Graphics operations.
void move(EditableText &s, int n)
Move the cursor by n codepoints.