From f3ebbc06d3e9ac16cd133c77aa877196b433ef2a Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Fri, 19 Feb 2021 16:02:20 -0500 Subject: [PATCH] audio: pipewire: Retrieve the channel count and default sample rate for sinks/sources Extend device enumeration to retrieve the channel count and default sample rate for sink and source nodes. This required a fairly significant rework of the enumeration procedure as multiple callbacks are involved now. Sink/source nodes are tracked in a separate list during the enumeration process so they can be cleaned up if a device is removed before completion. These changes also simplify any future efforts that may be needed to retrieve additional configuration information from the nodes. --- src/audio/pipewire/SDL_pipewire.c | 332 +++++++++++++++++++++++++----- 1 file changed, 285 insertions(+), 47 deletions(-) diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c index c37c89ad0..d7b40ecdc 100644 --- a/src/audio/pipewire/SDL_pipewire.c +++ b/src/audio/pipewire/SDL_pipewire.c @@ -57,6 +57,8 @@ 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 *, @@ -147,6 +149,8 @@ load_pipewire_syms() 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); @@ -183,17 +187,30 @@ deinit_pipewire_library() unload_pipewire_library(); } -/* Linked list for tracking connected devices */ +/* Linked lists for tracking pending and connected devices */ struct connected_device { struct spa_list link; - Uint32 id; - SDL_bool is_capture; + Uint32 id; + SDL_bool is_capture; + SDL_AudioSpec spec; char name[]; }; +struct node_object +{ + struct spa_list link; + + struct pw_proxy *proxy; + struct spa_hook node_listener; + struct spa_hook core_listener; + int seq; + + struct connected_device *dev; +}; + /* The global hotplug thread and associated objects. */ static struct pw_thread_loop *hotplug_loop; static struct pw_core * hotplug_core; @@ -201,37 +218,32 @@ 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 device_list; +static struct spa_list hotplug_pending_list; +static struct spa_list hotplug_device_list; static int hotplug_init_seq_val; static SDL_atomic_t hotplug_init_complete; static SDL_atomic_t hotplug_events_enabled; +/* The active device list */ static void -check_add_device(Uint32 id, const char *name, const char *desc, SDL_bool is_capture) +check_add_device(struct connected_device *new_dev) { struct connected_device *dev; - int str_buf_size; PIPEWIRE_pw_thread_loop_lock(hotplug_loop); /* See if the device is already in the list */ - spa_list_for_each (dev, &device_list, link) { - if (dev->id == id) { + spa_list_for_each (dev, &hotplug_device_list, link) { + if (dev->id == new_dev->id) { goto dup_found; } } /* Add to the list if the device doesn't already exist */ - str_buf_size = SDL_strlen(name) + SDL_strlen(desc) + 3; - dev = SDL_malloc(sizeof(struct connected_device) + str_buf_size); - dev->id = id; - dev->is_capture = is_capture; - SDL_snprintf(dev->name, str_buf_size, "%s: %s", name, desc); - - spa_list_append(&device_list, &dev->link); + spa_list_append(&hotplug_device_list, &new_dev->link); if (SDL_AtomicGet(&hotplug_events_enabled)) { - SDL_AddAudioDevice(is_capture, name, PW_ID_TO_HANDLE(id)); + SDL_AddAudioDevice(new_dev->is_capture, new_dev->name, PW_ID_TO_HANDLE(new_dev->id)); } dup_found: @@ -247,7 +259,7 @@ check_remove_device(Uint32 id) PIPEWIRE_pw_thread_loop_lock(hotplug_loop); /* Find and remove the device from the list */ - spa_list_for_each_safe (dev, temp, &device_list, link) { + spa_list_for_each_safe (dev, temp, &hotplug_device_list, link) { if (dev->id == id) { spa_list_remove(&dev->link); @@ -269,18 +281,227 @@ release_device_list() { struct connected_device *dev, *temp; - spa_list_for_each_safe (dev, temp, &device_list, link) { + spa_list_for_each_safe (dev, temp, &hotplug_device_list, link) { spa_list_remove(&dev->link); SDL_free(dev); } } +/* The pending device list */ +static void +add_pending(struct node_object *node) +{ + SDL_assert(node); + spa_list_append(&hotplug_pending_list, &node->link); +} + +static void +remove_pending(Uint32 id) +{ + struct node_object *n, *temp; + + spa_list_for_each_safe (n, temp, &hotplug_pending_list, link) { + if (n->dev->id == id) { + spa_list_remove(&n->link); + spa_hook_remove(&n->core_listener); + spa_hook_remove(&n->node_listener); + SDL_free(n->dev); + PIPEWIRE_pw_proxy_destroy(n->proxy); + } + } +} + +static void +release_pending_list() +{ + struct node_object *n, *temp; + + spa_list_for_each_safe (n, temp, &hotplug_pending_list, link) { + spa_list_remove(&n->link); + spa_hook_remove(&n->core_listener); + spa_hook_remove(&n->node_listener); + SDL_free(n->dev); + PIPEWIRE_pw_proxy_destroy(n->proxy); + } +} + +static void +pending_to_active(struct node_object *node) +{ + SDL_assert(node); + + /* + * Move the specified device to the active list + * and destroy the hooks and proxy for the node. + */ + spa_list_remove(&node->link); + check_add_device(node->dev); + spa_hook_remove(&node->node_listener); + spa_hook_remove(&node->core_listener); + PIPEWIRE_pw_proxy_destroy(node->proxy); +} + +/* 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) { + PIPEWIRE_pw_thread_loop_lock(hotplug_loop); + + /* This core listener is no longer needed. */ + spa_hook_remove(&hotplug_core_listener); + + /* Signal that the initial device list is populated */ + SDL_AtomicSet(&hotplug_init_complete, 1); + PIPEWIRE_pw_thread_loop_signal(hotplug_loop, false); + + PIPEWIRE_pw_thread_loop_unlock(hotplug_loop); + } +} + +static void +core_events_node_callback(void *object, uint32_t id, int seq) +{ + struct node_object *node = object; + + if (id == PW_ID_CORE && seq == node->seq) { + /* Move the node from the pending to the active list */ + pending_to_active(node); + } +} + +const struct pw_core_events hotplug_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_hotplug_init_callback }; +const struct pw_core_events node_core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_node_callback }; + +static void +hotplug_core_sync(struct node_object *node) +{ + /* + * Node sync events *must* come before the hotplug init sync events or the initial + * device 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 (!SDL_AtomicGet(&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 && prop->value.type == SPA_TYPE_Int) { + spa_pod_get_int(&prop->value, &v); + + if (val) { + *val = (int)v; + } + + return SDL_TRUE; + } + + return SDL_FALSE; +} + +/* Detailed node information callbacks */ +static void +node_event_info(void *object, const struct pw_node_info *info) +{ + struct node_object *node = object; + const char * prop_val; + Uint32 i; + + if (info) { + prop_val = spa_dict_lookup(info->props, PW_KEY_AUDIO_CHANNELS); + if (prop_val) { + node->dev->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((struct pw_node *)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; + + /* Get the default frequency */ + if (node->dev->spec.freq == 0) { + get_range_param(param, SPA_FORMAT_AUDIO_rate, &node->dev->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 (node->dev->spec.channels == 0) { + int channels; + if (get_int_param(param, SPA_FORMAT_AUDIO_channels, &channels)) { + node->dev->spec.channels = (Uint8)channels; + } + } +} + +static const struct pw_node_events node_events = { PW_VERSION_NODE_EVENTS, .info = node_event_info, .param = node_event_param }; + +/* 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) { - const char *node_nick, *node_desc; - SDL_bool is_capture; + const char * node_nick, *node_desc; + struct pw_proxy * proxy; + struct node_object *node; + SDL_bool is_capture; + int str_buffer_len; /* We're only interested in nodes */ if (!SDL_strcmp(type, PW_TYPE_INTERFACE_Node)) { @@ -299,7 +520,41 @@ registry_event_global_callback(void *object, uint32_t id, uint32_t permissions, node_desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); if (node_nick && node_desc) { - check_add_device(id, node_nick, node_desc, is_capture); + proxy = pw_registry_bind(hotplug_registry, id, type, PW_VERSION_NODE, sizeof(struct node_object)); + if (proxy == NULL) { + return; + } + + /* The proxy object owns the node's memory and it will be freed when the proxy is destroyed */ + node = PIPEWIRE_pw_proxy_get_user_data(proxy); + SDL_memset(node, 0, sizeof(struct node_object)); + + node->proxy = proxy; + + /* Allocate and initialize the device information struct */ + str_buffer_len = SDL_strlen(node_nick) + SDL_strlen(node_desc) + 3; + node->dev = SDL_calloc(1, sizeof(struct connected_device) + str_buffer_len); + if (node->dev == NULL) { + PIPEWIRE_pw_proxy_destroy(proxy); + SDL_OutOfMemory(); + return; + } + + /* Begin setting the device properties */ + node->dev->id = id; + node->dev->is_capture = is_capture; + node->dev->spec.format = AUDIO_F32; /* Pipewire uses floats internally, other formats require conversion */ + SDL_snprintf(node->dev->name, str_buffer_len, "%s: %s", node_nick, node_desc); + + /* Set the callbacks */ + pw_core_add_listener(hotplug_core, &node->core_listener, &node_core_events, node); + PIPEWIRE_pw_proxy_add_object_listener(node->proxy, &node->node_listener, &node_events, node); + + /* Add this node to the pending list */ + add_pending(node); + + /* Update sync points */ + hotplug_core_sync(node); } } } @@ -308,37 +563,20 @@ registry_event_global_callback(void *object, uint32_t id, uint32_t permissions, static void registry_event_remove_callback(void *object, uint32_t id) { + remove_pending(id); check_remove_device(id); } static const struct pw_registry_events registry_events = { PW_VERSION_REGISTRY_EVENTS, .global = registry_event_global_callback, .global_remove = registry_event_remove_callback }; -static void -core_events_callback(void *object, uint32_t id, int seq) -{ - if (id == PW_ID_CORE && seq == hotplug_init_seq_val) { - PIPEWIRE_pw_thread_loop_lock(hotplug_loop); - - /* This core listener is no longer needed. */ - spa_hook_remove(&hotplug_core_listener); - - /* Signal that the initial device list is populated */ - SDL_AtomicSet(&hotplug_init_complete, 1); - PIPEWIRE_pw_thread_loop_signal(hotplug_loop, false); - - PIPEWIRE_pw_thread_loop_unlock(hotplug_loop); - } -} - -const struct pw_core_events core_events = { PW_VERSION_CORE_EVENTS, .done = core_events_callback }; - static int hotplug_loop_init() { int res; - spa_list_init(&device_list); + spa_list_init(&hotplug_pending_list); + spa_list_init(&hotplug_device_list); hotplug_loop = PIPEWIRE_pw_thread_loop_new("SDLAudioHotplug", NULL); if (hotplug_loop == NULL) { @@ -364,7 +602,7 @@ hotplug_loop_init() 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, &core_events, NULL); + pw_core_add_listener(hotplug_core, &hotplug_core_listener, &hotplug_core_events, NULL); hotplug_init_seq_val = pw_core_sync(hotplug_core, PW_ID_CORE, 0); @@ -383,6 +621,9 @@ hotplug_loop_destroy() PIPEWIRE_pw_thread_loop_stop(hotplug_loop); } + release_pending_list(); + release_device_list(); + if (hotplug_registry) { PIPEWIRE_pw_proxy_destroy((struct pw_proxy *)hotplug_registry); } @@ -398,8 +639,6 @@ hotplug_loop_destroy() if (hotplug_loop) { PIPEWIRE_pw_thread_loop_destroy(hotplug_loop); } - - release_device_list(); } static void @@ -414,7 +653,7 @@ PIPEWIRE_DetectDevices() PIPEWIRE_pw_thread_loop_wait(hotplug_loop); } - spa_list_for_each (dev, &device_list, link) { + spa_list_for_each (dev, &hotplug_device_list, link) { SDL_AddAudioDevice(dev->is_capture, dev->name, PW_ID_TO_HANDLE(dev->id)); } @@ -704,11 +943,10 @@ PIPEWIRE_OpenDevice(_THIS, void *handle, const char *devname, int iscapture) min_period = PW_MIN_SAMPLES * SPA_MAX(this->spec.freq / 48000, 1); adjusted_samples = SPA_CLAMP(this->spec.samples, min_period, PW_MAX_SAMPLES); - if ((priv = SDL_malloc(sizeof(struct SDL_PrivateAudioData))) == NULL) { + if ((priv = SDL_calloc(1, sizeof(struct SDL_PrivateAudioData))) == NULL) { return SDL_OutOfMemory(); } - SDL_zerop(priv); this->hidden = priv; /* Size of a single audio frame in bytes */