diff options
Diffstat (limited to 'contrib/SDL-3.2.8/src/audio/pipewire')
| -rw-r--r-- | contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.c | 1349 | ||||
| -rw-r--r-- | contrib/SDL-3.2.8/src/audio/pipewire/SDL_pipewire.h | 43 |
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 | |||
| 43 | enum 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 | |||
| 55 | static bool pipewire_initialized = false; | ||
| 56 | |||
| 57 | // Pipewire entry points | ||
| 58 | static const char *(*PIPEWIRE_pw_get_library_version)(void); | ||
| 59 | static void (*PIPEWIRE_pw_init)(int *, char ***); | ||
| 60 | static void (*PIPEWIRE_pw_deinit)(void); | ||
| 61 | static struct pw_main_loop *(*PIPEWIRE_pw_main_loop_new)(const struct spa_dict *loop); | ||
| 62 | static struct pw_loop *(*PIPEWIRE_pw_main_loop_get_loop)(struct pw_main_loop *loop); | ||
| 63 | static int (*PIPEWIRE_pw_main_loop_run)(struct pw_main_loop *loop); | ||
| 64 | static int (*PIPEWIRE_pw_main_loop_quit)(struct pw_main_loop *loop); | ||
| 65 | static void(*PIPEWIRE_pw_main_loop_destroy)(struct pw_main_loop *loop); | ||
| 66 | static struct pw_thread_loop *(*PIPEWIRE_pw_thread_loop_new)(const char *, const struct spa_dict *); | ||
| 67 | static void (*PIPEWIRE_pw_thread_loop_destroy)(struct pw_thread_loop *); | ||
| 68 | static void (*PIPEWIRE_pw_thread_loop_stop)(struct pw_thread_loop *); | ||
| 69 | static struct pw_loop *(*PIPEWIRE_pw_thread_loop_get_loop)(struct pw_thread_loop *); | ||
| 70 | static void (*PIPEWIRE_pw_thread_loop_lock)(struct pw_thread_loop *); | ||
| 71 | static void (*PIPEWIRE_pw_thread_loop_unlock)(struct pw_thread_loop *); | ||
| 72 | static void (*PIPEWIRE_pw_thread_loop_signal)(struct pw_thread_loop *, bool); | ||
| 73 | static void (*PIPEWIRE_pw_thread_loop_wait)(struct pw_thread_loop *); | ||
| 74 | static int (*PIPEWIRE_pw_thread_loop_start)(struct pw_thread_loop *); | ||
| 75 | static struct pw_context *(*PIPEWIRE_pw_context_new)(struct pw_loop *, struct pw_properties *, size_t); | ||
| 76 | static void (*PIPEWIRE_pw_context_destroy)(struct pw_context *); | ||
| 77 | static struct pw_core *(*PIPEWIRE_pw_context_connect)(struct pw_context *, struct pw_properties *, size_t); | ||
| 78 | static void (*PIPEWIRE_pw_proxy_add_object_listener)(struct pw_proxy *, struct spa_hook *, const void *, void *); | ||
| 79 | static void *(*PIPEWIRE_pw_proxy_get_user_data)(struct pw_proxy *); | ||
| 80 | static void (*PIPEWIRE_pw_proxy_destroy)(struct pw_proxy *); | ||
| 81 | static int (*PIPEWIRE_pw_core_disconnect)(struct pw_core *); | ||
| 82 | static struct pw_stream *(*PIPEWIRE_pw_stream_new_simple)(struct pw_loop *, const char *, struct pw_properties *, | ||
| 83 | const struct pw_stream_events *, void *); | ||
| 84 | static void (*PIPEWIRE_pw_stream_destroy)(struct pw_stream *); | ||
| 85 | static int (*PIPEWIRE_pw_stream_connect)(struct pw_stream *, enum pw_direction, uint32_t, enum pw_stream_flags, | ||
| 86 | const struct spa_pod **, uint32_t); | ||
| 87 | static enum pw_stream_state (*PIPEWIRE_pw_stream_get_state)(struct pw_stream *stream, const char **error); | ||
| 88 | static struct pw_buffer *(*PIPEWIRE_pw_stream_dequeue_buffer)(struct pw_stream *); | ||
| 89 | static int (*PIPEWIRE_pw_stream_queue_buffer)(struct pw_stream *, struct pw_buffer *); | ||
| 90 | static struct pw_properties *(*PIPEWIRE_pw_properties_new)(const char *, ...)SPA_SENTINEL; | ||
| 91 | static int (*PIPEWIRE_pw_properties_set)(struct pw_properties *, const char *, const char *); | ||
| 92 | static 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 | |||
| 96 | static const char *pipewire_library = SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC; | ||
| 97 | static SDL_SharedObject *pipewire_handle = NULL; | ||
| 98 | |||
| 99 | static 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 | |||
| 114 | static bool load_pipewire_library(void) | ||
| 115 | { | ||
| 116 | pipewire_handle = SDL_LoadObject(pipewire_library); | ||
| 117 | return pipewire_handle ? true : false; | ||
| 118 | } | ||
| 119 | |||
| 120 | static 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 | |||
| 132 | static bool load_pipewire_library(void) | ||
| 133 | { | ||
| 134 | return true; | ||
| 135 | } | ||
| 136 | |||
| 137 | static void unload_pipewire_library(void) | ||
| 138 | { | ||
| 139 | // Nothing to do | ||
| 140 | } | ||
| 141 | |||
| 142 | #endif // SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC | ||
| 143 | |||
| 144 | static 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 | |||
| 183 | static 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 | |||
| 195 | static 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. | ||
| 202 | struct 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. | ||
| 225 | struct 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. | ||
| 240 | static struct pw_thread_loop *hotplug_loop; | ||
| 241 | static struct pw_core *hotplug_core; | ||
| 242 | static struct pw_context *hotplug_context; | ||
| 243 | static struct pw_registry *hotplug_registry; | ||
| 244 | static struct spa_hook hotplug_registry_listener; | ||
| 245 | static struct spa_hook hotplug_core_listener; | ||
| 246 | static struct spa_list hotplug_pending_list; | ||
| 247 | static struct spa_list hotplug_io_list; | ||
| 248 | static int hotplug_init_seq_val; | ||
| 249 | static bool hotplug_init_complete; | ||
| 250 | static bool hotplug_events_enabled; | ||
| 251 | |||
| 252 | static int pipewire_version_major; | ||
| 253 | static int pipewire_version_minor; | ||
| 254 | static int pipewire_version_patch; | ||
| 255 | static char *pipewire_default_sink_id = NULL; | ||
| 256 | static char *pipewire_default_source_id = NULL; | ||
| 257 | |||
| 258 | static 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 | ||
| 266 | static 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 | |||
| 286 | dup_found: | ||
| 287 | |||
| 288 | return ret; | ||
| 289 | } | ||
| 290 | |||
| 291 | static 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 | |||
| 311 | static 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 | |||
| 321 | static 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 | |||
| 332 | static 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 | ||
| 344 | static 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 | |||
| 350 | static 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 | |||
| 361 | static 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 | |||
| 370 | static 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 | ||
| 399 | static 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 | |||
| 411 | static 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 | |||
| 420 | static 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 | |||
| 438 | static 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 | |||
| 447 | static 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 }; | ||
| 448 | static const struct pw_core_events interface_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_interface_callback }; | ||
| 449 | static const struct pw_core_events metadata_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_metadata_callback }; | ||
| 450 | |||
| 451 | static 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 | ||
| 467 | static 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 | |||
| 500 | static 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 | |||
| 518 | static 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 | ||
| 538 | static 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 | |||
| 560 | static 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 | |||
| 591 | static const struct pw_node_events interface_node_events = { PW_VERSION_NODE_EVENTS, .info = node_event_info, | ||
| 592 | .param = node_event_param }; | ||
| 593 | |||
| 594 | static 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 | |||
| 615 | static 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 | ||
| 629 | static 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 | |||
| 654 | static const struct pw_metadata_events metadata_node_events = { PW_VERSION_METADATA_EVENTS, .property = metadata_property }; | ||
| 655 | |||
| 656 | // Global registry callbacks | ||
| 657 | static 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 | |||
| 729 | static void registry_event_remove_callback(void *object, uint32_t id) | ||
| 730 | { | ||
| 731 | io_list_remove(id); | ||
| 732 | pending_list_remove(id); | ||
| 733 | } | ||
| 734 | |||
| 735 | static 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 | ||
| 739 | static 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, ®istry_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 | |||
| 782 | static 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 | |||
| 824 | static 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 | ||
| 854 | static const enum spa_audio_channel PIPEWIRE_channel_map_1[] = { SPA_AUDIO_CHANNEL_MONO }; | ||
| 855 | static const enum spa_audio_channel PIPEWIRE_channel_map_2[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR }; | ||
| 856 | static const enum spa_audio_channel PIPEWIRE_channel_map_3[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_LFE }; | ||
| 857 | static 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 }; | ||
| 859 | static 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 }; | ||
| 861 | static 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 }; | ||
| 863 | static 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 }; | ||
| 866 | static 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 | |||
| 872 | static 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 | |||
| 936 | static 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 | |||
| 957 | static 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 | |||
| 972 | static 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 | |||
| 991 | static 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 | |||
| 1000 | static 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 | |||
| 1027 | static 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 | |||
| 1041 | static 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 | |||
| 1060 | static 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 | |||
| 1073 | static 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 }; | ||
| 1077 | static 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 | |||
| 1082 | static 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 | ¶ms, 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 | |||
| 1236 | static 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 | |||
| 1264 | static void PIPEWIRE_DeinitializeStart(void) | ||
| 1265 | { | ||
| 1266 | if (pipewire_initialized) { | ||
| 1267 | hotplug_loop_destroy(); | ||
| 1268 | } | ||
| 1269 | } | ||
| 1270 | |||
| 1271 | static void PIPEWIRE_Deinitialize(void) | ||
| 1272 | { | ||
| 1273 | if (pipewire_initialized) { | ||
| 1274 | hotplug_loop_destroy(); | ||
| 1275 | deinit_pipewire_library(); | ||
| 1276 | pipewire_initialized = false; | ||
| 1277 | } | ||
| 1278 | } | ||
| 1279 | |||
| 1280 | static 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 | |||
| 1311 | static 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 | |||
| 1337 | static bool PIPEWIRE_Init(SDL_AudioDriverImpl *impl) | ||
| 1338 | { | ||
| 1339 | return PipewireInitialize(impl); | ||
| 1340 | } | ||
| 1341 | |||
| 1342 | AudioBootStrap PIPEWIRE_PREFERRED_bootstrap = { | ||
| 1343 | "pipewire", "Pipewire", PIPEWIRE_PREFERRED_Init, false, true | ||
| 1344 | }; | ||
| 1345 | AudioBootStrap 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 | |||
| 30 | struct 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_ | ||
