| /* |
| Simple DirectMedia Layer |
| Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org> |
| |
| This software is provided 'as-is', without any express or implied |
| warranty. In no event will the authors be held liable for any damages |
| arising from the use of this software. |
| |
| Permission is granted to anyone to use this software for any purpose, |
| including commercial applications, and to alter it and redistribute it |
| freely, subject to the following restrictions: |
| |
| 1. The origin of this software must not be misrepresented; you must not |
| claim that you wrote the original software. If you use this software |
| in a product, an acknowledgment in the product documentation would be |
| appreciated but is not required. |
| 2. Altered source versions must be plainly marked as such, and must not be |
| misrepresented as being the original software. |
| 3. This notice may not be removed or altered from any source distribution. |
| */ |
| |
| #include "../../SDL_internal.h" |
| #include "SDL_hints.h" |
| |
| #if SDL_AUDIO_DRIVER_PIPEWIRE |
| |
| #include "SDL_audio.h" |
| #include "SDL_loadso.h" |
| #include "SDL_pipewire.h" |
| |
| #include <pipewire/extensions/metadata.h> |
| #include <spa/param/audio/format-utils.h> |
| #include <spa/utils/json.h> |
| |
| /* |
| * The following keys are defined for compatability when building against older versions of Pipewire |
| * prior to their introduction and can be removed if the minimum required Pipewire build version is |
| * increased to or beyond their point of introduction. |
| */ |
| |
| /* |
| * Introduced in 0.3.22 |
| * Taken from /src/pipewire/keys.h |
| */ |
| #ifndef PW_KEY_CONFIG_NAME |
| #define PW_KEY_CONFIG_NAME "config.name" |
| #endif |
| |
| /* |
| * Introduced in 0.3.33 |
| * Taken from src/pipewire/keys.h |
| */ |
| #ifndef PW_KEY_NODE_RATE |
| #define PW_KEY_NODE_RATE "node.rate" |
| #endif |
| |
| /* |
| * Introduced in 0.3.44 |
| * Taken from src/pipewire/keys.h |
| */ |
| #ifndef PW_KEY_TARGET_OBJECT |
| #define PW_KEY_TARGET_OBJECT "target.object" |
| #endif |
| |
| /* |
| * This seems to be a sane lower limit as Pipewire |
| * uses it in several of it's own modules. |
| */ |
| #define PW_MIN_SAMPLES 32 /* About 0.67ms at 48kHz */ |
| #define PW_BASE_CLOCK_RATE 48000 |
| |
| #define PW_POD_BUFFER_LENGTH 1024 |
| #define PW_THREAD_NAME_BUFFER_LENGTH 128 |
| #define PW_MAX_IDENTIFIER_LENGTH 256 |
| |
| enum PW_READY_FLAGS |
| { |
| PW_READY_FLAG_BUFFER_ADDED = 0x1, |
| PW_READY_FLAG_STREAM_READY = 0x2, |
| PW_READY_FLAG_ALL_BITS = 0x3 |
| }; |
| |
| #define PW_ID_TO_HANDLE(x) (void *)((uintptr_t)x) |
| #define PW_HANDLE_TO_ID(x) (uint32_t)((uintptr_t)x) |
| |
| static SDL_bool pipewire_initialized = SDL_FALSE; |
| |
| /* Pipewire entry points */ |
| static const char *(*PIPEWIRE_pw_get_library_version)(void); |
| static void (*PIPEWIRE_pw_init)(int *, char ***); |
| static void (*PIPEWIRE_pw_deinit)(void); |
| static struct pw_thread_loop *(*PIPEWIRE_pw_thread_loop_new)(const char *, const struct spa_dict *); |
| static void (*PIPEWIRE_pw_thread_loop_destroy)(struct pw_thread_loop *); |
| static void (*PIPEWIRE_pw_thread_loop_stop)(struct pw_thread_loop *); |
| static struct pw_loop *(*PIPEWIRE_pw_thread_loop_get_loop)(struct pw_thread_loop *); |
| static void (*PIPEWIRE_pw_thread_loop_lock)(struct pw_thread_loop *); |
| static void (*PIPEWIRE_pw_thread_loop_unlock)(struct pw_thread_loop *); |
| static void (*PIPEWIRE_pw_thread_loop_signal)(struct pw_thread_loop *, bool); |
| static void (*PIPEWIRE_pw_thread_loop_wait)(struct pw_thread_loop *); |
| static int (*PIPEWIRE_pw_thread_loop_start)(struct pw_thread_loop *); |
| static struct pw_context *(*PIPEWIRE_pw_context_new)(struct pw_loop *, struct pw_properties *, size_t); |
| static void (*PIPEWIRE_pw_context_destroy)(struct pw_context *); |
| static struct pw_core *(*PIPEWIRE_pw_context_connect)(struct pw_context *, struct pw_properties *, size_t); |
| static void (*PIPEWIRE_pw_proxy_add_object_listener)(struct pw_proxy *, struct spa_hook *, const void *, void *); |
| static void *(*PIPEWIRE_pw_proxy_get_user_data)(struct pw_proxy *); |
| static void (*PIPEWIRE_pw_proxy_destroy)(struct pw_proxy *); |
| static int (*PIPEWIRE_pw_core_disconnect)(struct pw_core *); |
| static struct pw_stream *(*PIPEWIRE_pw_stream_new_simple)(struct pw_loop *, const char *, struct pw_properties *, |
| const struct pw_stream_events *, void *); |
| static void (*PIPEWIRE_pw_stream_destroy)(struct pw_stream *); |
| static int (*PIPEWIRE_pw_stream_connect)(struct pw_stream *, enum pw_direction, uint32_t, enum pw_stream_flags, |
| const struct spa_pod **, uint32_t); |
| static enum pw_stream_state (*PIPEWIRE_pw_stream_get_state)(struct pw_stream *stream, const char **error); |
| static struct pw_buffer *(*PIPEWIRE_pw_stream_dequeue_buffer)(struct pw_stream *); |
| static int (*PIPEWIRE_pw_stream_queue_buffer)(struct pw_stream *, struct pw_buffer *); |
| static struct pw_properties *(*PIPEWIRE_pw_properties_new)(const char *, ...)SPA_SENTINEL; |
| static int (*PIPEWIRE_pw_properties_set)(struct pw_properties *, const char *, const char *); |
| static int (*PIPEWIRE_pw_properties_setf)(struct pw_properties *, const char *, const char *, ...) SPA_PRINTF_FUNC(3, 4); |
| |
| static int pipewire_version_major; |
| static int pipewire_version_minor; |
| static int pipewire_version_patch; |
| |
| #ifdef SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC |
| |
| static const char *pipewire_library = SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC; |
| static void *pipewire_handle = NULL; |
| |
| static int pipewire_dlsym(const char *fn, void **addr) |
| { |
| *addr = SDL_LoadFunction(pipewire_handle, fn); |
| if (*addr == NULL) { |
| /* Don't call SDL_SetError(): SDL_LoadFunction already did. */ |
| return 0; |
| } |
| |
| return 1; |
| } |
| |
| #define SDL_PIPEWIRE_SYM(x) \ |
| if (!pipewire_dlsym(#x, (void **)(char *)&PIPEWIRE_##x)) { \ |
| return -1; \ |
| } |
| |
| static int load_pipewire_library() |
| { |
| pipewire_handle = SDL_LoadObject(pipewire_library); |
| return pipewire_handle != NULL ? 0 : -1; |
| } |
| |
| static void unload_pipewire_library() |
| { |
| if (pipewire_handle) { |
| SDL_UnloadObject(pipewire_handle); |
| pipewire_handle = NULL; |
| } |
| } |
| |
| #else |
| |
| #define SDL_PIPEWIRE_SYM(x) PIPEWIRE_##x = x |
| |
| static int load_pipewire_library() |
| { |
| return 0; |
| } |
| |
| static void unload_pipewire_library() |
| { /* Nothing to do */ |
| } |
| |
| #endif /* SDL_AUDIO_DRIVER_PIPEWIRE_DYNAMIC */ |
| |
| static int load_pipewire_syms() |
| { |
| SDL_PIPEWIRE_SYM(pw_get_library_version); |
| SDL_PIPEWIRE_SYM(pw_init); |
| SDL_PIPEWIRE_SYM(pw_deinit); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_new); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_destroy); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_stop); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_get_loop); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_lock); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_unlock); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_signal); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_wait); |
| SDL_PIPEWIRE_SYM(pw_thread_loop_start); |
| SDL_PIPEWIRE_SYM(pw_context_new); |
| SDL_PIPEWIRE_SYM(pw_context_destroy); |
| SDL_PIPEWIRE_SYM(pw_context_connect); |
| SDL_PIPEWIRE_SYM(pw_proxy_add_object_listener); |
| SDL_PIPEWIRE_SYM(pw_proxy_get_user_data); |
| SDL_PIPEWIRE_SYM(pw_proxy_destroy); |
| SDL_PIPEWIRE_SYM(pw_core_disconnect); |
| SDL_PIPEWIRE_SYM(pw_stream_new_simple); |
| SDL_PIPEWIRE_SYM(pw_stream_destroy); |
| SDL_PIPEWIRE_SYM(pw_stream_connect); |
| SDL_PIPEWIRE_SYM(pw_stream_get_state); |
| SDL_PIPEWIRE_SYM(pw_stream_dequeue_buffer); |
| SDL_PIPEWIRE_SYM(pw_stream_queue_buffer); |
| SDL_PIPEWIRE_SYM(pw_properties_new); |
| SDL_PIPEWIRE_SYM(pw_properties_set); |
| SDL_PIPEWIRE_SYM(pw_properties_setf); |
| |
| return 0; |
| } |
| |
| SDL_FORCE_INLINE SDL_bool pipewire_version_at_least(int major, int minor, int patch) |
| { |
| return (pipewire_version_major >= major) && |
| (pipewire_version_major > major || pipewire_version_minor >= minor) && |
| (pipewire_version_major > major || pipewire_version_minor > minor || pipewire_version_patch >= patch); |
| } |
| |
| static int init_pipewire_library() |
| { |
| if (!load_pipewire_library()) { |
| if (!load_pipewire_syms()) { |
| int nargs; |
| const char *version = PIPEWIRE_pw_get_library_version(); |
| nargs = SDL_sscanf(version, "%d.%d.%d", &pipewire_version_major, &pipewire_version_minor, &pipewire_version_patch); |
| if (nargs < 3) { |
| return -1; |
| } |
| |
| /* SDL can build against 0.3.20, but requires 0.3.24 */ |
| if (pipewire_version_at_least(0, 3, 24)) { |
| PIPEWIRE_pw_init(NULL, NULL); |
| return 0; |
| } |
| } |
| } |
| |
| return -1; |
| } |
| |
| static void deinit_pipewire_library() |
| { |
| PIPEWIRE_pw_deinit(); |
| unload_pipewire_library(); |
| } |
| |
| /* A generic Pipewire node object used for enumeration. */ |
| struct node_object |
| { |
| struct spa_list link; |
| |
| Uint32 id; |
| int seq; |
| SDL_bool persist; |
| |
| /* |
| * NOTE: If used, this is *must* be allocated with SDL_malloc() or similar |
| * as SDL_free() will be called on it when the node_object is destroyed. |
| * |
| * If ownership of the referenced memory is transferred, this must be set |
| * to NULL or the memory will be freed when the node_object is destroyed. |
| */ |
| void *userdata; |
| |
| struct pw_proxy *proxy; |
| struct spa_hook node_listener; |
| struct spa_hook core_listener; |
| }; |
| |
| /* A sink/source node used for stream I/O. */ |
| struct io_node |
| { |
| struct spa_list link; |
| |
| Uint32 id; |
| SDL_bool is_capture; |
| SDL_AudioSpec spec; |
| |
| const char *name; /* Friendly name */ |
| const char *path; /* OS identifier (i.e. ALSA endpoint) */ |
| |
| char buf[]; /* Buffer to hold the name and path strings. */ |
| }; |
| |
| /* The global hotplug thread and associated objects. */ |
| static struct pw_thread_loop *hotplug_loop; |
| static struct pw_core *hotplug_core; |
| static struct pw_context *hotplug_context; |
| static struct pw_registry *hotplug_registry; |
| static struct spa_hook hotplug_registry_listener; |
| static struct spa_hook hotplug_core_listener; |
| static struct spa_list hotplug_pending_list; |
| static struct spa_list hotplug_io_list; |
| static int hotplug_init_seq_val; |
| static SDL_bool hotplug_init_complete; |
| static SDL_bool hotplug_events_enabled; |
| |
| static char *pipewire_default_sink_id = NULL; |
| static char *pipewire_default_source_id = NULL; |
| |
| /* The active node list */ |
| static SDL_bool io_list_check_add(struct io_node *node) |
| { |
| struct io_node *n; |
| SDL_bool ret = SDL_TRUE; |
| |
| /* See if the node is already in the list */ |
| spa_list_for_each (n, &hotplug_io_list, link) { |
| if (n->id == node->id) { |
| ret = SDL_FALSE; |
| goto dup_found; |
| } |
| } |
| |
| /* Add to the list if the node doesn't already exist */ |
| spa_list_append(&hotplug_io_list, &node->link); |
| |
| if (hotplug_events_enabled) { |
| SDL_AddAudioDevice(node->is_capture, node->name, &node->spec, PW_ID_TO_HANDLE(node->id)); |
| } |
| |
| dup_found: |
| |
| return ret; |
| } |
| |
| static void io_list_remove(Uint32 id) |
| { |
| struct io_node *n, *temp; |
| |
| /* Find and remove the node from the list */ |
| spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { |
| if (n->id == id) { |
| spa_list_remove(&n->link); |
| |
| if (hotplug_events_enabled) { |
| SDL_RemoveAudioDevice(n->is_capture, PW_ID_TO_HANDLE(id)); |
| } |
| |
| SDL_free(n); |
| |
| break; |
| } |
| } |
| } |
| |
| static void io_list_sort() |
| { |
| struct io_node *default_sink = NULL, *default_source = NULL; |
| struct io_node *n, *temp; |
| |
| /* Find and move the default nodes to the beginning of the list */ |
| spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { |
| if (pipewire_default_sink_id != NULL && SDL_strcmp(n->path, pipewire_default_sink_id) == 0) { |
| default_sink = n; |
| spa_list_remove(&n->link); |
| } else if (pipewire_default_source_id != NULL && SDL_strcmp(n->path, pipewire_default_source_id) == 0) { |
| default_source = n; |
| spa_list_remove(&n->link); |
| } |
| } |
| |
| if (default_source) { |
| spa_list_prepend(&hotplug_io_list, &default_source->link); |
| } |
| |
| if (default_sink) { |
| spa_list_prepend(&hotplug_io_list, &default_sink->link); |
| } |
| } |
| |
| static void io_list_clear() |
| { |
| struct io_node *n, *temp; |
| |
| spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { |
| spa_list_remove(&n->link); |
| SDL_free(n); |
| } |
| } |
| |
| static struct io_node *io_list_get_by_id(Uint32 id) |
| { |
| struct io_node *n, *temp; |
| spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { |
| if (n->id == id) { |
| return n; |
| } |
| } |
| return NULL; |
| } |
| |
| static struct io_node *io_list_get_by_path(char *path) |
| { |
| struct io_node *n, *temp; |
| spa_list_for_each_safe (n, temp, &hotplug_io_list, link) { |
| if (SDL_strcmp(n->path, path) == 0) { |
| return n; |
| } |
| } |
| return NULL; |
| } |
| |
| static void node_object_destroy(struct node_object *node) |
| { |
| SDL_assert(node); |
| |
| spa_list_remove(&node->link); |
| spa_hook_remove(&node->node_listener); |
| spa_hook_remove(&node->core_listener); |
| SDL_free(node->userdata); |
| PIPEWIRE_pw_proxy_destroy(node->proxy); |
| } |
| |
| /* The pending node list */ |
| static void pending_list_add(struct node_object *node) |
| { |
| SDL_assert(node); |
| spa_list_append(&hotplug_pending_list, &node->link); |
| } |
| |
| static void pending_list_remove(Uint32 id) |
| { |
| struct node_object *node, *temp; |
| |
| spa_list_for_each_safe (node, temp, &hotplug_pending_list, link) { |
| if (node->id == id) { |
| node_object_destroy(node); |
| } |
| } |
| } |
| |
| static void pending_list_clear() |
| { |
| struct node_object *node, *temp; |
| |
| spa_list_for_each_safe (node, temp, &hotplug_pending_list, link) { |
| node_object_destroy(node); |
| } |
| } |
| |
| static void *node_object_new(Uint32 id, const char *type, Uint32 version, const void *funcs, const struct pw_core_events *core_events) |
| { |
| struct pw_proxy *proxy; |
| struct node_object *node; |
| |
| /* Create the proxy object */ |
| proxy = pw_registry_bind(hotplug_registry, id, type, version, sizeof(struct node_object)); |
| if (proxy == NULL) { |
| SDL_SetError("Pipewire: Failed to create proxy object (%i)", errno); |
| return NULL; |
| } |
| |
| node = PIPEWIRE_pw_proxy_get_user_data(proxy); |
| SDL_zerop(node); |
| |
| node->id = id; |
| node->proxy = proxy; |
| |
| /* Add the callbacks */ |
| pw_core_add_listener(hotplug_core, &node->core_listener, core_events, node); |
| PIPEWIRE_pw_proxy_add_object_listener(node->proxy, &node->node_listener, funcs, node); |
| |
| /* Add the node to the active list */ |
| pending_list_add(node); |
| |
| return node; |
| } |
| |
| /* Core sync points */ |
| static void core_events_hotplug_init_callback(void *object, uint32_t id, int seq) |
| { |
| if (id == PW_ID_CORE && seq == hotplug_init_seq_val) { |
| /* This core listener is no longer needed. */ |
| spa_hook_remove(&hotplug_core_listener); |
| |
| /* Signal that the initial I/O list is populated */ |
| hotplug_init_complete = SDL_TRUE; |
| PIPEWIRE_pw_thread_loop_signal(hotplug_loop, false); |
| } |
| } |
| |
| static void core_events_interface_callback(void *object, uint32_t id, int seq) |
| { |
| struct node_object *node = object; |
| struct io_node *io = node->userdata; |
| |
| if (id == PW_ID_CORE && seq == node->seq) { |
| /* |
| * Move the I/O node to the connected list. |
| * On success, the list owns the I/O node object. |
| */ |
| if (io_list_check_add(io)) { |
| node->userdata = NULL; |
| } |
| |
| node_object_destroy(node); |
| } |
| } |
| |
| static void core_events_metadata_callback(void *object, uint32_t id, int seq) |
| { |
| struct node_object *node = object; |
| |
| if (id == PW_ID_CORE && seq == node->seq && !node->persist) { |
| node_object_destroy(node); |
| } |
| } |
| |
| static const struct pw_core_events hotplug_init_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_hotplug_init_callback }; |
| static const struct pw_core_events interface_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_interface_callback }; |
| static const struct pw_core_events metadata_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_metadata_callback }; |
| |
| static void hotplug_core_sync(struct node_object *node) |
| { |
| /* |
| * Node sync events *must* come before the hotplug init sync events or the initial |
| * I/O list will be incomplete when the main hotplug sync point is hit. |
| */ |
| if (node) { |
| node->seq = pw_core_sync(hotplug_core, PW_ID_CORE, node->seq); |
| } |
| |
| if (!hotplug_init_complete) { |
| hotplug_init_seq_val = pw_core_sync(hotplug_core, PW_ID_CORE, hotplug_init_seq_val); |
| } |
| } |
| |
| /* Helpers for retrieving values from params */ |
| static SDL_bool get_range_param(const struct spa_pod *param, Uint32 key, int *def, int *min, int *max) |
| { |
| const struct spa_pod_prop *prop; |
| struct spa_pod *value; |
| Uint32 n_values, choice; |
| |
| prop = spa_pod_find_prop(param, NULL, key); |
| |
| if (prop && prop->value.type == SPA_TYPE_Choice) { |
| value = spa_pod_get_values(&prop->value, &n_values, &choice); |
| |
| if (n_values == 3 && choice == SPA_CHOICE_Range) { |
| Uint32 *v = SPA_POD_BODY(value); |
| |
| if (v) { |
| if (def) { |
| *def = (int)v[0]; |
| } |
| if (min) { |
| *min = (int)v[1]; |
| } |
| if (max) { |
| *max = (int)v[2]; |
| } |
| |
| return SDL_TRUE; |
| } |
| } |
| } |
| |
| return SDL_FALSE; |
| } |
| |
| static SDL_bool get_int_param(const struct spa_pod *param, Uint32 key, int *val) |
| { |
| const struct spa_pod_prop *prop; |
| Sint32 v; |
| |
| prop = spa_pod_find_prop(param, NULL, key); |
| |
| if (prop && spa_pod_get_int(&prop->value, &v) == 0) { |
| if (val) { |
| *val = (int)v; |
| } |
| |
| return SDL_TRUE; |
| } |
| |
| return SDL_FALSE; |
| } |
| |
| /* Interface node callbacks */ |
| static void node_event_info(void *object, const struct pw_node_info *info) |
| { |
| struct node_object *node = object; |
| struct io_node *io = node->userdata; |
| const char *prop_val; |
| Uint32 i; |
| |
| if (info) { |
| prop_val = spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS); |
| if (prop_val) { |
| io->spec.channels = (Uint8)SDL_atoi(prop_val); |
| } |
| |
| /* Need to parse the parameters to get the sample rate */ |
| for (i = 0; i < info->n_params; ++i) { |
| pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL); |
| } |
| |
| hotplug_core_sync(node); |
| } |
| } |
| |
| static void node_event_param(void *object, int seq, uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param) |
| { |
| struct node_object *node = object; |
| struct io_node *io = node->userdata; |
| |
| /* Get the default frequency */ |
| if (io->spec.freq == 0) { |
| get_range_param(param, SPA_FORMAT_AUDIO_rate, &io->spec.freq, NULL, NULL); |
| } |
| |
| /* |
| * The channel count should have come from the node properties, |
| * but it is stored here as well. If one failed, try the other. |
| */ |
| if (io->spec.channels == 0) { |
| int channels; |
| if (get_int_param(param, SPA_FORMAT_AUDIO_channels, &channels)) { |
| io->spec.channels = (Uint8)channels; |
| } |
| } |
| } |
| |
| static const struct pw_node_events interface_node_events = { PW_VERSION_NODE_EVENTS, .info = node_event_info, |
| .param = node_event_param }; |
| |
| static char *get_name_from_json(const char *json) |
| { |
| struct spa_json parser[2]; |
| char key[7]; /* "name" */ |
| char value[PW_MAX_IDENTIFIER_LENGTH]; |
| spa_json_init(&parser[0], json, SDL_strlen(json)); |
| if (spa_json_enter_object(&parser[0], &parser[1]) <= 0) { |
| /* Not actually JSON */ |
| return NULL; |
| } |
| if (spa_json_get_string(&parser[1], key, sizeof(key)) <= 0) { |
| /* Not actually a key/value pair */ |
| return NULL; |
| } |
| if (spa_json_get_string(&parser[1], value, sizeof(value)) <= 0) { |
| /* Somehow had a key with no value? */ |
| return NULL; |
| } |
| return SDL_strdup(value); |
| } |
| |
| /* Metadata node callback */ |
| static int metadata_property(void *object, Uint32 subject, const char *key, const char *type, const char *value) |
| { |
| struct node_object *node = object; |
| |
| if (subject == PW_ID_CORE && key != NULL && value != NULL) { |
| if (!SDL_strcmp(key, "default.audio.sink")) { |
| if (pipewire_default_sink_id != NULL) { |
| SDL_free(pipewire_default_sink_id); |
| } |
| pipewire_default_sink_id = get_name_from_json(value); |
| node->persist = SDL_TRUE; |
| } else if (!SDL_strcmp(key, "default.audio.source")) { |
| if (pipewire_default_source_id != NULL) { |
| SDL_free(pipewire_default_source_id); |
| } |
| pipewire_default_source_id = get_name_from_json(value); |
| node->persist = SDL_TRUE; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static const struct pw_metadata_events metadata_node_events = { PW_VERSION_METADATA_EVENTS, .property = metadata_property }; |
| |
| /* Global registry callbacks */ |
| static void registry_event_global_callback(void *object, uint32_t id, uint32_t permissions, const char *type, uint32_t version, |
| const struct spa_dict *props) |
| { |
| struct node_object *node; |
| |
| /* We're only interested in interface and metadata nodes. */ |
| if (!SDL_strcmp(type, PW_TYPE_INTERFACE_Node)) { |
| const char *media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); |
| |
| if (media_class) { |
| const char *node_desc; |
| const char *node_path; |
| struct io_node *io; |
| SDL_bool is_capture; |
| int desc_buffer_len; |
| int path_buffer_len; |
| |
| /* Just want sink and capture */ |
| if (!SDL_strcasecmp(media_class, "Audio/Sink")) { |
| is_capture = SDL_FALSE; |
| } else if (!SDL_strcasecmp(media_class, "Audio/Source")) { |
| is_capture = SDL_TRUE; |
| } else { |
| return; |
| } |
| |
| node_desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); |
| node_path = spa_dict_lookup(props, PW_KEY_NODE_NAME); |
| |
| if (node_desc && node_path) { |
| node = node_object_new(id, type, version, &interface_node_events, &interface_core_events); |
| if (node == NULL) { |
| SDL_SetError("Pipewire: Failed to allocate interface node"); |
| return; |
| } |
| |
| /* Allocate and initialize the I/O node information struct */ |
| desc_buffer_len = SDL_strlen(node_desc) + 1; |
| path_buffer_len = SDL_strlen(node_path) + 1; |
| node->userdata = io = SDL_calloc(1, sizeof(struct io_node) + desc_buffer_len + path_buffer_len); |
| if (io == NULL) { |
| node_object_destroy(node); |
| SDL_OutOfMemory(); |
| return; |
| } |
| |
| /* Begin setting the node properties */ |
| io->id = id; |
| io->is_capture = is_capture; |
| io->spec.format = AUDIO_F32; /* Pipewire uses floats internally, other formats require conversion. */ |
| io->name = io->buf; |
| io->path = io->buf + desc_buffer_len; |
| SDL_strlcpy(io->buf, node_desc, desc_buffer_len); |
| SDL_strlcpy(io->buf + desc_buffer_len, node_path, path_buffer_len); |
| |
| /* Update sync points */ |
| hotplug_core_sync(node); |
| } |
| } |
| } else if (!SDL_strcmp(type, PW_TYPE_INTERFACE_Metadata)) { |
| node = node_object_new(id, type, version, &metadata_node_events, &metadata_core_events); |
| if (node == NULL) { |
| SDL_SetError("Pipewire: Failed to allocate metadata node"); |
| return; |
| } |
| |
| /* Update sync points */ |
| hotplug_core_sync(node); |
| } |
| } |
| |
| static void registry_event_remove_callback(void *object, uint32_t id) |
| { |
| io_list_remove(id); |
| pending_list_remove(id); |
| } |
| |
| static const struct pw_registry_events registry_events = { PW_VERSION_REGISTRY_EVENTS, .global = registry_event_global_callback, |
| .global_remove = registry_event_remove_callback }; |
| |
| /* The hotplug thread */ |
| static int hotplug_loop_init() |
| { |
| int res; |
| |
| spa_list_init(&hotplug_pending_list); |
| spa_list_init(&hotplug_io_list); |
| |
| hotplug_loop = PIPEWIRE_pw_thread_loop_new("SDLAudioHotplug", NULL); |
| if (hotplug_loop == NULL) { |
| return SDL_SetError("Pipewire: Failed to create hotplug detection loop (%i)", errno); |
| } |
| |
| hotplug_context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_thread_loop_get_loop(hotplug_loop), NULL, 0); |
| if (hotplug_context == NULL) { |
| return SDL_SetError("Pipewire: Failed to create hotplug detection context (%i)", errno); |
| } |
| |
| hotplug_core = PIPEWIRE_pw_context_connect(hotplug_context, NULL, 0); |
| if (hotplug_core == NULL) { |
| return SDL_SetError("Pipewire: Failed to connect hotplug detection context (%i)", errno); |
| } |
| |
| hotplug_registry = pw_core_get_registry(hotplug_core, PW_VERSION_REGISTRY, 0); |
| if (hotplug_registry == NULL) { |
| return SDL_SetError("Pipewire: Failed to acquire hotplug detection registry (%i)", errno); |
| } |
| |
| spa_zero(hotplug_registry_listener); |
| pw_registry_add_listener(hotplug_registry, &hotplug_registry_listener, ®istry_events, NULL); |
| |
| spa_zero(hotplug_core_listener); |
| pw_core_add_listener(hotplug_core, &hotplug_core_listener, &hotplug_init_core_events, NULL); |
| |
| hotplug_init_seq_val = pw_core_sync(hotplug_core, PW_ID_CORE, 0); |
| |
| res = PIPEWIRE_pw_thread_loop_start(hotplug_loop); |
| if (res != 0) { |
| return SDL_SetError("Pipewire: Failed to start hotplug detection loop"); |
| } |
| |
| return 0; |
| } |
| |
| static void hotplug_loop_destroy() |
| { |
| if (hotplug_loop) { |
| PIPEWIRE_pw_thread_loop_stop(hotplug_loop); |
| } |
| |
| pending_list_clear(); |
| io_list_clear(); |
| |
| hotplug_init_complete = SDL_FALSE; |
| hotplug_events_enabled = SDL_FALSE; |
| |
| if (pipewire_default_sink_id != NULL) { |
| SDL_free(pipewire_default_sink_id); |
| pipewire_default_sink_id = NULL; |
| } |
| if (pipewire_default_source_id != NULL) { |
| SDL_free(pipewire_default_source_id); |
| pipewire_default_source_id = NULL; |
| } |
| |
| if (hotplug_registry) { |
| PIPEWIRE_pw_proxy_destroy((struct pw_proxy *)hotplug_registry); |
| hotplug_registry = NULL; |
| } |
| |
| if (hotplug_core) { |
| PIPEWIRE_pw_core_disconnect(hotplug_core); |
| hotplug_core = NULL; |
| } |
| |
| if (hotplug_context) { |
| PIPEWIRE_pw_context_destroy(hotplug_context); |
| hotplug_context = NULL; |
| } |
| |
| if (hotplug_loop) { |
| PIPEWIRE_pw_thread_loop_destroy(hotplug_loop); |
| hotplug_loop = NULL; |
| } |
| } |
| |
| static void PIPEWIRE_DetectDevices() |
| { |
| struct io_node *io; |
| |
| PIPEWIRE_pw_thread_loop_lock(hotplug_loop); |
| |
| /* Wait until the initial registry enumeration is complete */ |
| if (!hotplug_init_complete) { |
| PIPEWIRE_pw_thread_loop_wait(hotplug_loop); |
| } |
| |
| /* Sort the I/O list so the default source/sink are listed first */ |
| io_list_sort(); |
| |
| spa_list_for_each (io, &hotplug_io_list, link) { |
| SDL_AddAudioDevice(io->is_capture, io->name, &io->spec, PW_ID_TO_HANDLE(io->id)); |
| } |
| |
| hotplug_events_enabled = SDL_TRUE; |
| |
| PIPEWIRE_pw_thread_loop_unlock(hotplug_loop); |
| } |
| |
| /* Channel maps that match the order in SDL_Audio.h */ |
| static const enum spa_audio_channel PIPEWIRE_channel_map_1[] = { SPA_AUDIO_CHANNEL_MONO }; |
| static const enum spa_audio_channel PIPEWIRE_channel_map_2[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR }; |
| static const enum spa_audio_channel PIPEWIRE_channel_map_3[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_LFE }; |
| static const enum spa_audio_channel PIPEWIRE_channel_map_4[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_RL, |
| SPA_AUDIO_CHANNEL_RR }; |
| static const enum spa_audio_channel PIPEWIRE_channel_map_5[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, |
| SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR }; |
| static const enum spa_audio_channel PIPEWIRE_channel_map_6[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, |
| SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR }; |
| static const enum spa_audio_channel PIPEWIRE_channel_map_7[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, |
| SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RC, SPA_AUDIO_CHANNEL_RL, |
| SPA_AUDIO_CHANNEL_RR }; |
| static const enum spa_audio_channel PIPEWIRE_channel_map_8[] = { SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, SPA_AUDIO_CHANNEL_FC, |
| SPA_AUDIO_CHANNEL_LFE, SPA_AUDIO_CHANNEL_RL, SPA_AUDIO_CHANNEL_RR, |
| SPA_AUDIO_CHANNEL_SL, SPA_AUDIO_CHANNEL_SR }; |
| |
| #define COPY_CHANNEL_MAP(c) SDL_memcpy(info->position, PIPEWIRE_channel_map_##c, sizeof(PIPEWIRE_channel_map_##c)) |
| |
| static void initialize_spa_info(const SDL_AudioSpec *spec, struct spa_audio_info_raw *info) |
| { |
| info->channels = spec->channels; |
| info->rate = spec->freq; |
| |
| switch (spec->channels) { |
| case 1: |
| COPY_CHANNEL_MAP(1); |
| break; |
| case 2: |
| COPY_CHANNEL_MAP(2); |
| break; |
| case 3: |
| COPY_CHANNEL_MAP(3); |
| break; |
| case 4: |
| COPY_CHANNEL_MAP(4); |
| break; |
| case 5: |
| COPY_CHANNEL_MAP(5); |
| break; |
| case 6: |
| COPY_CHANNEL_MAP(6); |
| break; |
| case 7: |
| COPY_CHANNEL_MAP(7); |
| break; |
| case 8: |
| COPY_CHANNEL_MAP(8); |
| break; |
| } |
| |
| /* Pipewire natively supports all of SDL's sample formats */ |
| switch (spec->format) { |
| case AUDIO_U8: |
| info->format = SPA_AUDIO_FORMAT_U8; |
| break; |
| case AUDIO_S8: |
| info->format = SPA_AUDIO_FORMAT_S8; |
| break; |
| case AUDIO_U16LSB: |
| info->format = SPA_AUDIO_FORMAT_U16_LE; |
| break; |
| case AUDIO_S16LSB: |
| info->format = SPA_AUDIO_FORMAT_S16_LE; |
| break; |
| case AUDIO_U16MSB: |
| info->format = SPA_AUDIO_FORMAT_U16_BE; |
| break; |
| case AUDIO_S16MSB: |
| info->format = SPA_AUDIO_FORMAT_S16_BE; |
| break; |
| case AUDIO_S32LSB: |
| info->format = SPA_AUDIO_FORMAT_S32_LE; |
| break; |
| case AUDIO_S32MSB: |
| info->format = SPA_AUDIO_FORMAT_S32_BE; |
| break; |
| case AUDIO_F32LSB: |
| info->format = SPA_AUDIO_FORMAT_F32_LE; |
| break; |
| case AUDIO_F32MSB: |
| info->format = SPA_AUDIO_FORMAT_F32_BE; |
| break; |
| } |
| } |
| |
| static void output_callback(void *data) |
| { |
| struct pw_buffer *pw_buf; |
| struct spa_buffer *spa_buf; |
| Uint8 *dst; |
| |
| _THIS = (SDL_AudioDevice *)data; |
| struct pw_stream *stream = this->hidden->stream; |
| |
| /* Shutting down, don't do anything */ |
| if (SDL_AtomicGet(&this->shutdown)) { |
| return; |
| } |
| |
| /* See if a buffer is available */ |
| pw_buf = PIPEWIRE_pw_stream_dequeue_buffer(stream); |
| if (pw_buf == NULL) { |
| return; |
| } |
| |
| spa_buf = pw_buf->buffer; |
| |
| if (spa_buf->datas[0].data == NULL) { |
| return; |
| } |
| |
| /* |
| * If the device is disabled, write silence to the stream buffer |
| * and run the callback with the work buffer to keep the callback |
| * firing regularly in case the audio is being used as a timer. |
| */ |
| SDL_LockMutex(this->mixer_lock); |
| if (!SDL_AtomicGet(&this->paused)) { |
| if (SDL_AtomicGet(&this->enabled)) { |
| dst = spa_buf->datas[0].data; |
| } else { |
| dst = this->work_buffer; |
| SDL_memset(spa_buf->datas[0].data, this->spec.silence, this->spec.size); |
| } |
| |
| if (!this->stream) { |
| this->callbackspec.callback(this->callbackspec.userdata, dst, this->callbackspec.size); |
| } else { |
| int got; |
| |
| /* Fire the callback until we have enough to fill a buffer */ |
| while (SDL_AudioStreamAvailable(this->stream) < this->spec.size) { |
| this->callbackspec.callback(this->callbackspec.userdata, this->work_buffer, this->callbackspec.size); |
| SDL_AudioStreamPut(this->stream, this->work_buffer, this->callbackspec.size); |
| } |
| |
| got = SDL_AudioStreamGet(this->stream, dst, this->spec.size); |
| SDL_assert(got == this->spec.size); |
| } |
| } else { |
| SDL_memset(spa_buf->datas[0].data, this->spec.silence, this->spec.size); |
| } |
| SDL_UnlockMutex(this->mixer_lock); |
| |
| spa_buf->datas[0].chunk->offset = 0; |
| spa_buf->datas[0].chunk->stride = this->hidden->stride; |
| spa_buf->datas[0].chunk->size = this->spec.size; |
| |
| PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf); |
| } |
| |
| static void input_callback(void *data) |
| { |
| struct pw_buffer *pw_buf; |
| struct spa_buffer *spa_buf; |
| Uint8 *src; |
| _THIS = (SDL_AudioDevice *)data; |
| struct pw_stream *stream = this->hidden->stream; |
| |
| /* Shutting down, don't do anything */ |
| if (SDL_AtomicGet(&this->shutdown)) { |
| return; |
| } |
| |
| pw_buf = PIPEWIRE_pw_stream_dequeue_buffer(stream); |
| if (pw_buf == NULL) { |
| return; |
| } |
| |
| spa_buf = pw_buf->buffer; |
| (src = (Uint8 *)spa_buf->datas[0].data); |
| if (src == NULL) { |
| return; |
| } |
| |
| if (!SDL_AtomicGet(&this->paused)) { |
| /* Calculate the offset and data size */ |
| const Uint32 offset = SPA_MIN(spa_buf->datas[0].chunk->offset, spa_buf->datas[0].maxsize); |
| const Uint32 size = SPA_MIN(spa_buf->datas[0].chunk->size, spa_buf->datas[0].maxsize - offset); |
| |
| src += offset; |
| |
| /* Fill the buffer with silence if the stream is disabled. */ |
| if (!SDL_AtomicGet(&this->enabled)) { |
| SDL_memset(src, this->callbackspec.silence, size); |
| } |
| |
| /* Pipewire can vary the latency, so buffer all incoming data */ |
| SDL_WriteToDataQueue(this->hidden->buffer, src, size); |
| |
| while (SDL_CountDataQueue(this->hidden->buffer) >= this->callbackspec.size) { |
| SDL_ReadFromDataQueue(this->hidden->buffer, this->work_buffer, this->callbackspec.size); |
| |
| SDL_LockMutex(this->mixer_lock); |
| this->callbackspec.callback(this->callbackspec.userdata, this->work_buffer, this->callbackspec.size); |
| SDL_UnlockMutex(this->mixer_lock); |
| } |
| } else if (this->hidden->buffer) { /* Flush the buffer when paused */ |
| if (SDL_CountDataQueue(this->hidden->buffer) != 0) { |
| SDL_ClearDataQueue(this->hidden->buffer, this->hidden->input_buffer_packet_size); |
| } |
| } |
| |
| PIPEWIRE_pw_stream_queue_buffer(stream, pw_buf); |
| } |
| |
| static void stream_add_buffer_callback(void *data, struct pw_buffer *buffer) |
| { |
| _THIS = data; |
| |
| if (this->iscapture == SDL_FALSE) { |
| /* |
| * Clamp the output spec samples and size to the max size of the Pipewire buffer. |
| * If they exceed the maximum size of the Pipewire buffer, double buffering will be used. |
| */ |
| if (this->spec.size > buffer->buffer->datas[0].maxsize) { |
| this->spec.samples = buffer->buffer->datas[0].maxsize / this->hidden->stride; |
| this->spec.size = buffer->buffer->datas[0].maxsize; |
| } |
| } else if (this->hidden->buffer == NULL) { |
| /* |
| * The latency of source nodes can change, so buffering is always required. |
| * |
| * Ensure that the intermediate input buffer is large enough to hold the requested |
| * application packet size or a full buffer of data from Pipewire, whichever is larger. |
| * |
| * A packet size of 2 periods should be more than is ever needed. |
| */ |
| this->hidden->input_buffer_packet_size = SPA_MAX(this->spec.size, buffer->buffer->datas[0].maxsize) * 2; |
| this->hidden->buffer = SDL_NewDataQueue(this->hidden->input_buffer_packet_size, this->hidden->input_buffer_packet_size); |
| } |
| |
| this->hidden->stream_init_status |= PW_READY_FLAG_BUFFER_ADDED; |
| PIPEWIRE_pw_thread_loop_signal(this->hidden->loop, false); |
| } |
| |
| static void stream_state_changed_callback(void *data, enum pw_stream_state old, enum pw_stream_state state, const char *error) |
| { |
| _THIS = data; |
| |
| if (state == PW_STREAM_STATE_STREAMING) { |
| this->hidden->stream_init_status |= PW_READY_FLAG_STREAM_READY; |
| } |
| |
| if (state == PW_STREAM_STATE_STREAMING || state == PW_STREAM_STATE_ERROR) { |
| PIPEWIRE_pw_thread_loop_signal(this->hidden->loop, false); |
| } |
| } |
| |
| static const struct pw_stream_events stream_output_events = { PW_VERSION_STREAM_EVENTS, |
| .state_changed = stream_state_changed_callback, |
| .add_buffer = stream_add_buffer_callback, |
| .process = output_callback }; |
| static const struct pw_stream_events stream_input_events = { PW_VERSION_STREAM_EVENTS, |
| .state_changed = stream_state_changed_callback, |
| .add_buffer = stream_add_buffer_callback, |
| .process = input_callback }; |
| |
| static int PIPEWIRE_OpenDevice(_THIS, const char *devname) |
| { |
| /* |
| * NOTE: The PW_STREAM_FLAG_RT_PROCESS flag can be set to call the stream |
| * processing callback from the realtime thread. However, it comes with some |
| * caveats: no file IO, allocations, locking or other blocking operations |
| * must occur in the mixer callback. As this cannot be guaranteed when the |
| * callback is in the calling application, this flag is omitted. |
| */ |
| static const enum pw_stream_flags STREAM_FLAGS = PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS; |
| |
| char thread_name[PW_THREAD_NAME_BUFFER_LENGTH]; |
| Uint8 pod_buffer[PW_POD_BUFFER_LENGTH]; |
| struct spa_pod_builder b = SPA_POD_BUILDER_INIT(pod_buffer, sizeof(pod_buffer)); |
| struct spa_audio_info_raw spa_info = { 0 }; |
| const struct spa_pod *params = NULL; |
| struct SDL_PrivateAudioData *priv; |
| struct pw_properties *props; |
| const char *app_name, *stream_name, *stream_role, *error; |
| Uint32 node_id = this->handle == NULL ? PW_ID_ANY : PW_HANDLE_TO_ID(this->handle); |
| SDL_bool iscapture = this->iscapture; |
| int res; |
| |
| /* Clamp the period size to sane values */ |
| const int min_period = PW_MIN_SAMPLES * SPA_MAX(this->spec.freq / PW_BASE_CLOCK_RATE, 1); |
| |
| /* Get the hints for the application name, stream name and role */ |
| app_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME); |
| if (app_name == NULL || *app_name == '\0') { |
| app_name = SDL_GetHint(SDL_HINT_APP_NAME); |
| if (app_name == NULL || *app_name == '\0') { |
| app_name = "SDL Application"; |
| } |
| } |
| |
| stream_name = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_NAME); |
| if (stream_name == NULL || *stream_name == '\0') { |
| stream_name = "Audio Stream"; |
| } |
| |
| /* |
| * 'Music' is the default used internally by Pipewire and it's modules, |
| * but 'Game' seems more appropriate for the majority of SDL applications. |
| */ |
| stream_role = SDL_GetHint(SDL_HINT_AUDIO_DEVICE_STREAM_ROLE); |
| if (stream_role == NULL || *stream_role == '\0') { |
| stream_role = "Game"; |
| } |
| |
| /* Initialize the Pipewire stream info from the SDL audio spec */ |
| initialize_spa_info(&this->spec, &spa_info); |
| params = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &spa_info); |
| if (params == NULL) { |
| return SDL_SetError("Pipewire: Failed to set audio format parameters"); |
| } |
| |
| priv = SDL_calloc(1, sizeof(struct SDL_PrivateAudioData)); |
| this->hidden = priv; |
| if (priv == NULL) { |
| return SDL_OutOfMemory(); |
| } |
| |
| /* Size of a single audio frame in bytes */ |
| priv->stride = (SDL_AUDIO_BITSIZE(this->spec.format) >> 3) * this->spec.channels; |
| |
| if (this->spec.samples < min_period) { |
| this->spec.samples = min_period; |
| this->spec.size = this->spec.samples * priv->stride; |
| } |
| |
| (void)SDL_snprintf(thread_name, sizeof(thread_name), "SDLAudio%c%ld", (iscapture) ? 'C' : 'P', (long)this->handle); |
| priv->loop = PIPEWIRE_pw_thread_loop_new(thread_name, NULL); |
| if (priv->loop == NULL) { |
| return SDL_SetError("Pipewire: Failed to create stream loop (%i)", errno); |
| } |
| |
| /* Load the realtime module so Pipewire can set the loop thread to the appropriate priority. */ |
| props = PIPEWIRE_pw_properties_new(PW_KEY_CONFIG_NAME, "client-rt.conf", NULL); |
| if (props == NULL) { |
| return SDL_SetError("Pipewire: Failed to create stream context properties (%i)", errno); |
| } |
| |
| priv->context = PIPEWIRE_pw_context_new(PIPEWIRE_pw_thread_loop_get_loop(priv->loop), props, 0); |
| if (priv->context == NULL) { |
| return SDL_SetError("Pipewire: Failed to create stream context (%i)", errno); |
| } |
| |
| props = PIPEWIRE_pw_properties_new(NULL, NULL); |
| if (props == NULL) { |
| return SDL_SetError("Pipewire: Failed to create stream properties (%i)", errno); |
| } |
| |
| PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_TYPE, "Audio"); |
| PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_CATEGORY, iscapture ? "Capture" : "Playback"); |
| PIPEWIRE_pw_properties_set(props, PW_KEY_MEDIA_ROLE, stream_role); |
| PIPEWIRE_pw_properties_set(props, PW_KEY_APP_NAME, app_name); |
| PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_NAME, stream_name); |
| PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_DESCRIPTION, stream_name); |
| PIPEWIRE_pw_properties_setf(props, PW_KEY_NODE_LATENCY, "%u/%i", this->spec.samples, this->spec.freq); |
| PIPEWIRE_pw_properties_setf(props, PW_KEY_NODE_RATE, "1/%u", this->spec.freq); |
| PIPEWIRE_pw_properties_set(props, PW_KEY_NODE_ALWAYS_PROCESS, "true"); |
| |
| /* |
| * Pipewire 0.3.44 introduced PW_KEY_TARGET_OBJECT that takes either a path |
| * (PW_KEY_NODE_NAME) or node serial number (PE_KEY_OBJECT_SERIAL) to connect |
| * the stream to its target. The target_id parameter in pw_stream_connect() is |
| * now deprecated and should always be PW_ID_ANY. |
| */ |
| if (pipewire_version_at_least(0, 3, 44)) { |
| if (node_id != PW_ID_ANY) { |
| const struct io_node *node; |
| |
| PIPEWIRE_pw_thread_loop_lock(hotplug_loop); |
| node = io_list_get_by_id(node_id); |
| if (node != NULL) { |
| PIPEWIRE_pw_properties_set(props, PW_KEY_TARGET_OBJECT, node->path); |
| } |
| PIPEWIRE_pw_thread_loop_unlock(hotplug_loop); |
| |
| node_id = PW_ID_ANY; |
| } |
| } |
| |
| /* Create the new stream */ |
| priv->stream = PIPEWIRE_pw_stream_new_simple(PIPEWIRE_pw_thread_loop_get_loop(priv->loop), stream_name, props, |
| iscapture ? &stream_input_events : &stream_output_events, this); |
| if (priv->stream == NULL) { |
| return SDL_SetError("Pipewire: Failed to create stream (%i)", errno); |
| } |
| |
| res = PIPEWIRE_pw_stream_connect(priv->stream, iscapture ? PW_DIRECTION_INPUT : PW_DIRECTION_OUTPUT, node_id, STREAM_FLAGS, |
| ¶ms, 1); |
| if (res != 0) { |
| return SDL_SetError("Pipewire: Failed to connect stream"); |
| } |
| |
| res = PIPEWIRE_pw_thread_loop_start(priv->loop); |
| if (res != 0) { |
| return SDL_SetError("Pipewire: Failed to start stream loop"); |
| } |
| |
| /* Wait until all init flags are set or the stream has failed. */ |
| PIPEWIRE_pw_thread_loop_lock(priv->loop); |
| while (priv->stream_init_status != PW_READY_FLAG_ALL_BITS && |
| PIPEWIRE_pw_stream_get_state(priv->stream, NULL) != PW_STREAM_STATE_ERROR) { |
| PIPEWIRE_pw_thread_loop_wait(priv->loop); |
| } |
| PIPEWIRE_pw_thread_loop_unlock(priv->loop); |
| |
| if (PIPEWIRE_pw_stream_get_state(priv->stream, &error) == PW_STREAM_STATE_ERROR) { |
| return SDL_SetError("Pipewire: Stream error: %s", error); |
| } |
| |
| /* If this is a capture stream, make sure the intermediate buffer was successfully allocated. */ |
| if (iscapture && priv->buffer == NULL) { |
| return SDL_SetError("Pipewire: Failed to allocate source buffer"); |
| } |
| |
| return 0; |
| } |
| |
| static void PIPEWIRE_CloseDevice(_THIS) |
| { |
| if (this->hidden->loop) { |
| PIPEWIRE_pw_thread_loop_stop(this->hidden->loop); |
| } |
| |
| if (this->hidden->stream) { |
| PIPEWIRE_pw_stream_destroy(this->hidden->stream); |
| } |
| |
| if (this->hidden->context) { |
| PIPEWIRE_pw_context_destroy(this->hidden->context); |
| } |
| |
| if (this->hidden->loop) { |
| PIPEWIRE_pw_thread_loop_destroy(this->hidden->loop); |
| } |
| |
| if (this->hidden->buffer) { |
| SDL_FreeDataQueue(this->hidden->buffer); |
| } |
| |
| SDL_free(this->hidden); |
| } |
| |
| static int PIPEWIRE_GetDefaultAudioInfo(char **name, SDL_AudioSpec *spec, int iscapture) |
| { |
| struct io_node *node; |
| char *target; |
| int ret = 0; |
| |
| PIPEWIRE_pw_thread_loop_lock(hotplug_loop); |
| |
| if (iscapture) { |
| if (pipewire_default_source_id == NULL) { |
| ret = SDL_SetError("PipeWire could not find a default source"); |
| goto failed; |
| } |
| target = pipewire_default_source_id; |
| } else { |
| if (pipewire_default_sink_id == NULL) { |
| ret = SDL_SetError("PipeWire could not find a default sink"); |
| goto failed; |
| } |
| target = pipewire_default_sink_id; |
| } |
| |
| node = io_list_get_by_path(target); |
| if (node == NULL) { |
| ret = SDL_SetError("PipeWire device list is out of sync with defaults"); |
| goto failed; |
| } |
| |
| if (name != NULL) { |
| *name = SDL_strdup(node->name); |
| } |
| SDL_copyp(spec, &node->spec); |
| |
| failed: |
| PIPEWIRE_pw_thread_loop_unlock(hotplug_loop); |
| return ret; |
| } |
| |
| static void PIPEWIRE_Deinitialize() |
| { |
| if (pipewire_initialized) { |
| hotplug_loop_destroy(); |
| deinit_pipewire_library(); |
| pipewire_initialized = SDL_FALSE; |
| } |
| } |
| |
| static SDL_bool PIPEWIRE_Init(SDL_AudioDriverImpl *impl) |
| { |
| if (!pipewire_initialized) { |
| if (init_pipewire_library() < 0) { |
| return SDL_FALSE; |
| } |
| |
| pipewire_initialized = SDL_TRUE; |
| |
| if (hotplug_loop_init() < 0) { |
| PIPEWIRE_Deinitialize(); |
| return SDL_FALSE; |
| } |
| } |
| |
| /* Set the function pointers */ |
| impl->DetectDevices = PIPEWIRE_DetectDevices; |
| impl->OpenDevice = PIPEWIRE_OpenDevice; |
| impl->CloseDevice = PIPEWIRE_CloseDevice; |
| impl->Deinitialize = PIPEWIRE_Deinitialize; |
| impl->GetDefaultAudioInfo = PIPEWIRE_GetDefaultAudioInfo; |
| |
| impl->HasCaptureSupport = SDL_TRUE; |
| impl->ProvidesOwnCallbackThread = SDL_TRUE; |
| impl->SupportsNonPow2Samples = SDL_TRUE; |
| |
| return SDL_TRUE; |
| } |
| |
| AudioBootStrap PIPEWIRE_bootstrap = { "pipewire", "Pipewire", PIPEWIRE_Init, SDL_FALSE }; |
| |
| #endif /* SDL_AUDIO_DRIVER_PIPEWIRE */ |
| |
| /* vi: set ts=4 sw=4 expandtab: */ |