summaryrefslogtreecommitdiff
path: root/contrib/SDL-3.2.8/src/audio/pipewire
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/SDL-3.2.8/src/audio/pipewire')
-rw-r--r--contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c1349
-rw-r--r--contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h43
2 files changed, 1392 insertions, 0 deletions
diff --git a/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c b/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c
new file mode 100644
index 0000000..e3f9439
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c
@@ -0,0 +1,1349 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21
22#include "SDL_internal.h"
23
24#ifdef SDL_AUDIO_DRIVER_PIPEWIRE
25
26#include "SDL_pipewire.h"
27
28#include <pipewire/extensions/metadata.h>
29#include <spa/param/audio/format-utils.h>
30#include <spa/utils/json.h>
31
32/*
33 * This seems to be a sane lower limit as Pipewire
34 * uses it in several of it's own modules.
35 */
36#define PW_MIN_SAMPLES 32 // About 0.67ms at 48kHz
37#define PW_BASE_CLOCK_RATE 48000
38
39#define PW_POD_BUFFER_LENGTH 1024
40#define PW_THREAD_NAME_BUFFER_LENGTH 128
41#define PW_MAX_IDENTIFIER_LENGTH 256
42
43enum PW_READY_FLAGS
44{
45 PW_READY_FLAG_BUFFER_ADDED = 0x1,
46 PW_READY_FLAG_STREAM_READY = 0x2,
47 PW_READY_FLAG_ALL_PREOPEN_BITS = 0x3,
48 PW_READY_FLAG_OPEN_COMPLETE = 0x4,
49 PW_READY_FLAG_ALL_BITS = 0x7
50};
51
52#define PW_ID_TO_HANDLE(x) (void *)((uintptr_t)x)
53#define PW_HANDLE_TO_ID(x) (uint32_t)((uintptr_t)x)
54
55static bool pipewire_initialized = false;
56
57// Pipewire entry points
58static const char *(*PIPEWIRE_pw_get_library_version)(void);
59static void (*PIPEWIRE_pw_init)(int *, char ***);
60static void (*PIPEWIRE_pw_deinit)(void);
61static struct pw_main_loop *(*PIPEWIRE_pw_main_loop_new)(const struct spa_dict *loop);
62static struct pw_loop *(*PIPEWIRE_pw_main_loop_get_loop)(struct pw_main_loop *loop);
63static int (*PIPEWIRE_pw_main_loop_run)(struct pw_main_loop *loop);
64static int (*PIPEWIRE_pw_main_loop_quit)(struct pw_main_loop *loop);
65static void(*PIPEWIRE_pw_main_loop_destroy)(struct pw_main_loop *loop);
66static struct pw_thread_loop *(*PIPEWIRE_pw_thread_loop_new)(const char *, const struct spa_dict *);
67static void (*PIPEWIRE_pw_thread_loop_destroy)(struct pw_thread_loop *);
68static void (*PIPEWIRE_pw_thread_loop_stop)(struct pw_thread_loop *);
69static struct pw_loop *(*PIPEWIRE_pw_thread_loop_get_loop)(struct pw_thread_loop *);
70static void (*PIPEWIRE_pw_thread_loop_lock)(struct pw_thread_loop *);
71static void (*PIPEWIRE_pw_thread_loop_unlock)(struct pw_thread_loop *);
72static void (*PIPEWIRE_pw_thread_loop_signal)(struct pw_thread_loop *, bool);
73static void (*PIPEWIRE_pw_thread_loop_wait)(struct pw_thread_loop *);
74static int (*PIPEWIRE_pw_thread_loop_start)(struct pw_thread_loop *);
75static struct pw_context *(*PIPEWIRE_pw_context_new)(struct pw_loop *, struct pw_properties *, size_t);
76static void (*PIPEWIRE_pw_context_destroy)(struct pw_context *);
77static struct pw_core *(*PIPEWIRE_pw_context_connect)(struct pw_context *, struct pw_properties *, size_t);
78static void (*PIPEWIRE_pw_proxy_add_object_listener)(struct pw_proxy *, struct spa_hook *, const void *, void *);
79static void *(*PIPEWIRE_pw_proxy_get_user_data)(struct pw_proxy *);
80static void (*PIPEWIRE_pw_proxy_destroy)(struct pw_proxy *);
81static int (*PIPEWIRE_pw_core_disconnect)(struct pw_core *);
82static struct pw_stream *(*PIPEWIRE_pw_stream_new_simple)(struct pw_loop *, const char *, struct pw_properties *,
83 const struct pw_stream_events *, void *);
84static void (*PIPEWIRE_pw_stream_destroy)(struct pw_stream *);
85static int (*PIPEWIRE_pw_stream_connect)(struct pw_stream *, enum pw_direction, uint32_t, enum pw_stream_flags,
86 const struct spa_pod **, uint32_t);
87static enum pw_stream_state (*PIPEWIRE_pw_stream_get_state)(struct pw_stream *stream, const char **error);
88static struct pw_buffer *(*PIPEWIRE_pw_stream_dequeue_buffer)(struct pw_stream *);
89static int (*PIPEWIRE_pw_stream_queue_buffer)(struct pw_stream *, struct pw_buffer *);
90static struct pw_properties *(*PIPEWIRE_pw_properties_new)(const char *, ...)SPA_SENTINEL;
91static int (*PIPEWIRE_pw_properties_set)(struct pw_properties *, const char *, const char *);
92static int (*PIPEWIRE_pw_properties_setf)(struct pw_properties *, const char *, const char *, ...) SPA_PRINTF_FUNC(3, 4);
93
94#ifdef SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC
95
96static const char *pipewire_library = SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC;
97static SDL_SharedObject *pipewire_handle = NULL;
98
99static bool pipewire_dlsym(const char *fn, void **addr)
100{
101 *addr = SDL_LoadFunction(pipewire_handle, fn);
102 if (!*addr) {
103 // Don't call SDL_SetError(): SDL_LoadFunction already did.
104 return false;
105 }
106
107 return true;
108}
109
110#define SDL_PIPEWIRE_SYM(x) \
111 if (!pipewire_dlsym(#x, (void **)(char *)&PIPEWIRE_##x)) \
112 return false
113
114static bool load_pipewire_library(void)
115{
116 pipewire_handle = SDL_LoadObject(pipewire_library);
117 return pipewire_handle ? true : false;
118}
119
120static void unload_pipewire_library(void)
121{
122 if (pipewire_handle) {
123 SDL_UnloadObject(pipewire_handle);
124 pipewire_handle = NULL;
125 }
126}
127
128#else
129
130#define SDL_PIPEWIRE_SYM(x) PIPEWIRE_##x = x
131
132static bool load_pipewire_library(void)
133{
134 return true;
135}
136
137static void unload_pipewire_library(void)
138{
139 // Nothing to do
140}
141
142#endif // SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC
143
144static bool load_pipewire_syms(void)
145{
146 SDL_PIPEWIRE_SYM(pw_get_library_version);
147 SDL_PIPEWIRE_SYM(pw_init);
148 SDL_PIPEWIRE_SYM(pw_deinit);
149 SDL_PIPEWIRE_SYM(pw_main_loop_new);
150 SDL_PIPEWIRE_SYM(pw_main_loop_get_loop);
151 SDL_PIPEWIRE_SYM(pw_main_loop_run);
152 SDL_PIPEWIRE_SYM(pw_main_loop_quit);
153 SDL_PIPEWIRE_SYM(pw_main_loop_destroy);
154 SDL_PIPEWIRE_SYM(pw_thread_loop_new);
155 SDL_PIPEWIRE_SYM(pw_thread_loop_destroy);
156 SDL_PIPEWIRE_SYM(pw_thread_loop_stop);
157 SDL_PIPEWIRE_SYM(pw_thread_loop_get_loop);
158 SDL_PIPEWIRE_SYM(pw_thread_loop_lock);
159 SDL_PIPEWIRE_SYM(pw_thread_loop_unlock);
160 SDL_PIPEWIRE_SYM(pw_thread_loop_signal);
161 SDL_PIPEWIRE_SYM(pw_thread_loop_wait);
162 SDL_PIPEWIRE_SYM(pw_thread_loop_start);
163 SDL_PIPEWIRE_SYM(pw_context_new);
164 SDL_PIPEWIRE_SYM(pw_context_destroy);
165 SDL_PIPEWIRE_SYM(pw_context_connect);
166 SDL_PIPEWIRE_SYM(pw_proxy_add_object_listener);
167 SDL_PIPEWIRE_SYM(pw_proxy_get_user_data);
168 SDL_PIPEWIRE_SYM(pw_proxy_destroy);
169 SDL_PIPEWIRE_SYM(pw_core_disconnect);
170 SDL_PIPEWIRE_SYM(pw_stream_new_simple);
171 SDL_PIPEWIRE_SYM(pw_stream_destroy);
172 SDL_PIPEWIRE_SYM(pw_stream_connect);
173 SDL_PIPEWIRE_SYM(pw_stream_get_state);
174 SDL_PIPEWIRE_SYM(pw_stream_dequeue_buffer);
175 SDL_PIPEWIRE_SYM(pw_stream_queue_buffer);
176 SDL_PIPEWIRE_SYM(pw_properties_new);
177 SDL_PIPEWIRE_SYM(pw_properties_set);
178 SDL_PIPEWIRE_SYM(pw_properties_setf);
179
180 return true;
181}
182
183static bool init_pipewire_library(void)
184{
185 if (load_pipewire_library()) {
186 if (load_pipewire_syms()) {
187 PIPEWIRE_pw_init(NULL, NULL);
188 return true;
189 }
190 }
191
192 return false;
193}
194
195static void deinit_pipewire_library(void)
196{
197 PIPEWIRE_pw_deinit();
198 unload_pipewire_library();
199}
200
201// A generic Pipewire node object used for enumeration.
202struct node_object
203{
204 struct spa_list link;
205
206 Uint32 id;
207 int seq;
208 bool persist;
209
210 /*
211 * NOTE: If used, this is *must* be allocated with SDL_malloc() or similar
212 * as SDL_free() will be called on it when the node_object is destroyed.
213 *
214 * If ownership of the referenced memory is transferred, this must be set
215 * to NULL or the memory will be freed when the node_object is destroyed.
216 */
217 void *userdata;
218
219 struct pw_proxy *proxy;
220 struct spa_hook node_listener;
221 struct spa_hook core_listener;
222};
223
224// A sink/source node used for stream I/O.
225struct io_node
226{
227 struct spa_list link;
228
229 Uint32 id;
230 bool recording;
231 SDL_AudioSpec spec;
232
233 const char *name; // Friendly name
234 const char *path; // OS identifier (i.e. ALSA endpoint)
235
236 char buf[]; // Buffer to hold the name and path strings.
237};
238
239// The global hotplug thread and associated objects.
240static struct pw_thread_loop *hotplug_loop;
241static struct pw_core *hotplug_core;
242static struct pw_context *hotplug_context;
243static struct pw_registry *hotplug_registry;
244static struct spa_hook hotplug_registry_listener;
245static struct spa_hook hotplug_core_listener;
246static struct spa_list hotplug_pending_list;
247static struct spa_list hotplug_io_list;
248static int hotplug_init_seq_val;
249static bool hotplug_init_complete;
250static bool hotplug_events_enabled;
251
252static int pipewire_version_major;
253static int pipewire_version_minor;
254static int pipewire_version_patch;
255static char *pipewire_default_sink_id = NULL;
256static char *pipewire_default_source_id = NULL;
257
258static bool pipewire_core_version_at_least(int major, int minor, int patch)
259{
260 return (pipewire_version_major >= major) &&
261 (pipewire_version_major > major || pipewire_version_minor >= minor) &&
262 (pipewire_version_major > major || pipewire_version_minor > minor || pipewire_version_patch >= patch);
263}
264
265// The active node list
266static bool io_list_check_add(struct io_node *node)
267{
268 struct io_node *n;
269 bool ret = true;
270
271 // See if the node is already in the list
272 spa_list_for_each (n, &hotplug_io_list, link) {
273 if (n->id == node->id) {
274 ret = false;
275 goto dup_found;
276 }
277 }
278
279 // Add to the list if the node doesn't already exist
280 spa_list_append(&hotplug_io_list, &node->link);
281
282 if (hotplug_events_enabled) {
283 SDL_AddAudioDevice(node->recording, node->name, &node->spec, PW_ID_TO_HANDLE(node->id));
284 }
285
286dup_found:
287
288 return ret;
289}
290
291static void io_list_remove(Uint32 id)
292{
293 struct io_node *n, *temp;
294
295 // Find and remove the node from the list
296 spa_list_for_each_safe (n, temp, &hotplug_io_list, link) {
297 if (n->id == id) {
298 spa_list_remove(&n->link);
299
300 if (hotplug_events_enabled) {
301 SDL_AudioDeviceDisconnected(SDL_FindPhysicalAudioDeviceByHandle(PW_ID_TO_HANDLE(id)));
302 }
303
304 SDL_free(n);
305
306 break;
307 }
308 }
309}
310
311static void io_list_clear(void)
312{
313 struct io_node *n, *temp;
314
315 spa_list_for_each_safe (n, temp, &hotplug_io_list, link) {
316 spa_list_remove(&n->link);
317 SDL_free(n);
318 }
319}
320
321static struct io_node *io_list_get_by_id(Uint32 id)
322{
323 struct io_node *n, *temp;
324 spa_list_for_each_safe (n, temp, &hotplug_io_list, link) {
325 if (n->id == id) {
326 return n;
327 }
328 }
329 return NULL;
330}
331
332static void node_object_destroy(struct node_object *node)
333{
334 SDL_assert(node != NULL);
335
336 spa_list_remove(&node->link);
337 spa_hook_remove(&node->node_listener);
338 spa_hook_remove(&node->core_listener);
339 SDL_free(node->userdata);
340 PIPEWIRE_pw_proxy_destroy(node->proxy);
341}
342
343// The pending node list
344static void pending_list_add(struct node_object *node)
345{
346 SDL_assert(node != NULL);
347 spa_list_append(&hotplug_pending_list, &node->link);
348}
349
350static void pending_list_remove(Uint32 id)
351{
352 struct node_object *node, *temp;
353
354 spa_list_for_each_safe (node, temp, &hotplug_pending_list, link) {
355 if (node->id == id) {
356 node_object_destroy(node);
357 }
358 }
359}
360
361static void pending_list_clear(void)
362{
363 struct node_object *node, *temp;
364
365 spa_list_for_each_safe (node, temp, &hotplug_pending_list, link) {
366 node_object_destroy(node);
367 }
368}
369
370static void *node_object_new(Uint32 id, const char *type, Uint32 version, const void *funcs, const struct pw_core_events *core_events)
371{
372 struct pw_proxy *proxy;
373 struct node_object *node;
374
375 // Create the proxy object
376 proxy = pw_registry_bind(hotplug_registry, id, type, version, sizeof(struct node_object));
377 if (!proxy) {
378 SDL_SetError("Pipewire: Failed to create proxy object (%i)", errno);
379 return NULL;
380 }
381
382 node = PIPEWIRE_pw_proxy_get_user_data(proxy);
383 SDL_zerop(node);
384
385 node->id = id;
386 node->proxy = proxy;
387
388 // Add the callbacks
389 pw_core_add_listener(hotplug_core, &node->core_listener, core_events, node);
390 PIPEWIRE_pw_proxy_add_object_listener(node->proxy, &node->node_listener, funcs, node);
391
392 // Add the node to the active list
393 pending_list_add(node);
394
395 return node;
396}
397
398// Core sync points
399static void core_events_hotplug_init_callback(void *object, uint32_t id, int seq)
400{
401 if (id == PW_ID_CORE && seq == hotplug_init_seq_val) {
402 // This core listener is no longer needed.
403 spa_hook_remove(&hotplug_core_listener);
404
405 // Signal that the initial I/O list is populated
406 hotplug_init_complete = true;
407 PIPEWIRE_pw_thread_loop_signal(hotplug_loop, false);
408 }
409}
410
411static void core_events_hotplug_info_callback(void *data, const struct pw_core_info *info)
412{
413 if (SDL_sscanf(info->version, "%d.%d.%d", &pipewire_version_major, &pipewire_version_minor, &pipewire_version_patch) < 3) {
414 pipewire_version_major = 0;
415 pipewire_version_minor = 0;
416 pipewire_version_patch = 0;
417 }
418}
419
420static void core_events_interface_callback(void *object, uint32_t id, int seq)
421{
422 struct node_object *node = object;
423 struct io_node *io = node->userdata;
424
425 if (id == PW_ID_CORE && seq == node->seq) {
426 /*
427 * Move the I/O node to the connected list.
428 * On success, the list owns the I/O node object.
429 */
430 if (io_list_check_add(io)) {
431 node->userdata = NULL;
432 }
433
434 node_object_destroy(node);
435 }
436}
437
438static void core_events_metadata_callback(void *object, uint32_t id, int seq)
439{
440 struct node_object *node = object;
441
442 if (id == PW_ID_CORE && seq == node->seq && !node->persist) {
443 node_object_destroy(node);
444 }
445}
446
447static const struct pw_core_events hotplug_init_core_events = { PW_VERSION_CORE_EVENTS, .info = core_events_hotplug_info_callback, .done = core_events_hotplug_init_callback };
448static const struct pw_core_events interface_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_interface_callback };
449static const struct pw_core_events metadata_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_metadata_callback };
450
451static void hotplug_core_sync(struct node_object *node)
452{
453 /*
454 * Node sync events *must* come before the hotplug init sync events or the initial
455 * I/O list will be incomplete when the main hotplug sync point is hit.
456 */
457 if (node) {
458 node->seq = pw_core_sync(hotplug_core, PW_ID_CORE, node->seq);
459 }
460
461 if (!hotplug_init_complete) {
462 hotplug_init_seq_val = pw_core_sync(hotplug_core, PW_ID_CORE, hotplug_init_seq_val);
463 }
464}
465
466// Helpers for retrieving values from params
467static bool get_range_param(const struct spa_pod *param, Uint32 key, int *def, int *min, int *max)
468{
469 const struct spa_pod_prop *prop;
470 struct spa_pod *value;
471 Uint32 n_values, choice;
472
473 prop = spa_pod_find_prop(param, NULL, key);
474
475 if (prop && prop->value.type == SPA_TYPE_Choice) {
476 value = spa_pod_get_values(&prop->value, &n_values, &choice);
477
478 if (n_values == 3 && choice == SPA_CHOICE_Range) {
479 Uint32 *v = SPA_POD_BODY(value);
480
481 if (v) {
482 if (def) {
483 *def = (int)v[0];
484 }
485 if (min) {
486 *min = (int)v[1];
487 }
488 if (max) {
489 *max = (int)v[2];
490 }
491
492 return true;
493 }
494 }
495 }
496
497 return false;
498}
499
500static bool get_int_param(const struct spa_pod *param, Uint32 key, int *val)
501{
502 const struct spa_pod_prop *prop;
503 Sint32 v;
504
505 prop = spa_pod_find_prop(param, NULL, key);
506
507 if (prop && spa_pod_get_int(&prop->value, &v) == 0) {
508 if (val) {
509 *val = (int)v;
510 }
511
512 return true;
513 }
514
515 return false;
516}
517
518static SDL_AudioFormat SPAFormatToSDL(enum spa_audio_format spafmt)
519{
520 switch (spafmt) {
521 #define CHECKFMT(spa,sdl) case SPA_AUDIO_FORMAT_##spa: return SDL_AUDIO_##sdl
522 CHECKFMT(U8, U8);
523 CHECKFMT(S8, S8);
524 CHECKFMT(S16_LE, S16LE);
525 CHECKFMT(S16_BE, S16BE);
526 CHECKFMT(S32_LE, S32LE);
527 CHECKFMT(S32_BE, S32BE);
528 CHECKFMT(F32_LE, F32LE);
529 CHECKFMT(F32_BE, F32BE);
530 #undef CHECKFMT
531 default: break;
532 }
533
534 return SDL_AUDIO_UNKNOWN;
535}
536
537// Interface node callbacks
538static void node_event_info(void *object, const struct pw_node_info *info)
539{
540 struct node_object *node = object;
541 struct io_node *io = node->userdata;
542 const char *prop_val;
543 Uint32 i;
544
545 if (info) {
546 prop_val = spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS);
547 if (prop_val) {
548 io->spec.channels = (Uint8)SDL_atoi(prop_val);
549 }
550
551 // Need to parse the parameters to get the sample rate
552 for (i = 0; i < info->n_params; ++i) {
553 pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL);
554 }
555
556 hotplug_core_sync(node);
557 }
558}
559
560static void node_event_param(void *object, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param)
561{
562 struct node_object *node = object;
563 struct io_node *io = node->userdata;
564
565 if ((id == SPA_PARAM_Format) && (io->spec.format == SDL_AUDIO_UNKNOWN)) {
566 struct spa_audio_info_raw info;
567 SDL_zero(info);
568 if (spa_format_audio_raw_parse(param, &info) == 0) {
569 //SDL_Log("Sink Format: %d, Rate: %d Hz, Channels: %d", info.format, info.rate, info.channels);
570 io->spec.format = SPAFormatToSDL(info.format);
571 }
572 }
573
574 // Get the default frequency
575 if (io->spec.freq == 0) {
576 get_range_param(param, SPA_FORMAT_AUDIO_rate, &io->spec.freq, NULL, NULL);
577 }
578
579 /*
580 * The channel count should have come from the node properties,
581 * but it is stored here as well. If one failed, try the other.
582 */
583 if (io->spec.channels == 0) {
584 int channels;
585 if (get_int_param(param, SPA_FORMAT_AUDIO_channels, &channels)) {
586 io->spec.channels = (Uint8)channels;
587 }
588 }
589}
590
591static const struct pw_node_events interface_node_events = { PW_VERSION_NODE_EVENTS, .info = node_event_info,
592 .param = node_event_param };
593
594static char *get_name_from_json(const char *json)
595{
596 struct spa_json parser[2];
597 char key[7]; // "name"
598 char value[PW_MAX_IDENTIFIER_LENGTH];
599 spa_json_init(&parser[0], json, SDL_strlen(json));
600 if (spa_json_enter_object(&parser[0], &parser[1]) <= 0) {
601 // Not actually JSON
602 return NULL;
603 }
604 if (spa_json_get_string(&parser[1], key, sizeof(key)) <= 0) {
605 // Not actually a key/value pair
606 return NULL;
607 }
608 if (spa_json_get_string(&parser[1], value, sizeof(value)) <= 0) {
609 // Somehow had a key with no value?
610 return NULL;
611 }
612 return SDL_strdup(value);
613}
614
615static void change_default_device(const char *path)
616{
617 if (hotplug_events_enabled) {
618 struct io_node *n, *temp;
619 spa_list_for_each_safe (n, temp, &hotplug_io_list, link) {
620 if (SDL_strcmp(n->path, path) == 0) {
621 SDL_DefaultAudioDeviceChanged(SDL_FindPhysicalAudioDeviceByHandle(PW_ID_TO_HANDLE(n->id)));
622 return; // found it, we're done.
623 }
624 }
625 }
626}
627
628// Metadata node callback
629static int metadata_property(void *object, Uint32 subject, const char *key, const char *type, const char *value)
630{
631 struct node_object *node = object;
632
633 if (subject == PW_ID_CORE && key && value) {
634 if (!SDL_strcmp(key, "default.audio.sink")) {
635 if (pipewire_default_sink_id) {
636 SDL_free(pipewire_default_sink_id);
637 }
638 pipewire_default_sink_id = get_name_from_json(value);
639 node->persist = true;
640 change_default_device(pipewire_default_sink_id);
641 } else if (!SDL_strcmp(key, "default.audio.source")) {
642 if (pipewire_default_source_id) {
643 SDL_free(pipewire_default_source_id);
644 }
645 pipewire_default_source_id = get_name_from_json(value);
646 node->persist = true;
647 change_default_device(pipewire_default_source_id);
648 }
649 }
650
651 return 0;
652}
653
654static const struct pw_metadata_events metadata_node_events = { PW_VERSION_METADATA_EVENTS, .property = metadata_property };
655
656// Global registry callbacks
657static void registry_event_global_callback(void *object, uint32_t id, uint32_t permissions, const char *type, uint32_t version,
658 const struct spa_dict *props)
659{
660 struct node_object *node;
661
662 // We're only interested in interface and metadata nodes.
663 if (!SDL_strcmp(type, PW_TYPE_INTERFACE_Node)) {
664 const char *media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS);
665
666 if (media_class) {
667 const char *node_desc;
668 const char *node_path;
669 struct io_node *io;
670 bool recording;
671 int desc_buffer_len;
672 int path_buffer_len;
673
674 // Just want sink and source
675 if (!SDL_strcasecmp(media_class, "Audio/Sink")) {
676 recording = false;
677 } else if (!SDL_strcasecmp(media_class, "Audio/Source")) {
678 recording = true;
679 } else {
680 return;
681 }
682
683 node_desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION);
684 node_path = spa_dict_lookup(props, PW_KEY_NODE_NAME);
685
686 if (node_desc && node_path) {
687 node = node_object_new(id, type, version, &interface_node_events, &interface_core_events);
688 if (!node) {
689 SDL_SetError("Pipewire: Failed to allocate interface node");
690 return;
691 }
692
693 // Allocate and initialize the I/O node information struct
694 desc_buffer_len = SDL_strlen(node_desc) + 1;
695 path_buffer_len = SDL_strlen(node_path) + 1;
696 node->userdata = io = SDL_calloc(1, sizeof(struct io_node) + desc_buffer_len + path_buffer_len);
697 if (!io) {
698 node_object_destroy(node);
699 return;
700 }
701
702 // Begin setting the node properties
703 io->id = id;
704 io->recording = recording;
705 if (io->spec.format == SDL_AUDIO_UNKNOWN) {
706 io->spec.format = SDL_AUDIO_S16; // we'll go conservative here if for some reason the format isn't known.
707 }
708 io->name = io->buf;
709 io->path = io->buf + desc_buffer_len;
710 SDL_strlcpy(io->buf, node_desc, desc_buffer_len);
711 SDL_strlcpy(io->buf + desc_buffer_len, node_path, path_buffer_len);
712
713 // Update sync points
714 hotplug_core_sync(node);
715 }
716 }
717 } else if (!SDL_strcmp(type, PW_TYPE_INTERFACE_Metadata)) {
718 node = node_object_new(id, type, version, &metadata_node_events, &metadata_core_events);
719 if (!node) {
720 SDL_SetError("Pipewire: Failed to allocate metadata node");
721 return;
722 }
723
724 // Update sync points
725 hotplug_core_sync(node);
726 }
727}
728
729static void registry_event_remove_callback(void *object, uint32_t id)
730{
731 io_list_remove(id);
732 pending_list_remove(id);
733}
734
735static const struct pw_registry_events registry_events = { PW_VERSION_REGISTRY_EVENTS, .global = registry_event_global_callback,
736 .global_remove = registry_event_remove_callback };
737
738// The hotplug thread
739static bool hotplug_loop_init(void)
740{
741 int res;
742
743 spa_list_init(&hotplug_pending_list);
744 spa_list_init(&hotplug_io_list);
745
746 hotplug_loop = PIPEWIRE_pw_thread_loop_new("SDLPwAudioPlug", NULL);
747 if (!hotplug_loop) {
748 return SDL_SetError("Pipewire: Failed to create hotplug detection loop (%i)", errno);
749 }
750
751 hotplug_context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_thread_loop_get_loop(hotplug_loop), NULL, 0);
752 if (!hotplug_context) {
753 return SDL_SetError("Pipewire: Failed to create hotplug detection context (%i)", errno);
754 }
755
756 hotplug_core = PIPEWIRE_pw_context_connect(hotplug_context, NULL, 0);
757 if (!hotplug_core) {
758 return SDL_SetError("Pipewire: Failed to connect hotplug detection context (%i)", errno);
759 }
760
761 hotplug_registry = pw_core_get_registry(hotplug_core, PW_VERSION_REGISTRY, 0);
762 if (!hotplug_registry) {
763 return SDL_SetError("Pipewire: Failed to acquire hotplug detection registry (%i)", errno);
764 }
765
766 spa_zero(hotplug_registry_listener);
767 pw_registry_add_listener(hotplug_registry, &hotplug_registry_listener, &registry_events, NULL);
768
769 spa_zero(hotplug_core_listener);
770 pw_core_add_listener(hotplug_core, &hotplug_core_listener, &hotplug_init_core_events, NULL);
771
772 hotplug_init_seq_val = pw_core_sync(hotplug_core, PW_ID_CORE, 0);
773
774 res = PIPEWIRE_pw_thread_loop_start(hotplug_loop);
775 if (res != 0) {
776 return SDL_SetError("Pipewire: Failed to start hotplug detection loop");
777 }
778
779 return true;
780}
781
782static void hotplug_loop_destroy(void)
783{
784 if (hotplug_loop) {
785 PIPEWIRE_pw_thread_loop_stop(hotplug_loop);
786 }
787
788 pending_list_clear();
789 io_list_clear();
790
791 hotplug_init_complete = false;
792 hotplug_events_enabled = false;
793
794 if (pipewire_default_sink_id) {
795 SDL_free(pipewire_default_sink_id);
796 pipewire_default_sink_id = NULL;
797 }
798 if (pipewire_default_source_id) {
799 SDL_free(pipewire_default_source_id);
800 pipewire_default_source_id = NULL;
801 }
802
803 if (hotplug_registry) {
804 PIPEWIRE_pw_proxy_destroy((struct pw_proxy *)hotplug_registry);
805 hotplug_registry = NULL;
806 }
807
808 if (hotplug_core) {
809 PIPEWIRE_pw_core_disconnect(hotplug_core);
810 hotplug_core = NULL;
811 }
812
813 if (hotplug_context) {
814 PIPEWIRE_pw_context_destroy(hotplug_context);
815 hotplug_context = NULL;
816 }
817
818 if (hotplug_loop) {
819 PIPEWIRE_pw_thread_loop_destroy(hotplug_loop);
820 hotplug_loop = NULL;
821 }
822}
823
824static void PIPEWIRE_DetectDevices(SDL_AudioDevice **default_playback, SDL_AudioDevice **default_recording)
825{
826 struct io_node *io;
827
828 PIPEWIRE_pw_thread_loop_lock(hotplug_loop);
829
830 // Wait until the initial registry enumeration is complete
831 if (!hotplug_init_complete) {
832 PIPEWIRE_pw_thread_loop_wait(hotplug_loop);
833 }
834
835 spa_list_for_each (io, &hotplug_io_list, link) {
836 SDL_AudioDevice *device = SDL_AddAudioDevice(io->recording, io->name, &io->spec, PW_ID_TO_HANDLE(io->id));
837 if (pipewire_default_sink_id && SDL_strcmp(io->path, pipewire_default_sink_id) == 0) {
838 if (!io->recording) {
839 *default_playback = device;
840 }
841 } else if (pipewire_default_source_id && SDL_strcmp(io->path, pipewire_default_source_id) == 0) {
842 if (io->recording) {
843 *default_recording = device;
844 }
845 }
846 }
847
848 hotplug_events_enabled = true;
849
850 PIPEWIRE_pw_thread_loop_unlock(hotplug_loop);
851}
852
853// Channel maps that match the order in SDL_Audio.h
854static const enum spa_audio_channel PIPEWIRE_channel_map_1[] = { SPA_AUDIO_CHANNEL_MONO };
855static const enum spa_audio_channel PIPEWIRE_channel_map_2[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR };
856static const enum spa_audio_channel PIPEWIRE_channel_map_3[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_LFE };
857static const enum spa_audio_channel PIPEWIRE_channel_map_4[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_RL,
858 SPA_AUDIO_CHANNEL_RR };
859static const enum spa_audio_channel PIPEWIRE_channel_map_5[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC,
860 SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR };
861static const enum spa_audio_channel PIPEWIRE_channel_map_6[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC,
862 SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR };
863static const enum spa_audio_channel PIPEWIRE_channel_map_7[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC,
864 SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_RL,
865 SPA_AUDIO_CHANNEL_RR };
866static const enum spa_audio_channel PIPEWIRE_channel_map_8[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC,
867 SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR,
868 SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR };
869
870#define COPY_CHANNEL_MAP(c) SDL_memcpy(info->position, PIPEWIRE_channel_map_##c, sizeof(PIPEWIRE_channel_map_##c))
871
872static void initialize_spa_info(const SDL_AudioSpec *spec, struct spa_audio_info_raw *info)
873{
874 info->channels = spec->channels;
875 info->rate = spec->freq;
876
877 switch (spec->channels) {
878 case 1:
879 COPY_CHANNEL_MAP(1);
880 break;
881 case 2:
882 COPY_CHANNEL_MAP(2);
883 break;
884 case 3:
885 COPY_CHANNEL_MAP(3);
886 break;
887 case 4:
888 COPY_CHANNEL_MAP(4);
889 break;
890 case 5:
891 COPY_CHANNEL_MAP(5);
892 break;
893 case 6:
894 COPY_CHANNEL_MAP(6);
895 break;
896 case 7:
897 COPY_CHANNEL_MAP(7);
898 break;
899 case 8:
900 COPY_CHANNEL_MAP(8);
901 break;
902 }
903
904 // Pipewire natively supports all of SDL's sample formats
905 switch (spec->format) {
906 case SDL_AUDIO_U8:
907 info->format = SPA_AUDIO_FORMAT_U8;
908 break;
909 case SDL_AUDIO_S8:
910 info->format = SPA_AUDIO_FORMAT_S8;
911 break;
912 case SDL_AUDIO_S16LE:
913 info->format = SPA_AUDIO_FORMAT_S16_LE;
914 break;
915 case SDL_AUDIO_S16BE:
916 info->format = SPA_AUDIO_FORMAT_S16_BE;
917 break;
918 case SDL_AUDIO_S32LE:
919 info->format = SPA_AUDIO_FORMAT_S32_LE;
920 break;
921 case SDL_AUDIO_S32BE:
922 info->format = SPA_AUDIO_FORMAT_S32_BE;
923 break;
924 case SDL_AUDIO_F32LE:
925 info->format = SPA_AUDIO_FORMAT_F32_LE;
926 break;
927 case SDL_AUDIO_F32BE:
928 info->format = SPA_AUDIO_FORMAT_F32_BE;
929 break;
930 default:
931 info->format = SPA_AUDIO_FORMAT_UNKNOWN;
932 break;
933 }
934}
935
936static Uint8 *PIPEWIRE_GetDeviceBuf(SDL_AudioDevice *device, int *buffer_size)
937{
938 // See if a buffer is available. If this returns NULL, SDL_PlaybackAudioThreadIterate will return false, but since we own the thread, it won't kill playback.
939 // !!! FIXME: It's not clear to me if this ever returns NULL or if this was just defensive coding.
940
941 struct pw_stream *stream = device->hidden->stream;
942 struct pw_buffer *pw_buf = PIPEWIRE_pw_stream_dequeue_buffer(stream);
943 if (pw_buf == NULL) {
944 return NULL;
945 }
946
947 struct spa_buffer *spa_buf = pw_buf->buffer;
948 if (spa_buf->datas[0].data == NULL) {
949 PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf);
950 return NULL;
951 }
952
953 device->hidden->pw_buf = pw_buf;
954 return (Uint8 *) spa_buf->datas[0].data;
955}
956
957static bool PIPEWIRE_PlayDevice(SDL_AudioDevice *device, const Uint8 *buffer, int buffer_size)
958{
959 struct pw_stream *stream = device->hidden->stream;
960 struct pw_buffer *pw_buf = device->hidden->pw_buf;
961 struct spa_buffer *spa_buf = pw_buf->buffer;
962 spa_buf->datas[0].chunk->offset = 0;
963 spa_buf->datas[0].chunk->stride = device->hidden->stride;
964 spa_buf->datas[0].chunk->size = buffer_size;
965
966 PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf);
967 device->hidden->pw_buf = NULL;
968
969 return true;
970}
971
972static void output_callback(void *data)
973{
974 SDL_AudioDevice *device = (SDL_AudioDevice *) data;
975
976 // this callback can fire in a background thread during OpenDevice, while we're still blocking
977 // _with the device lock_ until the stream is ready, causing a deadlock. Write silence in this case.
978 if (device->hidden->stream_init_status != PW_READY_FLAG_ALL_BITS) {
979 int bufsize = 0;
980 Uint8 *buf = PIPEWIRE_GetDeviceBuf(device, &bufsize);
981 if (buf && bufsize) {
982 SDL_memset(buf, device->silence_value, bufsize);
983 }
984 PIPEWIRE_PlayDevice(device, buf, bufsize);
985 return;
986 }
987
988 SDL_PlaybackAudioThreadIterate(device);
989}
990
991static void PIPEWIRE_FlushRecording(SDL_AudioDevice *device)
992{
993 struct pw_stream *stream = device->hidden->stream;
994 struct pw_buffer *pw_buf = PIPEWIRE_pw_stream_dequeue_buffer(stream);
995 if (pw_buf) { // just requeue it without any further thought.
996 PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf);
997 }
998}
999
1000static int PIPEWIRE_RecordDevice(SDL_AudioDevice *device, void *buffer, int buflen)
1001{
1002 struct pw_stream *stream = device->hidden->stream;
1003 struct pw_buffer *pw_buf = PIPEWIRE_pw_stream_dequeue_buffer(stream);
1004 if (!pw_buf) {
1005 return 0;
1006 }
1007
1008 struct spa_buffer *spa_buf = pw_buf->buffer;
1009 if (!spa_buf) {
1010 PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf);
1011 return 0;
1012 }
1013
1014 const Uint8 *src = (const Uint8 *)spa_buf->datas[0].data;
1015 const Uint32 offset = SPA_MIN(spa_buf->datas[0].chunk->offset, spa_buf->datas[0].maxsize);
1016 const Uint32 size = SPA_MIN(spa_buf->datas[0].chunk->size, spa_buf->datas[0].maxsize - offset);
1017 const int cpy = SDL_min(buflen, (int) size);
1018
1019 SDL_assert(size <= buflen); // We'll have to reengineer some stuff if this turns out to not be true.
1020
1021 SDL_memcpy(buffer, src + offset, cpy);
1022 PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf);
1023
1024 return cpy;
1025}
1026
1027static void input_callback(void *data)
1028{
1029 SDL_AudioDevice *device = (SDL_AudioDevice *) data;
1030
1031 // this callback can fire in a background thread during OpenDevice, while we're still blocking
1032 // _with the device lock_ until the stream is ready, causing a deadlock. Drop data in this case.
1033 if (device->hidden->stream_init_status != PW_READY_FLAG_ALL_BITS) {
1034 PIPEWIRE_FlushRecording(device);
1035 return;
1036 }
1037
1038 SDL_RecordingAudioThreadIterate(device);
1039}
1040
1041static void stream_add_buffer_callback(void *data, struct pw_buffer *buffer)
1042{
1043 SDL_AudioDevice *device = (SDL_AudioDevice *) data;
1044
1045 if (device->recording == false) {
1046 /* Clamp the output spec samples and size to the max size of the Pipewire buffer.
1047 If they exceed the maximum size of the Pipewire buffer, double buffering will be used. */
1048 if (device->buffer_size > buffer->buffer->datas[0].maxsize) {
1049 SDL_LockMutex(device->lock);
1050 device->sample_frames = buffer->buffer->datas[0].maxsize / device->hidden->stride;
1051 device->buffer_size = buffer->buffer->datas[0].maxsize;
1052 SDL_UnlockMutex(device->lock);
1053 }
1054 }
1055
1056 device->hidden->stream_init_status |= PW_READY_FLAG_BUFFER_ADDED;
1057 PIPEWIRE_pw_thread_loop_signal(device->hidden->loop, false);
1058}
1059
1060static void stream_state_changed_callback(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error)
1061{
1062 SDL_AudioDevice *device = (SDL_AudioDevice *) data;
1063
1064 if (state == PW_STREAM_STATE_STREAMING) {
1065 device->hidden->stream_init_status |= PW_READY_FLAG_STREAM_READY;
1066 }
1067
1068 if (state == PW_STREAM_STATE_STREAMING || state == PW_STREAM_STATE_ERROR) {
1069 PIPEWIRE_pw_thread_loop_signal(device->hidden->loop, false);
1070 }
1071}
1072
1073static const struct pw_stream_events stream_output_events = { PW_VERSION_STREAM_EVENTS,
1074 .state_changed = stream_state_changed_callback,
1075 .add_buffer = stream_add_buffer_callback,
1076 .process = output_callback };
1077static const struct pw_stream_events stream_input_events = { PW_VERSION_STREAM_EVENTS,
1078 .state_changed = stream_state_changed_callback,
1079 .add_buffer = stream_add_buffer_callback,
1080 .process = input_callback };
1081
1082static bool PIPEWIRE_OpenDevice(SDL_AudioDevice *device)
1083{
1084 /*
1085 * NOTE: The PW_STREAM_FLAG_RT_PROCESS flag can be set to call the stream
1086 * processing callback from the realtime thread. However, it comes with some
1087 * caveats: no file IO, allocations, locking or other blocking operations
1088 * must occur in the mixer callback. As this cannot be guaranteed when the
1089 * callback is in the calling application, this flag is omitted.
1090 */
1091 static const enum pw_stream_flags STREAM_FLAGS = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS;
1092
1093 char thread_name[PW_THREAD_NAME_BUFFER_LENGTH];
1094 Uint8 pod_buffer[PW_POD_BUFFER_LENGTH];
1095 struct spa_pod_builder b = SPA_POD_BUILDER_INIT(pod_buffer, sizeof(pod_buffer));
1096 struct spa_audio_info_raw spa_info = { 0 };
1097 const struct spa_pod *params = NULL;
1098 struct SDL_PrivateAudioData *priv;
1099 struct pw_properties *props;
1100 const char *app_name, *icon_name, *app_id, *stream_name, *stream_role, *error;
1101 Uint32 node_id = !device->handle ? PW_ID_ANY : PW_HANDLE_TO_ID(device->handle);
1102 const bool recording = device->recording;
1103 int res;
1104
1105 // Clamp the period size to sane values
1106 const int min_period = PW_MIN_SAMPLES * SPA_MAX(device->spec.freq / PW_BASE_CLOCK_RATE, 1);
1107
1108 // Get the hints for the application name, icon name, stream name and role
1109 app_name = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_NAME_STRING);
1110
1111 icon_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_APP_ICON_NAME);
1112 if (!icon_name || *icon_name == '\0') {
1113 icon_name = "applications-games";
1114 }
1115
1116 // App ID. Default to NULL if not available.
1117 app_id = SDL_GetAppMetadataProperty(SDL_PROP_APP_METADATA_IDENTIFIER_STRING);
1118
1119 stream_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_NAME);
1120 if (!stream_name || *stream_name == '\0') {
1121 stream_name = "Audio Stream";
1122 }
1123
1124 /*
1125 * 'Music' is the default used internally by Pipewire and it's modules,
1126 * but 'Game' seems more appropriate for the majority of SDL applications.
1127 */
1128 stream_role = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE);
1129 if (!stream_role || *stream_role == '\0') {
1130 stream_role = "Game";
1131 }
1132
1133 // Initialize the Pipewire stream info from the SDL audio spec
1134 initialize_spa_info(&device->spec, &spa_info);
1135 params = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &spa_info);
1136 if (!params) {
1137 return SDL_SetError("Pipewire: Failed to set audio format parameters");
1138 }
1139
1140 priv = SDL_calloc(1, sizeof(struct SDL_PrivateAudioData));
1141 device->hidden = priv;
1142 if (!priv) {
1143 return false;
1144 }
1145
1146 // Size of a single audio frame in bytes
1147 priv->stride = SDL_AUDIO_FRAMESIZE(device->spec);
1148
1149 if (device->sample_frames < min_period) {
1150 device->sample_frames = min_period;
1151 }
1152
1153 SDL_UpdatedAudioDeviceFormat(device);
1154
1155 SDL_GetAudioThreadName(device, thread_name, sizeof(thread_name));
1156 priv->loop = PIPEWIRE_pw_thread_loop_new(thread_name, NULL);
1157 if (!priv->loop) {
1158 return SDL_SetError("Pipewire: Failed to create stream loop (%i)", errno);
1159 }
1160
1161 // Load the realtime module so Pipewire can set the loop thread to the appropriate priority.
1162 props = PIPEWIRE_pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL);
1163 if (!props) {
1164 return SDL_SetError("Pipewire: Failed to create stream context properties (%i)", errno);
1165 }
1166
1167 priv->context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_thread_loop_get_loop(priv->loop), props, 0);
1168 if (!priv->context) {
1169 return SDL_SetError("Pipewire: Failed to create stream context (%i)", errno);
1170 }
1171
1172 props = PIPEWIRE_pw_properties_new(NULL, NULL);
1173 if (!props) {
1174 return SDL_SetError("Pipewire: Failed to create stream properties (%i)", errno);
1175 }
1176
1177 PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_TYPE, "Audio");
1178 PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, recording ? "Capture" : "Playback");
1179 PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_ROLE, stream_role);
1180 PIPEWIRE_pw_properties_set(props, PW_KEY_APP_NAME, app_name);
1181 PIPEWIRE_pw_properties_set(props, PW_KEY_APP_ICON_NAME, icon_name);
1182 if (app_id) {
1183 PIPEWIRE_pw_properties_set(props, PW_KEY_APP_ID, app_id);
1184 }
1185 PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_NAME, stream_name);
1186 PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, stream_name);
1187 PIPEWIRE_pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%i", device->sample_frames, device->spec.freq);
1188 PIPEWIRE_pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", device->spec.freq);
1189 PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_ALWAYS_PROCESS, "true");
1190 PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_DONT_RECONNECT, "true"); // Requesting a specific device, don't migrate to new default hardware.
1191
1192 if (node_id != PW_ID_ANY) {
1193 PIPEWIRE_pw_thread_loop_lock(hotplug_loop);
1194 const struct io_node *node = io_list_get_by_id(node_id);
1195 if (node) {
1196 PIPEWIRE_pw_properties_set(props, PW_KEY_TARGET_OBJECT, node->path);
1197 }
1198 PIPEWIRE_pw_thread_loop_unlock(hotplug_loop);
1199 }
1200
1201 // Create the new stream
1202 priv->stream = PIPEWIRE_pw_stream_new_simple(PIPEWIRE_pw_thread_loop_get_loop(priv->loop), stream_name, props,
1203 recording ? &stream_input_events : &stream_output_events, device);
1204 if (!priv->stream) {
1205 return SDL_SetError("Pipewire: Failed to create stream (%i)", errno);
1206 }
1207
1208 // The target node is passed via PW_KEY_TARGET_OBJECT; target_id is a legacy parameter and must be PW_ID_ANY.
1209 res = PIPEWIRE_pw_stream_connect(priv->stream, recording ? PW_DIRECTION_INPUT : PW_DIRECTION_OUTPUT, PW_ID_ANY, STREAM_FLAGS,
1210 &params, 1);
1211 if (res != 0) {
1212 return SDL_SetError("Pipewire: Failed to connect stream");
1213 }
1214
1215 res = PIPEWIRE_pw_thread_loop_start(priv->loop);
1216 if (res != 0) {
1217 return SDL_SetError("Pipewire: Failed to start stream loop");
1218 }
1219
1220 // Wait until all pre-open init flags are set or the stream has failed.
1221 PIPEWIRE_pw_thread_loop_lock(priv->loop);
1222 while (priv->stream_init_status != PW_READY_FLAG_ALL_PREOPEN_BITS &&
1223 PIPEWIRE_pw_stream_get_state(priv->stream, NULL) != PW_STREAM_STATE_ERROR) {
1224 PIPEWIRE_pw_thread_loop_wait(priv->loop);
1225 }
1226 priv->stream_init_status |= PW_READY_FLAG_OPEN_COMPLETE;
1227 PIPEWIRE_pw_thread_loop_unlock(priv->loop);
1228
1229 if (PIPEWIRE_pw_stream_get_state(priv->stream, &error) == PW_STREAM_STATE_ERROR) {
1230 return SDL_SetError("Pipewire: Stream error: %s", error);
1231 }
1232
1233 return true;
1234}
1235
1236static void PIPEWIRE_CloseDevice(SDL_AudioDevice *device)
1237{
1238 if (!device->hidden) {
1239 return;
1240 }
1241
1242 if (device->hidden->loop) {
1243 PIPEWIRE_pw_thread_loop_stop(device->hidden->loop);
1244 }
1245
1246 if (device->hidden->stream) {
1247 PIPEWIRE_pw_stream_destroy(device->hidden->stream);
1248 }
1249
1250 if (device->hidden->context) {
1251 PIPEWIRE_pw_context_destroy(device->hidden->context);
1252 }
1253
1254 if (device->hidden->loop) {
1255 PIPEWIRE_pw_thread_loop_destroy(device->hidden->loop);
1256 }
1257
1258 SDL_free(device->hidden);
1259 device->hidden = NULL;
1260
1261 SDL_AudioThreadFinalize(device);
1262}
1263
1264static void PIPEWIRE_DeinitializeStart(void)
1265{
1266 if (pipewire_initialized) {
1267 hotplug_loop_destroy();
1268 }
1269}
1270
1271static void PIPEWIRE_Deinitialize(void)
1272{
1273 if (pipewire_initialized) {
1274 hotplug_loop_destroy();
1275 deinit_pipewire_library();
1276 pipewire_initialized = false;
1277 }
1278}
1279
1280static bool PipewireInitialize(SDL_AudioDriverImpl *impl)
1281{
1282 if (!pipewire_initialized) {
1283 if (!init_pipewire_library()) {
1284 return false;
1285 }
1286
1287 pipewire_initialized = true;
1288
1289 if (!hotplug_loop_init()) {
1290 PIPEWIRE_Deinitialize();
1291 return false;
1292 }
1293 }
1294
1295 impl->DetectDevices = PIPEWIRE_DetectDevices;
1296 impl->OpenDevice = PIPEWIRE_OpenDevice;
1297 impl->DeinitializeStart = PIPEWIRE_DeinitializeStart;
1298 impl->Deinitialize = PIPEWIRE_Deinitialize;
1299 impl->PlayDevice = PIPEWIRE_PlayDevice;
1300 impl->GetDeviceBuf = PIPEWIRE_GetDeviceBuf;
1301 impl->RecordDevice = PIPEWIRE_RecordDevice;
1302 impl->FlushRecording = PIPEWIRE_FlushRecording;
1303 impl->CloseDevice = PIPEWIRE_CloseDevice;
1304
1305 impl->HasRecordingSupport = true;
1306 impl->ProvidesOwnCallbackThread = true;
1307
1308 return true;
1309}
1310
1311static bool PIPEWIRE_PREFERRED_Init(SDL_AudioDriverImpl *impl)
1312{
1313 if (!PipewireInitialize(impl)) {
1314 return false;
1315 }
1316
1317 // run device detection but don't add any devices to SDL; we're just waiting to see if PipeWire sees any devices. If not, fall back to the next backend.
1318 PIPEWIRE_pw_thread_loop_lock(hotplug_loop);
1319
1320 // Wait until the initial registry enumeration is complete
1321 if (!hotplug_init_complete) {
1322 PIPEWIRE_pw_thread_loop_wait(hotplug_loop);
1323 }
1324
1325 const bool no_devices = spa_list_is_empty(&hotplug_io_list);
1326
1327 PIPEWIRE_pw_thread_loop_unlock(hotplug_loop);
1328
1329 if (no_devices || !pipewire_core_version_at_least(1, 0, 0)) {
1330 PIPEWIRE_Deinitialize();
1331 return false;
1332 }
1333
1334 return true; // this will move on to PIPEWIRE_DetectDevices and reuse hotplug_io_list.
1335}
1336
1337static bool PIPEWIRE_Init(SDL_AudioDriverImpl *impl)
1338{
1339 return PipewireInitialize(impl);
1340}
1341
1342AudioBootStrap PIPEWIRE_PREFERRED_bootstrap = {
1343 "pipewire", "Pipewire", PIPEWIRE_PREFERRED_Init, false, true
1344};
1345AudioBootStrap PIPEWIRE_bootstrap = {
1346 "pipewire", "Pipewire", PIPEWIRE_Init, false, false
1347};
1348
1349#endif // SDL_AUDIO_DRIVER_PIPEWIRE
diff --git a/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h b/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h
new file mode 100644
index 0000000..e609e25
--- /dev/null
+++ b/contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h
@@ -0,0 +1,43 @@
1/*
2 Simple DirectMedia Layer
3 Copyright (C) 1997-2025 Sam Lantinga <slouken@libsdl.org>
4
5 This software is provided 'as-is', without any express or implied
6 warranty. In no event will the authors be held liable for any damages
7 arising from the use of this software.
8
9 Permission is granted to anyone to use this software for any purpose,
10 including commercial applications, and to alter it and redistribute it
11 freely, subject to the following restrictions:
12
13 1. The origin of this software must not be misrepresented; you must not
14 claim that you wrote the original software. If you use this software
15 in a product, an acknowledgment in the product documentation would be
16 appreciated but is not required.
17 2. Altered source versions must be plainly marked as such, and must not be
18 misrepresented as being the original software.
19 3. This notice may not be removed or altered from any source distribution.
20*/
21
22#include "SDL_internal.h"
23
24#ifndef SDL_pipewire_h_
25#define SDL_pipewire_h_
26
27#include "../SDL_sysaudio.h"
28#include <pipewire/pipewire.h>
29
30struct SDL_PrivateAudioData
31{
32 struct pw_thread_loop *loop;
33 struct pw_stream *stream;
34 struct pw_context *context;
35
36 Sint32 stride; // Bytes-per-frame
37 int stream_init_status;
38
39 // Set in GetDeviceBuf, filled in AudioThreadIterate, queued in PlayDevice
40 struct pw_buffer *pw_buf;
41};
42
43#endif // SDL_pipewire_h_