MayaFlux 1.0.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
PipewireBackend.cpp
Go to the documentation of this file.
1#include "PipewireBackend.hpp"
3
4#ifdef PIPEWIRE_BACKEND
5
6#include <pipewire/pipewire.h>
7#include <spa/param/audio/format-utils.h>
8#include <spa/pod/builder.h>
9#include <spa/utils/result.h>
10
11namespace MayaFlux::Core {
12
13namespace {
14 constexpr auto C = Journal::Component::Core;
15 constexpr auto X = Journal::Context::AudioBackend;
16}
17
18// ---------------------------------------------------------------------------
19// PipewireBackend
20// ---------------------------------------------------------------------------
21
23{
24 pw_init(nullptr, nullptr);
25 m_pipewire_initialized = true;
26 m_loop = pw_main_loop_new(nullptr);
27
28 if (!m_loop) {
29 cleanup();
30 error(C, X, std::source_location::current(), "pw_main_loop_new failed");
31 }
32
33 m_context = pw_context_new(pw_main_loop_get_loop(m_loop), nullptr, 0);
34 if (!m_context) {
35 cleanup();
36 error(C, X, std::source_location::current(), "pw_context_new failed");
37 }
38
39 m_core = pw_context_connect(m_context, nullptr, 0);
40 if (!m_core) {
41 cleanup();
42 error(C, X, std::source_location::current(),
43 "pw_context_connect failed - is PipeWire running?");
44 }
45
46 MF_INFO(C, X, "PipeWire backend initialised (version: {})", pw_get_library_version());
47}
48
50{
51 cleanup();
52}
53
54std::unique_ptr<AudioDevice> PipewireBackend::create_device_manager()
55{
56 auto dev = std::make_unique<PipewireDevice>(m_loop, m_context, m_core);
57 m_device_manager = dev.get();
58 return dev;
59}
60
61std::unique_ptr<AudioStream> PipewireBackend::create_stream(
62 unsigned int output_node_id,
63 unsigned int input_node_id,
64 GlobalStreamInfo& stream_info,
65 void* user_data)
66{
67 auto out_id = static_cast<uint32_t>(output_node_id);
68 auto in_id = static_cast<uint32_t>(input_node_id);
69
70 if (m_device_manager) {
71 auto* pw_dev = static_cast<PipewireDevice*>(m_device_manager);
72 if (!stream_info.output.device_name.empty())
73 out_id = pw_dev->find_output_node_id(stream_info.output.device_name);
74 if (stream_info.input.enabled && !stream_info.input.device_name.empty())
75 in_id = pw_dev->find_input_node_id(stream_info.input.device_name);
76 }
77
78 return std::make_unique<PipewireStream>(
79 out_id, in_id,
80 stream_info,
81 user_data);
82}
83
85{
86 return pw_get_library_version();
87}
88
90{
91 if (m_core) {
92 pw_core_disconnect(m_core);
93 m_core = nullptr;
94 }
95 if (m_context) {
96 pw_context_destroy(m_context);
97 m_context = nullptr;
98 }
99 if (m_loop) {
100 pw_main_loop_destroy(m_loop);
101 m_loop = nullptr;
102 }
103 if (m_pipewire_initialized) {
104 pw_deinit();
105 m_pipewire_initialized = false;
106 }
107}
108
109// ---------------------------------------------------------------------------
110// PipewireDevice
111// ---------------------------------------------------------------------------
112
113namespace {
114
115 struct EnumState {
116 pw_main_loop* loop {};
117 pw_registry* registry {};
118 spa_hook registry_listener {};
119 spa_hook core_listener {};
120 std::vector<PipewireDevice::NodeEntry>* nodes {};
121 int pending = 0;
122 };
123
124 void on_global(
125 void* userdata,
126 uint32_t id,
127 uint32_t,
128 const char* type,
129 uint32_t,
130 const struct spa_dict* props)
131 {
132 auto* st = static_cast<EnumState*>(userdata);
133 if (!props || std::strcmp(type, PW_TYPE_INTERFACE_Node) != 0)
134 return;
135
136 const char* media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
137 if (!media_class)
138 return;
139
140 const bool is_sink = std::strcmp(media_class, "Audio/Sink") == 0;
141 const bool is_source = std::strcmp(media_class, "Audio/Source") == 0;
142 if (!is_sink && !is_source)
143 return;
144
145 PipewireDevice::NodeEntry e;
146 e.id = id;
147 e.is_output = is_sink;
148
149 const char* name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
150 if (!name)
151 name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
152 if (!name)
153 name = "(unknown)";
154 e.info.name = name;
155
156 const char* prio = spa_dict_lookup(props, "priority.session");
157 e.priority_session = prio ? static_cast<uint32_t>(std::atoi(prio)) : 0;
158
159 if (is_sink) {
160 e.info.output_channels = 2;
161 } else {
162 e.info.input_channels = 2;
163 }
164
165 st->nodes->push_back(e);
166 }
167
168 void on_global_remove(void*, uint32_t) { }
169
170 void on_core_done(void* userdata, uint32_t id, int)
171 {
172 if (id != PW_ID_CORE)
173 return;
174 pw_main_loop_quit(static_cast<EnumState*>(userdata)->loop);
175 }
176
177 const pw_core_events k_core_events = {
178 .version = PW_VERSION_CORE_EVENTS,
179 .done = on_core_done,
180 };
181
182 const pw_registry_events k_registry_events = {
183 .version = PW_VERSION_REGISTRY_EVENTS,
184 .global = on_global,
185 .global_remove = on_global_remove,
186 };
187
188} // namespace
189
190PipewireDevice::PipewireDevice(pw_main_loop* loop, pw_context* ctx, pw_core* core)
191{
192 enumerate(loop, ctx, core);
193}
194
195void PipewireDevice::enumerate(pw_main_loop* loop, pw_context*, pw_core* core)
196{
197 EnumState st;
198 st.loop = loop;
199 st.nodes = &m_nodes;
200
201 st.registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0);
202 if (!st.registry)
203 error(C, X, std::source_location::current(), "pw_core_get_registry failed");
204
205 spa_zero(st.registry_listener);
206 pw_registry_add_listener(st.registry, &st.registry_listener, &k_registry_events, &st);
207
208 spa_zero(st.core_listener);
209 pw_core_add_listener(core, &st.core_listener, &k_core_events, &st);
210
211 st.pending = pw_core_sync(core, PW_ID_CORE, 0);
212 pw_main_loop_run(loop);
213
214 spa_hook_remove(&st.core_listener);
215 spa_hook_remove(&st.registry_listener);
216 pw_proxy_destroy(reinterpret_cast<pw_proxy*>(st.registry));
217
218 uint32_t best_out = 0, best_in = 0;
219 for (const auto& e : m_nodes) {
220 if (e.is_output) {
221 if (m_default_output_id == 0 || e.priority_session > best_out) {
222 m_default_output_id = e.id;
223 best_out = e.priority_session;
224 }
225 } else {
226 if (m_default_input_id == 0 || e.priority_session > best_in) {
227 m_default_input_id = e.id;
228 best_in = e.priority_session;
229 }
230 }
231 }
232
233 MF_INFO(C, X, "PipeWire enumeration: {} sink(s), {} source(s)",
234 std::count_if(m_nodes.begin(), m_nodes.end(), [](const auto& e) { return e.is_output; }),
235 std::count_if(m_nodes.begin(), m_nodes.end(), [](const auto& e) { return !e.is_output; }));
236
237 for (const auto& e : m_nodes)
238 MF_INFO(C, X, " id={} '{}' output={} priority={}", e.id, e.info.name, e.is_output, e.priority_session);
239
240 MF_INFO(C, X, "default output={} input={}", m_default_output_id, m_default_input_id);
241}
242
243std::vector<DeviceInfo> PipewireDevice::get_output_devices() const
244{
245 std::vector<DeviceInfo> out;
246 for (const auto& e : m_nodes) {
247 if (e.is_output)
248 out.push_back(e.info);
249 }
250 return out;
251}
252
253std::vector<DeviceInfo> PipewireDevice::get_input_devices() const
254{
255 std::vector<DeviceInfo> out;
256 for (const auto& e : m_nodes) {
257 if (!e.is_output)
258 out.push_back(e.info);
259 }
260 return out;
261}
262
264{
265 return static_cast<unsigned int>(m_default_output_id);
266}
267
269{
270 return static_cast<unsigned int>(m_default_input_id);
271}
272
273uint32_t PipewireDevice::find_output_node_id(std::string_view name) const
274{
275 for (const auto& e : m_nodes) {
276 if (e.is_output && e.info.name == name)
277 return e.id;
278 }
279
280 return m_default_output_id;
281}
282
283uint32_t PipewireDevice::find_input_node_id(std::string_view name) const
284{
285 for (const auto& e : m_nodes) {
286 if (!e.is_output && e.info.name == name)
287 return e.id;
288 }
289
290 return m_default_input_id;
291}
292
293// ---------------------------------------------------------------------------
294// PipewireStream
295// ---------------------------------------------------------------------------
296
297PipewireStream::PipewireStream(
298 uint32_t output_node_id,
299 uint32_t input_node_id,
300 GlobalStreamInfo& stream_info,
301 void* user_data)
302 : m_output_node_id(output_node_id)
303 , m_input_node_id(input_node_id)
304 , m_stream_info(stream_info)
305 , m_user_data(user_data)
306{
307}
308
309PipewireStream::~PipewireStream()
310{
311 if (is_running())
312 stop();
313 if (is_open())
314 close();
315}
316
317pw_time PipewireStream::get_stream_time() const
318{
319 pw_time t {};
320 if (m_output_stream)
321 pw_stream_get_time_n(m_output_stream, &t, sizeof(t));
322 return t;
323}
324
325void PipewireStream::set_process_callback(std::function<int(void*, void*, unsigned int)> cb)
326{
327 m_process_callback = std::move(cb);
328}
329
330void PipewireStream::on_input_process(void* userdata)
331{
332 auto* self = static_cast<PipewireStream*>(userdata);
333
334 pw_buffer* pb = pw_stream_dequeue_buffer(self->m_input_stream);
335 if (!pb)
336 return;
337
338 spa_meta_header* h = static_cast<spa_meta_header*>(
339 spa_buffer_find_meta_data(pb->buffer, SPA_META_Header, sizeof(*h)));
340
341 if (h && (h->flags & SPA_META_HEADER_FLAG_CORRUPTED)) {
342 MF_RT_WARN(C, X, "xrun detected on input buffer");
343 }
344
345 spa_buffer* sb = pb->buffer;
346 const uint32_t ch = self->m_stream_info.input.channels;
347 const uint32_t frames = self->m_negotiated_frames.load(std::memory_order_relaxed);
348 const size_t n_samples = static_cast<size_t>(frames) * ch;
349
350 if (sb->datas[0].data && n_samples > 0) {
351 const auto src = static_cast<const double*>(sb->datas[0].data);
352 std::memcpy(self->m_input_staging.data(), src, n_samples * sizeof(double));
353 } else {
354 std::ranges::fill(self->m_input_staging, 0.0);
355 }
356
357 pw_stream_queue_buffer(self->m_input_stream, pb);
358 self->m_input_ready.store(true, std::memory_order_release);
359}
360
361void PipewireStream::build_output_format_params(
362 uint8_t* buf, uint32_t buf_size,
363 const struct spa_pod** params, uint32_t& n_params)
364{
365 spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, buf_size);
366 struct spa_audio_info_raw raw = SPA_AUDIO_INFO_RAW_INIT(
367 .format = SPA_AUDIO_FORMAT_F64,
368 .rate = m_stream_info.sample_rate,
369 .channels = m_stream_info.output.channels);
370 params[0] = static_cast<const struct spa_pod*>(
371 spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &raw));
372 n_params = 1;
373}
374
375void PipewireStream::build_input_format_params(
376 uint8_t* buf, uint32_t buf_size,
377 const struct spa_pod** params, uint32_t& n_params)
378{
379 spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, buf_size);
380 struct spa_audio_info_raw raw = SPA_AUDIO_INFO_RAW_INIT(
381 .format = SPA_AUDIO_FORMAT_F64,
382 .rate = m_stream_info.sample_rate,
383 .channels = m_stream_info.input.channels);
384 params[0] = static_cast<const struct spa_pod*>(
385 spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &raw));
386 n_params = 1;
387}
388
389void PipewireStream::on_output_process(void* userdata)
390{
391 auto* self = static_cast<PipewireStream*>(userdata);
392 if (!self->m_process_callback)
393 return;
394
395 pw_buffer* pb = pw_stream_dequeue_buffer(self->m_output_stream);
396 if (!pb) {
397 MF_RT_WARN(C, X, "on_process: dequeue returned null");
398 return;
399 }
400
401 spa_buffer* sb = pb->buffer;
402 void* data = sb->datas[0].data;
403 const uint32_t ch = self->m_stream_info.output.channels;
404
405 uint32_t frames = self->m_rate_match
406 ? self->m_rate_match->size
407 : self->m_negotiated_frames.load(std::memory_order_relaxed);
408
409 spa_meta_header* h = static_cast<spa_meta_header*>(
410 spa_buffer_find_meta_data(pb->buffer, SPA_META_Header, sizeof(*h)));
411
412 const bool xrun = h && (h->flags & SPA_META_HEADER_FLAG_CORRUPTED);
413 if (xrun) {
414 MF_RT_WARN(C, X, "output xrun");
415 if (self->m_stream_info.handle_xruns) {
416 std::memset(data, 0, frames * ch * sizeof(double));
417 pw_stream_queue_buffer(self->m_output_stream, pb);
418 return;
419 }
420 }
421
422 if (frames == 0)
423 frames = sb->datas[0].chunk->size / (ch * sizeof(double));
424
425 if (frames == 0 || !data) {
426 MF_RT_WARN(C, X, "on_process: zero frames or null data, skipping");
427 pw_stream_queue_buffer(self->m_output_stream, pb);
428 return;
429 }
430
431 sb->datas[0].chunk->offset = 0;
432 sb->datas[0].chunk->stride = static_cast<int32_t>(ch * sizeof(double));
433 sb->datas[0].chunk->size = static_cast<size_t>(frames * ch) * sizeof(double);
434
435 void* input_ptr = nullptr;
436 if (self->m_stream_info.input.enabled && self->m_input_ready.load(std::memory_order_acquire)) {
437 input_ptr = self->m_input_staging.data();
438 self->m_input_ready.store(false, std::memory_order_release);
439 }
440
441 self->m_process_callback(data, input_ptr, frames);
442
443 pw_stream_queue_buffer(self->m_output_stream, pb);
444}
445
446void PipewireStream::on_param_changed(void* userdata, uint32_t id, const struct spa_pod* param)
447{
448 auto* self = static_cast<PipewireStream*>(userdata);
449 if (!param || id != SPA_PARAM_Format)
450 return;
451
452 struct spa_audio_info info {};
453 if (spa_format_audio_raw_parse(param, &info.info.raw) < 0) {
454 MF_WARN(C, X, "on_param_changed: failed to parse audio format");
455 return;
456 }
457
458 self->m_negotiated_frames.store(self->m_stream_info.buffer_size, std::memory_order_relaxed);
459
460 MF_INFO(C, X, "format negotiated: F64 {} ch @ {} Hz",
461 info.info.raw.channels, info.info.raw.rate);
462
463 uint8_t buf[1024];
464 spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf));
465 const uint32_t ch = self->m_stream_info.output.channels;
466
467 const auto* bufparam = static_cast<const struct spa_pod*>(
468 spa_pod_builder_add_object(&b,
469 SPA_TYPE_OBJECT_ParamBuffers, SPA_PARAM_Buffers,
470 SPA_PARAM_BUFFERS_blocks, SPA_POD_Int(1),
471 SPA_PARAM_BUFFERS_size, SPA_POD_Int(self->m_stream_info.buffer_size * ch * sizeof(double)),
472 SPA_PARAM_BUFFERS_stride, SPA_POD_Int(ch * sizeof(double))));
473
474 pw_stream_update_params(self->m_output_stream, &bufparam, 1);
475}
476
477void PipewireStream::on_state_changed(
478 void* userdata,
479 enum pw_stream_state,
480 enum pw_stream_state new_state,
481 const char* error_str)
482{
483 auto* self = static_cast<PipewireStream*>(userdata);
484
485 if (error_str)
486 MF_WARN(C, X, "stream error: {}", error_str);
487
488 switch (new_state) {
489 case PW_STREAM_STATE_STREAMING:
490 self->m_is_running.store(true, std::memory_order_release);
491 MF_INFO(C, X, "stream streaming");
492 break;
493 case PW_STREAM_STATE_PAUSED:
494 self->m_is_running.store(false, std::memory_order_release);
495 MF_INFO(C, X, "stream paused");
496 break;
497 case PW_STREAM_STATE_ERROR:
498 self->m_is_running.store(false, std::memory_order_release);
499 MF_WARN(C, X, "stream error state");
500 break;
501 default:
502 break;
503 }
504}
505
506void PipewireStream::on_io_changed(void* userdata, uint32_t id, void* area, uint32_t)
507{
508 auto* self = static_cast<PipewireStream*>(userdata);
509 if (id == SPA_IO_RateMatch && area) {
510 self->m_rate_match = static_cast<spa_io_rate_match*>(area);
511 }
512}
513
514void PipewireStream::open()
515{
516 if (m_is_open.load())
517 return;
518
519 m_thread_loop = pw_thread_loop_new("mayaflux-audio", nullptr);
520 if (!m_thread_loop)
521 error(C, X, std::source_location::current(), "pw_thread_loop_new failed");
522
523 pw_loop* loop = pw_thread_loop_get_loop(m_thread_loop);
524
525 auto read_opt = [&](const char* key) -> std::optional<std::string> {
526 auto it = m_stream_info.backend_options.find(key);
527 if (it == m_stream_info.backend_options.end())
528 return std::nullopt;
529 try {
530 return std::any_cast<std::string>(it->second);
531 } catch (const std::bad_any_cast&) {
532 return std::nullopt;
533 }
534 };
535
536 auto read_bool = [&](const char* key) -> std::optional<bool> {
537 auto it = m_stream_info.backend_options.find(key);
538 if (it == m_stream_info.backend_options.end())
539 return std::nullopt;
540 try {
541 return std::any_cast<bool>(it->second);
542 } catch (const std::bad_any_cast&) {
543 return std::nullopt;
544 }
545 };
546
547 const bool force_quantum = (m_stream_info.priority == GlobalStreamInfo::StreamPriority::REALTIME) && !read_bool("pipewire.disable_force_quantum").value_or(false);
548
549 const std::string latency_str = std::to_string(m_stream_info.buffer_size) + "/" + std::to_string(m_stream_info.sample_rate);
550
551 const std::string node_name = read_opt("pipewire.node.name").value_or("mayaflux-output");
552 const std::string node_desc = read_opt("pipewire.node.description").value_or("MayaFlux Audio");
553 const std::string media_role = read_opt("pipewire.role").value_or("Production");
554 const std::string pid_str = std::to_string(getpid());
555
556 pw_properties* props = pw_properties_new(
557 PW_KEY_MEDIA_TYPE, "Audio",
558 PW_KEY_MEDIA_CATEGORY, "Playback",
559 PW_KEY_MEDIA_ROLE, media_role.c_str(),
560 PW_KEY_MEDIA_CLASS, "Stream/Output/Audio",
561 PW_KEY_APP_NAME, "MayaFlux",
562 PW_KEY_APP_PROCESS_ID, pid_str.c_str(),
563 PW_KEY_APP_PROCESS_BINARY, "mayaflux",
564 PW_KEY_NODE_NAME, node_name.c_str(),
565 PW_KEY_NODE_DESCRIPTION, node_desc.c_str(),
566 PW_KEY_NODE_LATENCY, latency_str.c_str(),
567 nullptr);
568
569 if (force_quantum) {
570 pw_properties_set(props, "node.force-quantum",
571 std::to_string(m_stream_info.buffer_size).c_str());
572 }
573
574 m_output_stream_events = pw_stream_events {
575 .version = PW_VERSION_STREAM_EVENTS,
576 .state_changed = on_state_changed,
577 .io_changed = on_io_changed,
578 .param_changed = on_param_changed,
579 .process = on_output_process,
580 };
581
582 pw_thread_loop_lock(m_thread_loop);
583
584 m_output_stream = pw_stream_new_simple(
585 loop, "mayaflux-output", props, &m_output_stream_events, this);
586
587 if (!m_output_stream) {
588 pw_thread_loop_unlock(m_thread_loop);
589 pw_thread_loop_destroy(m_thread_loop);
590 m_thread_loop = nullptr;
591 error(C, X, std::source_location::current(), "pw_stream_new_simple failed (output)");
592 }
593
594 uint8_t buf[1024];
595 uint32_t n_params = 0;
596 const struct spa_pod* params[1];
597 build_output_format_params(buf, sizeof(buf), params, n_params);
598
599 const uint32_t flags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS;
600
601 if (auto v = read_opt("pipewire.node.name"))
602 pw_properties_set(props, PW_KEY_NODE_NAME, v->c_str());
603
604 if (auto v = read_opt("pipewire.role"))
605 pw_properties_set(props, PW_KEY_MEDIA_ROLE, v->c_str());
606
607 if (auto v = read_opt("pipewire.target_object"))
608 pw_properties_set(props, PW_KEY_TARGET_OBJECT, v->c_str());
609
610 int ret = pw_stream_connect(
611 m_output_stream,
612 PW_DIRECTION_OUTPUT,
613 PW_ID_ANY,
614 static_cast<pw_stream_flags>(flags),
615 params, n_params);
616
617 if (ret < 0) {
618 pw_thread_loop_unlock(m_thread_loop);
619 pw_stream_destroy(m_output_stream);
620 m_output_stream = nullptr;
621 pw_thread_loop_destroy(m_thread_loop);
622 m_thread_loop = nullptr;
623 error(C, X, std::source_location::current(),
624 "pw_stream_connect failed (output): {}", spa_strerror(ret));
625 }
626
627 if (m_stream_info.input.enabled && m_stream_info.input.channels > 0) {
628 const size_t staging_samples = static_cast<size_t>(m_stream_info.buffer_size) * m_stream_info.input.channels;
629 m_input_staging.assign(staging_samples, 0.0);
630
631 pw_properties* in_props = pw_properties_new(
632 PW_KEY_MEDIA_TYPE, "Audio",
633 PW_KEY_MEDIA_CATEGORY, "Capture",
634 PW_KEY_MEDIA_ROLE, "Production",
635 PW_KEY_MEDIA_CLASS, "Stream/Input/Audio",
636 PW_KEY_APP_NAME, "MayaFlux",
637 PW_KEY_NODE_NAME, "mayaflux-input",
638 PW_KEY_NODE_LATENCY, latency_str.c_str(),
639 nullptr);
640
641 m_input_stream_events = pw_stream_events {
642 .version = PW_VERSION_STREAM_EVENTS,
643 .process = on_input_process,
644 };
645
646 m_input_stream = pw_stream_new_simple(
647 loop, "mayaflux-input", in_props, &m_input_stream_events, this);
648
649 if (!m_input_stream) {
650 pw_thread_loop_unlock(m_thread_loop);
651 pw_stream_destroy(m_output_stream);
652 m_output_stream = nullptr;
653 pw_thread_loop_destroy(m_thread_loop);
654 m_thread_loop = nullptr;
655 error(C, X, std::source_location::current(), "pw_stream_new_simple failed (input)");
656 }
657
658 uint8_t in_buf[1024];
659 uint32_t in_n_params = 0;
660 const struct spa_pod* in_params[1];
661 build_input_format_params(in_buf, sizeof(in_buf), in_params, in_n_params);
662
663 ret = pw_stream_connect(
664 m_input_stream,
665 PW_DIRECTION_INPUT,
666 m_input_node_id != 0 ? m_input_node_id : PW_ID_ANY,
667 static_cast<pw_stream_flags>(flags),
668 in_params, in_n_params);
669
670 if (ret < 0) {
671 pw_thread_loop_unlock(m_thread_loop);
672 pw_stream_destroy(m_input_stream);
673 m_input_stream = nullptr;
674 pw_stream_destroy(m_output_stream);
675 m_output_stream = nullptr;
676 pw_thread_loop_destroy(m_thread_loop);
677 m_thread_loop = nullptr;
678 error(C, X, std::source_location::current(),
679 "pw_stream_connect failed (input): {}", spa_strerror(ret));
680 }
681 }
682
683 pw_thread_loop_unlock(m_thread_loop);
684
685 m_is_open.store(true, std::memory_order_release);
686
687 MF_INFO(C, X, "stream opened (quantum: {}/{}, force: {})",
688 m_stream_info.buffer_size, m_stream_info.sample_rate, force_quantum);
689}
690
691void PipewireStream::start()
692{
693 if (!m_is_open.load())
694 error(C, X, std::source_location::current(), "start() called before open()");
695 if (m_is_running.load())
696 return;
697
698 int ret = pw_thread_loop_start(m_thread_loop);
699 if (ret < 0) {
700 error(C, X, std::source_location::current(),
701 "pw_thread_loop_start failed: {}", spa_strerror(ret));
702 }
703
704 pw_thread_loop_lock(m_thread_loop);
705 pw_stream_set_active(m_output_stream, true);
706
707 if (m_input_stream) {
708 pw_stream_set_active(m_input_stream, true);
709 }
710 pw_thread_loop_unlock(m_thread_loop);
711}
712
713void PipewireStream::stop()
714{
715 if (!m_is_running.load())
716 return;
717
718 pw_thread_loop_stop(m_thread_loop);
719 m_is_running.store(false, std::memory_order_release);
720}
721
722void PipewireStream::pause()
723{
724 if (!m_is_running.load() || m_is_paused.load())
725 return;
726
727 pw_thread_loop_stop(m_thread_loop);
728 m_is_paused.store(true, std::memory_order_release);
729 m_is_running.store(false, std::memory_order_release);
730}
731
732void PipewireStream::resume()
733{
734 if (!m_is_paused.load())
735 return;
736
737 int ret = pw_thread_loop_start(m_thread_loop);
738 if (ret < 0) {
739 error(C, X, std::source_location::current(),
740 "pw_thread_loop_start failed on resume: {}", spa_strerror(ret));
741 }
742
743 pw_thread_loop_lock(m_thread_loop);
744 pw_stream_set_active(m_output_stream, true);
745 if (m_input_stream)
746 pw_stream_set_active(m_input_stream, true);
747 pw_thread_loop_unlock(m_thread_loop);
748
749 m_is_paused.store(false, std::memory_order_release);
750 m_is_running.store(true, std::memory_order_release);
751}
752
753void PipewireStream::close()
754{
755 if (!m_is_open.load())
756 return;
757
758 if (m_is_running.load() || m_is_paused.load())
759 pw_thread_loop_stop(m_thread_loop);
760
761 m_is_running.store(false, std::memory_order_release);
762 m_is_paused.store(false, std::memory_order_release);
763
764 if (m_input_stream) {
765 pw_stream_destroy(m_input_stream);
766 m_input_stream = nullptr;
767 }
768 if (m_output_stream) {
769 pw_stream_destroy(m_output_stream);
770 m_output_stream = nullptr;
771 }
772 if (m_thread_loop) {
773 pw_thread_loop_destroy(m_thread_loop);
774 m_thread_loop = nullptr;
775 }
776
777 m_is_open.store(false, std::memory_order_release);
778}
779
780bool PipewireStream::is_running() const { return m_is_running.load(std::memory_order_acquire); }
781bool PipewireStream::is_open() const { return m_is_open.load(std::memory_order_acquire); }
782
783} // namespace MayaFlux::Core
784
785#endif // PIPEWIRE_BACKEND
#define MF_INFO(comp, ctx,...)
#define MF_RT_WARN(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
bool format
float rate
uint32_t h
Definition InkPress.cpp:29
const uint32_t * src
size_t b
std::vector< float > * out
vk::PhysicalDeviceType type
Definition VKDevice.cpp:126
std::string name
Definition VKDevice.cpp:123
void cleanup() override
Releases all resources held by the backend.
std::unique_ptr< AudioStream > create_stream(unsigned int output_node_id, unsigned int input_node_id, GlobalStreamInfo &stream_info, void *user_data) override
Creates an audio stream for a specific device.
std::unique_ptr< AudioDevice > create_device_manager() override
Creates a device manager for audio endpoint discovery.
std::string get_version_string() const override
Retrieves the backend implementation version.
unsigned int get_default_input_device() const override
Gets the system's primary input device identifier.
std::vector< NodeEntry > m_nodes
std::vector< DeviceInfo > get_output_devices() const override
Retrieves information about all available output devices.
std::vector< DeviceInfo > get_input_devices() const override
Retrieves information about all available input devices.
unsigned int get_default_output_device() const override
Gets the system's primary output device identifier.
uint32_t find_input_node_id(std::string_view name) const
uint32_t find_output_node_id(std::string_view name) const
@ AudioBackend
Audio processing backend (Pipewire, wasapi, coreaudio)
void error(Component component, Context context, std::source_location location, std::string_view message)
Log an error message and optionally throw an exception.
@ Core
Core engine, backend, subsystems.
void close(std::span< const float > mask, std::span< float > tmp, std::span< float > dst, uint32_t w, uint32_t h, uint32_t radius)
Morphological closing writing into dst.
void stop()
Stop all Portal::Graphics operations.
Definition Graphics.cpp:69