2#include <ossia/detail/config.hpp>
4#if defined(OSSIA_ENABLE_PIPEWIRE)
5#if __has_include(<libremidi/backends/linux/pipewire/context.hpp>) \
6 && __has_include(<pipewire/filter.h>) \
7 && __has_include(<spa/param/latency-utils.h>)
8#define OSSIA_AUDIO_PIPEWIRE 1
10#include <ossia/audio/audio_engine.hpp>
12#include <ossia/detail/thread.hpp>
14#include <libremidi/backends/linux/pipewire/context.hpp>
15#include <libremidi/backends/linux/pipewire/filter.hpp>
16#include <libremidi/backends/linux/pipewire/loader.hpp>
17#include <libremidi/backends/linux/pipewire/subscription.hpp>
18#include <libremidi/backends/linux/pipewire/types.hpp>
20#include <pipewire/filter.h>
21#include <pipewire/keys.h>
22#include <pipewire/properties.h>
23#include <spa/param/latency-utils.h>
24#include <spa/utils/result.h>
26#include <fmt/format.h>
46 std::vector<std::string> inputs;
47 std::vector<std::string> outputs;
53class pipewire_audio_protocol :
public audio_engine
60 std::shared_ptr<libremidi::pipewire::context> loop;
62 std::vector<pw_proxy*> links;
64 std::vector<port*> input_ports;
65 std::vector<port*> output_ports;
69 explicit pipewire_audio_protocol(
70 std::shared_ptr<libremidi::pipewire::context> ctx,
71 const audio_setup& setup)
72 : loop{std::move(ctx)}
74 if (!loop || !loop->ok())
77 auto& pw = libremidi::pipewire::load();
78 if (!pw.filter_available)
82 static constexpr const struct pw_filter_events filter_events = {
83 .version = PW_VERSION_FILTER_EVENTS,
90 .process = &on_process,
92#if PW_VERSION_CORE > 3
97 std::string default_sink_name = loop->default_audio_sink_name();
99 "PipeWire filter: default sink name = '{}'", default_sink_name);
101 bool created =
false;
102 loop->with_lock([&] {
103 auto* filter_props = pw.properties_new(
104 PW_KEY_MEDIA_TYPE,
"Audio",
105 PW_KEY_MEDIA_CATEGORY,
"Duplex",
106 PW_KEY_MEDIA_ROLE,
"DSP",
107 PW_KEY_MEDIA_NAME, setup.name.c_str(),
108 PW_KEY_NODE_NAME, setup.name.c_str(),
109 PW_KEY_NODE_GROUP,
"group.dsp.0",
110 PW_KEY_NODE_DESCRIPTION,
"ossia score",
114 fmt::format(
"{}/{}", setup.buffer_size, setup.rate).c_str(),
115 PW_KEY_NODE_FORCE_QUANTUM,
116 fmt::format(
"{}", setup.buffer_size).c_str(),
117 PW_KEY_NODE_FORCE_RATE, fmt::format(
"{}", setup.rate).c_str(),
118 PW_KEY_NODE_LOCK_RATE,
"true",
119 PW_KEY_NODE_TRANSPORT_SYNC,
"true",
120 PW_KEY_NODE_ALWAYS_PROCESS,
"true",
121 PW_KEY_NODE_PAUSE_ON_IDLE,
"false",
122 PW_KEY_NODE_SUSPEND_ON_IDLE,
"false",
126 if (!default_sink_name.empty())
129 filter_props, PW_KEY_TARGET_OBJECT, default_sink_name.c_str());
132 this->filter = pw.filter_new_simple(
133 loop->bare_loop(), setup.name.c_str(), filter_props,
134 &filter_events,
this);
139 for (
const auto& name : setup.inputs)
141 auto* p =
static_cast<port*
>(pw.filter_add_port(
142 this->filter, PW_DIRECTION_INPUT,
143 PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(
struct port),
145 PW_KEY_FORMAT_DSP,
"32 bit float mono audio",
146 PW_KEY_PORT_NAME, name.c_str(),
nullptr),
148 input_ports.push_back(p);
151 for (
const auto& name : setup.outputs)
153 auto* p =
static_cast<port*
>(pw.filter_add_port(
154 this->filter, PW_DIRECTION_OUTPUT,
155 PW_FILTER_PORT_FLAG_MAP_BUFFERS,
sizeof(
struct port),
157 PW_KEY_FORMAT_DSP,
"32 bit float mono audio",
158 PW_KEY_PORT_NAME, name.c_str(),
nullptr),
160 output_ports.push_back(p);
163 created = (pw.filter_connect(
164 this->filter, PW_FILTER_FLAG_RT_PROCESS,
nullptr, 0)
169 throw std::runtime_error(
"PipeWire: could not create filter instance");
171 throw std::runtime_error(
"PipeWire: cannot connect");
173 if (!loop->synchronize())
176 "PipeWire: synchronize() failed after filter_connect — engine inactive");
181 auto node_id = filter_node_id();
182 while (node_id == 0xFFFFFFFFu)
184 if (!loop->synchronize())
187 "PipeWire: synchronize() failed while waiting for node id");
190 node_id = filter_node_id();
196 const auto num_in = input_ports.size();
197 const auto num_out = output_ports.size();
198 bool have_ports =
false;
199 for (
int j = 0; j < 200; ++j)
201 auto snap = loop->snapshot();
202 if (
const auto* self = snap.find_by_id(node_id))
204 if (self->inputs.size() >= num_in
205 && self->outputs.size() >= num_out)
211 if (!loop->synchronize())
214 "PipeWire: synchronize() failed while waiting for ports");
221 "PipeWire: ports never appeared in graph — engine inactive");
227 this->effective_buffer_size = setup.buffer_size;
228 this->effective_sample_rate = setup.rate;
229 this->effective_inputs = setup.inputs.size();
230 this->effective_outputs = setup.outputs.size();
233 std::uint32_t filter_node_id() const noexcept
237 auto& pw = libremidi::pipewire::load();
238 if (!pw.filter_get_node_id)
240 return pw.filter_get_node_id(this->filter);
248 const auto our_node = filter_node_id();
249 if (our_node == 0xFFFFFFFFu)
252 const std::string default_sink = loop->default_audio_sink_name();
253 const std::string default_source = loop->default_audio_source_name();
255 "PipeWire autoconnect: defaults src='{}' sink='{}'",
256 default_source, default_sink);
258 std::vector<std::uint32_t> source_outputs;
259 std::vector<std::uint32_t> sink_inputs;
260 std::vector<std::uint32_t> self_in_ids, self_out_ids;
261 bool have_self =
false;
263 for (
int attempt = 0; attempt < 50; ++attempt)
265 auto snap = loop->snapshot();
268 self_out_ids.clear();
270 if (
const auto* self_node = snap.find_by_id(our_node))
273 for (
const auto& p : self_node->inputs)
274 self_in_ids.push_back(p.id);
275 for (
const auto& p : self_node->outputs)
276 self_out_ids.push_back(p.id);
279 source_outputs.clear();
281 if (!default_source.empty())
283 if (
const auto* n = snap.find_by_name(default_source))
285 for (
const auto& p : n->outputs)
286 source_outputs.push_back(p.id);
289 if (!default_sink.empty())
291 if (
const auto* n = snap.find_by_name(default_sink))
293 for (
const auto& p : n->inputs)
294 sink_inputs.push_back(p.id);
300 if (have_self && !source_outputs.empty() && !sink_inputs.empty())
302 if (!loop->synchronize())
310 "PipeWire autoconnect: src_ports={}, sink_ports={}, "
311 "self_in={}, self_out={}",
312 source_outputs.size(), sink_inputs.size(),
313 self_in_ids.size(), self_out_ids.size());
316 auto snap = loop->snapshot();
318 "PipeWire autoconnect: snapshot has {} nodes", snap.nodes.size());
319 for (
const auto& n : snap.nodes)
322 "PipeWire autoconnect: snap id={} name='{}' class='{}' "
323 "inputs={} outputs={}",
324 n.id, n.name, n.media_class_str, n.inputs.size(),
329 for (std::size_t i = 0;
330 i < self_in_ids.size() && i < source_outputs.size(); ++i)
332 if (
auto* link = libremidi::pipewire::link_ports(
333 *loop, source_outputs[i], self_in_ids[i]))
334 links.push_back(link);
336 for (std::size_t i = 0;
337 i < self_out_ids.size() && i < sink_inputs.size(); ++i)
339 if (
auto* link = libremidi::pipewire::link_ports(
340 *loop, self_out_ids[i], sink_inputs[i]))
341 links.push_back(link);
345 void wait(
int ms)
override
348 std::this_thread::sleep_for(std::chrono::milliseconds(ms));
351 bool running()
const override {
return loop && activated; }
355 audio_engine::stop();
356 if (!loop || !activated)
359 auto& pw = libremidi::pipewire::load();
365 loop->with_lock([&] {
366 if (
int res = pw.filter_disconnect(this->filter); res < 0)
369 "PipeWire: filter_disconnect failed: {}", spa_strerror(res));
372 (void)loop->synchronize();
375 for (
auto* link : this->links)
376 libremidi::pipewire::unlink_ports(*loop, link);
381 loop->with_lock([&] {
382 pw.filter_destroy(this->filter);
383 this->filter =
nullptr;
387 (void)loop->synchronize();
391 ~pipewire_audio_protocol()
override { stop(); }
395 clear_buffers(pipewire_audio_protocol& self, std::uint32_t nframes,
398 auto& pw = libremidi::pipewire::load();
399 for (std::size_t i = 0; i < outputs; i++)
402 =
static_cast<float*
>(pw.filter_get_dsp_buffer(self.output_ports[i], nframes));
404 for (std::size_t j = 0; j < nframes; j++)
409 void do_process(std::uint32_t nframes,
double secs)
411 auto& pw = libremidi::pipewire::load();
415 const auto inputs = input_ports.size();
416 const auto outputs = output_ports.size();
420 clear_buffers(*
this, nframes, outputs);
424 auto* dummy =
static_cast<float*
>(alloca(
sizeof(
float) * nframes));
425 std::memset(dummy, 0,
sizeof(
float) * nframes);
427 auto** float_input =
static_cast<float**
>(alloca(
sizeof(
float*) * inputs));
428 auto** float_output =
static_cast<float**
>(alloca(
sizeof(
float*) * outputs));
429 for (std::size_t i = 0; i < inputs; i++)
432 =
static_cast<float*
>(pw.filter_get_dsp_buffer(input_ports[i], nframes));
433 if (float_input[i] ==
nullptr)
434 float_input[i] = dummy;
436 for (std::size_t i = 0; i < outputs; i++)
439 =
static_cast<float*
>(pw.filter_get_dsp_buffer(output_ports[i], nframes));
440 if (float_output[i] ==
nullptr)
441 float_output[i] = dummy;
444 ossia::audio_tick_state ts{
445 float_input, float_output, (int)inputs, (
int)outputs, nframes, secs};
450 static void on_process(
void* userdata,
struct spa_io_position* position)
452 [[maybe_unused]]
static const thread_local auto _ = [] {
453 ossia::set_thread_name(
"ossia audio 0");
454 ossia::set_thread_pinned(thread_type::Audio, 0);
461 auto& self = *
static_cast<pipewire_audio_protocol*
>(userdata);
462 const std::uint32_t nframes = position->clock.duration;
463 const double current_time_ns = position->clock.nsec * 1e-9;
465 if (nframes !=
static_cast<std::uint32_t
>(self.effective_buffer_size))
468 "PipeWire: unexpected block size {} (expected {}), skipping cycle",
469 nframes, self.effective_buffer_size);
473 self.do_process(nframes, current_time_ns);
spdlog::logger & logger() noexcept
Where the errors will be logged. Default is stderr.
Definition context.cpp:118