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>
24 pw_init(
nullptr,
nullptr);
25 m_pipewire_initialized =
true;
26 m_loop = pw_main_loop_new(
nullptr);
30 error(C, X, std::source_location::current(),
"pw_main_loop_new failed");
33 m_context = pw_context_new(pw_main_loop_get_loop(m_loop),
nullptr, 0);
36 error(C, X, std::source_location::current(),
"pw_context_new failed");
39 m_core = pw_context_connect(m_context,
nullptr, 0);
42 error(C, X, std::source_location::current(),
43 "pw_context_connect failed - is PipeWire running?");
46 MF_INFO(C, X,
"PipeWire backend initialised (version: {})", pw_get_library_version());
56 auto dev = std::make_unique<PipewireDevice>(m_loop, m_context, m_core);
57 m_device_manager = dev.get();
62 unsigned int output_node_id,
63 unsigned int input_node_id,
64 GlobalStreamInfo& stream_info,
67 auto out_id =
static_cast<uint32_t
>(output_node_id);
68 auto in_id =
static_cast<uint32_t
>(input_node_id);
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);
78 return std::make_unique<PipewireStream>(
86 return pw_get_library_version();
92 pw_core_disconnect(m_core);
96 pw_context_destroy(m_context);
100 pw_main_loop_destroy(m_loop);
103 if (m_pipewire_initialized) {
105 m_pipewire_initialized =
false;
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 {};
130 const struct spa_dict* props)
132 auto* st =
static_cast<EnumState*
>(userdata);
133 if (!props || std::strcmp(
type, PW_TYPE_INTERFACE_Node) != 0)
136 const char* media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
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)
145 PipewireDevice::NodeEntry e;
147 e.is_output = is_sink;
149 const char*
name = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
151 name = spa_dict_lookup(props, PW_KEY_NODE_NAME);
156 const char* prio = spa_dict_lookup(props,
"priority.session");
157 e.priority_session = prio ?
static_cast<uint32_t
>(std::atoi(prio)) : 0;
160 e.info.output_channels = 2;
162 e.info.input_channels = 2;
165 st->nodes->push_back(e);
168 void on_global_remove(
void*, uint32_t) { }
170 void on_core_done(
void* userdata, uint32_t
id,
int)
172 if (
id != PW_ID_CORE)
174 pw_main_loop_quit(
static_cast<EnumState*
>(userdata)->loop);
177 const pw_core_events k_core_events = {
178 .version = PW_VERSION_CORE_EVENTS,
179 .done = on_core_done,
182 const pw_registry_events k_registry_events = {
183 .version = PW_VERSION_REGISTRY_EVENTS,
185 .global_remove = on_global_remove,
190PipewireDevice::PipewireDevice(pw_main_loop* loop, pw_context* ctx, pw_core* core)
192 enumerate(loop, ctx, core);
195void PipewireDevice::enumerate(pw_main_loop* loop, pw_context*, pw_core* core)
201 st.registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0);
203 error(C, X, std::source_location::current(),
"pw_core_get_registry failed");
205 spa_zero(st.registry_listener);
206 pw_registry_add_listener(st.registry, &st.registry_listener, &k_registry_events, &st);
208 spa_zero(st.core_listener);
209 pw_core_add_listener(core, &st.core_listener, &k_core_events, &st);
211 st.pending = pw_core_sync(core, PW_ID_CORE, 0);
212 pw_main_loop_run(loop);
214 spa_hook_remove(&st.core_listener);
215 spa_hook_remove(&st.registry_listener);
216 pw_proxy_destroy(
reinterpret_cast<pw_proxy*
>(st.registry));
218 uint32_t best_out = 0, best_in = 0;
219 for (
const auto& e :
m_nodes) {
223 best_out = e.priority_session;
228 best_in = e.priority_session;
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; }));
238 MF_INFO(
C,
X,
" id={} '{}' output={} priority={}", e.id, e.info.
name, e.is_output, e.priority_session);
245 std::vector<DeviceInfo>
out;
246 for (
const auto& e :
m_nodes) {
248 out.push_back(e.info);
255 std::vector<DeviceInfo>
out;
256 for (
const auto& e :
m_nodes) {
258 out.push_back(e.info);
275 for (
const auto& e :
m_nodes) {
276 if (e.is_output && e.info.name ==
name)
285 for (
const auto& e :
m_nodes) {
286 if (!e.is_output && e.info.name ==
name)
297PipewireStream::PipewireStream(
298 uint32_t output_node_id,
299 uint32_t input_node_id,
300 GlobalStreamInfo& stream_info,
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)
309PipewireStream::~PipewireStream()
317pw_time PipewireStream::get_stream_time()
const
321 pw_stream_get_time_n(m_output_stream, &t,
sizeof(t));
325void PipewireStream::set_process_callback(std::function<
int(
void*,
void*,
unsigned int)> cb)
327 m_process_callback = std::move(cb);
330void PipewireStream::on_input_process(
void* userdata)
332 auto* self =
static_cast<PipewireStream*
>(userdata);
334 pw_buffer* pb = pw_stream_dequeue_buffer(self->m_input_stream);
338 spa_meta_header*
h =
static_cast<spa_meta_header*
>(
339 spa_buffer_find_meta_data(pb->buffer, SPA_META_Header,
sizeof(*
h)));
341 if (
h && (
h->flags & SPA_META_HEADER_FLAG_CORRUPTED)) {
342 MF_RT_WARN(C, X,
"xrun detected on input buffer");
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;
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));
354 std::ranges::fill(self->m_input_staging, 0.0);
357 pw_stream_queue_buffer(self->m_input_stream, pb);
358 self->m_input_ready.store(
true, std::memory_order_release);
361void PipewireStream::build_output_format_params(
362 uint8_t* buf, uint32_t buf_size,
363 const struct spa_pod** params, uint32_t& n_params)
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));
375void PipewireStream::build_input_format_params(
376 uint8_t* buf, uint32_t buf_size,
377 const struct spa_pod** params, uint32_t& n_params)
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));
389void PipewireStream::on_output_process(
void* userdata)
391 auto* self =
static_cast<PipewireStream*
>(userdata);
392 if (!self->m_process_callback)
395 pw_buffer* pb = pw_stream_dequeue_buffer(self->m_output_stream);
397 MF_RT_WARN(C, X,
"on_process: dequeue returned null");
401 spa_buffer* sb = pb->buffer;
402 void* data = sb->datas[0].data;
403 const uint32_t ch = self->m_stream_info.output.channels;
405 uint32_t frames = self->m_rate_match
406 ? self->m_rate_match->size
407 : self->m_negotiated_frames.load(std::memory_order_relaxed);
409 spa_meta_header*
h =
static_cast<spa_meta_header*
>(
410 spa_buffer_find_meta_data(pb->buffer, SPA_META_Header,
sizeof(*
h)));
412 const bool xrun =
h && (
h->flags & SPA_META_HEADER_FLAG_CORRUPTED);
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);
423 frames = sb->datas[0].chunk->size / (ch *
sizeof(double));
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);
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);
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);
441 self->m_process_callback(data, input_ptr, frames);
443 pw_stream_queue_buffer(self->m_output_stream, pb);
446void PipewireStream::on_param_changed(
void* userdata, uint32_t
id,
const struct spa_pod* param)
448 auto* self =
static_cast<PipewireStream*
>(userdata);
449 if (!param ||
id != SPA_PARAM_Format)
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");
458 self->m_negotiated_frames.store(self->m_stream_info.buffer_size, std::memory_order_relaxed);
460 MF_INFO(C, X,
"format negotiated: F64 {} ch @ {} Hz",
461 info.info.raw.channels, info.info.raw.rate);
464 spa_pod_builder
b = SPA_POD_BUILDER_INIT(buf,
sizeof(buf));
465 const uint32_t ch = self->m_stream_info.output.channels;
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))));
474 pw_stream_update_params(self->m_output_stream, &bufparam, 1);
477void PipewireStream::on_state_changed(
479 enum pw_stream_state,
480 enum pw_stream_state new_state,
481 const char* error_str)
483 auto* self =
static_cast<PipewireStream*
>(userdata);
486 MF_WARN(C, X,
"stream error: {}", error_str);
489 case PW_STREAM_STATE_STREAMING:
490 self->m_is_running.store(
true, std::memory_order_release);
491 MF_INFO(C, X,
"stream streaming");
493 case PW_STREAM_STATE_PAUSED:
494 self->m_is_running.store(
false, std::memory_order_release);
495 MF_INFO(C, X,
"stream paused");
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");
506void PipewireStream::on_io_changed(
void* userdata, uint32_t
id,
void* area, uint32_t)
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);
514void PipewireStream::open()
516 if (m_is_open.load())
519 m_thread_loop = pw_thread_loop_new(
"mayaflux-audio",
nullptr);
521 error(C, X, std::source_location::current(),
"pw_thread_loop_new failed");
523 pw_loop* loop = pw_thread_loop_get_loop(m_thread_loop);
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())
530 return std::any_cast<std::string>(it->second);
531 }
catch (
const std::bad_any_cast&) {
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())
541 return std::any_cast<bool>(it->second);
542 }
catch (
const std::bad_any_cast&) {
547 const bool force_quantum = (m_stream_info.priority == GlobalStreamInfo::StreamPriority::REALTIME) && !read_bool(
"pipewire.disable_force_quantum").value_or(
false);
549 const std::string latency_str = std::to_string(m_stream_info.buffer_size) +
"/" + std::to_string(m_stream_info.sample_rate);
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());
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(),
570 pw_properties_set(props,
"node.force-quantum",
571 std::to_string(m_stream_info.buffer_size).c_str());
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,
582 pw_thread_loop_lock(m_thread_loop);
584 m_output_stream = pw_stream_new_simple(
585 loop,
"mayaflux-output", props, &m_output_stream_events,
this);
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)");
595 uint32_t n_params = 0;
596 const struct spa_pod* params[1];
597 build_output_format_params(buf,
sizeof(buf), params, n_params);
599 const uint32_t flags = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS;
601 if (
auto v = read_opt(
"pipewire.node.name"))
602 pw_properties_set(props, PW_KEY_NODE_NAME, v->c_str());
604 if (
auto v = read_opt(
"pipewire.role"))
605 pw_properties_set(props, PW_KEY_MEDIA_ROLE, v->c_str());
607 if (
auto v = read_opt(
"pipewire.target_object"))
608 pw_properties_set(props, PW_KEY_TARGET_OBJECT, v->c_str());
610 int ret = pw_stream_connect(
614 static_cast<pw_stream_flags
>(flags),
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));
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);
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(),
641 m_input_stream_events = pw_stream_events {
642 .version = PW_VERSION_STREAM_EVENTS,
643 .process = on_input_process,
646 m_input_stream = pw_stream_new_simple(
647 loop,
"mayaflux-input", in_props, &m_input_stream_events,
this);
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)");
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);
663 ret = pw_stream_connect(
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);
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));
683 pw_thread_loop_unlock(m_thread_loop);
685 m_is_open.store(
true, std::memory_order_release);
687 MF_INFO(C, X,
"stream opened (quantum: {}/{}, force: {})",
688 m_stream_info.buffer_size, m_stream_info.sample_rate, force_quantum);
691void PipewireStream::start()
693 if (!m_is_open.load())
694 error(C, X, std::source_location::current(),
"start() called before open()");
695 if (m_is_running.load())
698 int ret = pw_thread_loop_start(m_thread_loop);
700 error(C, X, std::source_location::current(),
701 "pw_thread_loop_start failed: {}", spa_strerror(ret));
704 pw_thread_loop_lock(m_thread_loop);
705 pw_stream_set_active(m_output_stream,
true);
707 if (m_input_stream) {
708 pw_stream_set_active(m_input_stream,
true);
710 pw_thread_loop_unlock(m_thread_loop);
713void PipewireStream::stop()
715 if (!m_is_running.load())
718 pw_thread_loop_stop(m_thread_loop);
719 m_is_running.store(
false, std::memory_order_release);
722void PipewireStream::pause()
724 if (!m_is_running.load() || m_is_paused.load())
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);
732void PipewireStream::resume()
734 if (!m_is_paused.load())
737 int ret = pw_thread_loop_start(m_thread_loop);
739 error(C, X, std::source_location::current(),
740 "pw_thread_loop_start failed on resume: {}", spa_strerror(ret));
743 pw_thread_loop_lock(m_thread_loop);
744 pw_stream_set_active(m_output_stream,
true);
746 pw_stream_set_active(m_input_stream,
true);
747 pw_thread_loop_unlock(m_thread_loop);
749 m_is_paused.store(
false, std::memory_order_release);
750 m_is_running.store(
true, std::memory_order_release);
753void PipewireStream::close()
755 if (!m_is_open.load())
758 if (m_is_running.load() || m_is_paused.load())
759 pw_thread_loop_stop(m_thread_loop);
761 m_is_running.store(
false, std::memory_order_release);
762 m_is_paused.store(
false, std::memory_order_release);
764 if (m_input_stream) {
765 pw_stream_destroy(m_input_stream);
766 m_input_stream =
nullptr;
768 if (m_output_stream) {
769 pw_stream_destroy(m_output_stream);
770 m_output_stream =
nullptr;
773 pw_thread_loop_destroy(m_thread_loop);
774 m_thread_loop =
nullptr;
777 m_is_open.store(
false, std::memory_order_release);
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); }
#define MF_INFO(comp, ctx,...)
#define MF_RT_WARN(comp, ctx,...)
#define MF_WARN(comp, ctx,...)
std::vector< float > * out
vk::PhysicalDeviceType type
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.
~PipewireBackend() override
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.
uint32_t m_default_output_id
unsigned int get_default_output_device() const override
Gets the system's primary output device identifier.
uint32_t m_default_input_id
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.