/* WirePlumber * * Copyright © 2019 Collabora Ltd. * @author George Kiagiadakis * * SPDX-License-Identifier: MIT */ /** * module-pw-alsa-udev provides alsa device detection through pipewire * and automatically creates endpoints for all alsa device nodes that appear */ #include #include #include #include #include struct monitor { struct spa_handle *handle; struct spa_monitor *monitor; struct spa_list device_list; }; struct impl { WpModule *module; GHashTable *registered_endpoints; GVariant *streams; /* The alsa monitor */ struct monitor monitor; }; struct device { struct impl *impl; struct spa_list link; uint32_t id; struct pw_properties *props; struct spa_handle *handle; struct pw_proxy *proxy; struct spa_device *device; struct spa_hook device_listener; struct spa_list node_list; }; struct node { struct impl *impl; struct device *device; struct spa_list link; uint32_t id; WpProxy *proxy; struct spa_node *node; }; static void on_endpoint_created(GObject *initable, GAsyncResult *res, gpointer d) { struct impl *impl = d; g_autoptr (WpEndpoint) endpoint = NULL; g_autoptr (WpProxy) proxy = NULL; guint global_id = 0; GError *error = NULL; /* Get the endpoint */ endpoint = wp_endpoint_new_finish(initable, res, &error); if (error) { g_warning ("Failed to create alsa endpoint: %s", error->message); return; } /* Get the endpoint global id */ g_object_get (endpoint, "proxy-node", &proxy, NULL); global_id = wp_proxy_get_global_id (proxy); g_debug ("Created alsa endpoint for global id %d", global_id); /* Register the endpoint and add it to the table */ wp_endpoint_register (endpoint); g_hash_table_insert (impl->registered_endpoints, GUINT_TO_POINTER(global_id), g_steal_pointer (&endpoint)); } static gboolean parse_alsa_properties (WpProperties *props, const gchar **name, const gchar **media_class, enum pw_direction *direction) { const char *local_name = NULL; const char *local_media_class = NULL; enum pw_direction local_direction; /* Get the name */ local_name = wp_properties_get (props, PW_KEY_NODE_NAME); if (!local_name) return FALSE; /* Get the media class */ local_media_class = wp_properties_get (props, PW_KEY_MEDIA_CLASS); if (!local_media_class) return FALSE; /* Get the direction */ if (g_str_has_prefix (local_media_class, "Audio/Sink")) local_direction = PW_DIRECTION_INPUT; else if (g_str_has_prefix (local_media_class, "Audio/Source")) local_direction = PW_DIRECTION_OUTPUT; else return FALSE; /* Set the name */ if (name) *name = local_name; /* Set the media class */ if (media_class) { switch (local_direction) { case PW_DIRECTION_INPUT: *media_class = "Alsa/Sink"; break; case PW_DIRECTION_OUTPUT: *media_class = "Alsa/Source"; break; default: break; } } /* Set the direction */ if (direction) *direction = local_direction; return TRUE; } /* TODO: we need to find a better way to do this */ static gboolean is_alsa_node (WpProperties * props) { const gchar *name = NULL; const gchar *media_class = NULL; /* Get the name */ name = wp_properties_get (props, "node.name"); if (!name) return FALSE; /* Get the media class */ media_class = wp_properties_get (props, SPA_KEY_MEDIA_CLASS); if (!media_class) return FALSE; /* Check if it is an audio device */ if (!g_str_has_prefix (media_class, "Audio/")) return FALSE; /* Check it is not a convert */ if (g_str_has_prefix (media_class, "Audio/Convert")) return FALSE; /* Check if it is not a bluez device */ if (g_str_has_prefix (name, "bluez5.")) return FALSE; return TRUE; } static void on_node_added(WpCore *core, WpProxy *proxy, struct impl *impl) { const gchar *media_class, *name; enum pw_direction direction; GVariantBuilder b; g_autoptr (WpProperties) props = NULL; g_autoptr (GVariant) endpoint_props = NULL; props = wp_proxy_get_global_properties (proxy); g_return_if_fail(props); /* Only handle alsa nodes */ if (!is_alsa_node (props)) return; /* Parse the alsa properties */ if (!parse_alsa_properties (props, &name, &media_class, &direction)) { g_critical ("failed to parse alsa properties"); return; } /* Set the properties */ g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (&b, "{sv}", "name", g_variant_new_take_string (g_strdup_printf ( "Alsa %u (%s)", wp_proxy_get_global_id (proxy), name))); g_variant_builder_add (&b, "{sv}", "media-class", g_variant_new_string (media_class)); g_variant_builder_add (&b, "{sv}", "direction", g_variant_new_uint32 (direction)); g_variant_builder_add (&b, "{sv}", "proxy-node", g_variant_new_uint64 ((guint64) proxy)); g_variant_builder_add (&b, "{sv}", "streams", impl->streams); endpoint_props = g_variant_builder_end (&b); /* Create the endpoint async */ wp_factory_make (core, "pw-audio-softdsp-endpoint", WP_TYPE_ENDPOINT, endpoint_props, on_endpoint_created, impl); } static void on_node_removed (WpCore *core, WpProxy *proxy, struct impl *impl) { WpEndpoint *endpoint = NULL; guint32 id = wp_proxy_get_global_id (proxy); /* Get the endpoint */ endpoint = g_hash_table_lookup (impl->registered_endpoints, GUINT_TO_POINTER(id)); if (!endpoint) return; /* Unregister the endpoint and remove it from the table */ wp_endpoint_unregister (endpoint); g_hash_table_remove (impl->registered_endpoints, GUINT_TO_POINTER(id)); } static struct node * create_node(struct impl *impl, struct device *dev, uint32_t id, const struct spa_device_object_info *info) { struct node *node; const char *name; g_autoptr (WpProperties) props = NULL; g_autoptr (WpCore) core = wp_module_get_core (impl->module); /* Check if the type is a node */ if (info->type != SPA_TYPE_INTERFACE_Node) return NULL; props = wp_properties_new_copy (dev->props); /* Get the alsa name */ name = wp_properties_get (props, SPA_KEY_DEVICE_NICK); if (name == NULL) name = wp_properties_get (props, SPA_KEY_DEVICE_NAME); if (name == NULL) name = wp_properties_get (props, SPA_KEY_DEVICE_ALIAS); if (name == NULL) name = "alsa-device"; /* Create the properties */ wp_properties_update_from_dict (props, info->props); wp_properties_set(props, PW_KEY_NODE_NAME, name); wp_properties_set(props, PW_KEY_FACTORY_NAME, info->factory_name); wp_properties_set(props, "merger.monitor", "1"); /* Create the node */ node = g_slice_new0(struct node); node->impl = impl; node->device = dev; node->id = id; node->proxy = wp_core_create_remote_object (core, "adapter", PW_TYPE_INTERFACE_Node, PW_VERSION_NODE_PROXY, props); if (!node->proxy) { g_slice_free (struct node, node); return NULL; } /* Add the node to the list */ spa_list_append(&dev->node_list, &node->link); return node; } static void update_node(struct impl *impl, struct device *dev, struct node *node, const struct spa_device_object_info *info) { } static void destroy_node(struct impl *impl, struct device *dev, struct node *node) { /* Remove the node from the list */ spa_list_remove(&node->link); /* Destroy the proxy node */ g_clear_object (&node->proxy); /* Destroy the node */ g_slice_free (struct node, node); } static struct node * find_node(struct device *dev, uint32_t id) { struct node *node; /* Find the node in the list */ spa_list_for_each(node, &dev->node_list, link) { if (node->id == id) return node; } return NULL; } static void device_object_info(void *data, uint32_t id, const struct spa_device_object_info *info) { struct device *dev = data; struct impl *impl = dev->impl; struct node *node = NULL; /* Find the node */ node = find_node(dev, id); if (info) { /* Just update the node if it already exits, otherwise create it */ if (node) update_node(impl, dev, node, info); else create_node(impl, dev, id, info); } else { /* Just remove the node if it already exists */ if (node) destroy_node(impl, dev, node); } } static void device_info(void *data, const struct spa_device_info *info) { struct device *dev = data; pw_properties_update(dev->props, info->props); } static const struct spa_device_events device_events = { SPA_VERSION_DEVICE_EVENTS, .info = device_info, .object_info = device_object_info }; static struct device* create_device(struct impl *impl, uint32_t id, const struct spa_monitor_object_info *info) { g_autoptr (WpCore) core = wp_module_get_core (impl->module); struct device *dev; struct spa_handle *handle; int res; void *iface; /* Check if the type is a device */ if (info->type != SPA_TYPE_INTERFACE_Device) return NULL; /* Load the device handle */ handle = pw_core_load_spa_handle (wp_core_get_pw_core (core), info->factory_name, info->props); if (!handle) return NULL; /* Get the handle interface */ res = spa_handle_get_interface(handle, info->type, &iface); if (res < 0) { pw_unload_spa_handle(handle); return NULL; } /* Create the device */ dev = g_slice_new0(struct device); dev->impl = impl; dev->id = id; dev->handle = handle; dev->device = iface; dev->props = pw_properties_new_dict(info->props); dev->proxy = pw_remote_export (wp_core_get_pw_remote (core), info->type, dev->props, dev->device, 0); if (!dev->proxy) { pw_unload_spa_handle(handle); return NULL; } spa_list_init(&dev->node_list); /* Add device listener for events */ spa_device_add_listener(dev->device, &dev->device_listener, &device_events, dev); /* Add the device to the list */ spa_list_append(&impl->monitor.device_list, &dev->link); return dev; } static void update_device(struct impl *impl, struct device *dev, const struct spa_monitor_object_info *info) { /* Make sure the device and its info are valid */ g_return_if_fail (dev); g_return_if_fail (info); /* Update the properties of the device */ pw_properties_update(dev->props, info->props); } static void destroy_device(struct impl *impl, struct device *dev) { struct node *node; /* Remove the device from the list */ spa_list_remove(&dev->link); /* Remove the device listener */ spa_hook_remove(&dev->device_listener); /* Destry all the nodes that the device has */ spa_list_consume(node, &dev->node_list, link) destroy_node(impl, dev, node); /* Destroy the device proxy */ pw_proxy_destroy(dev->proxy); /* Unload the device handle */ pw_unload_spa_handle(dev->handle); /* Destroy the object */ g_slice_free (struct device, dev); } static struct device * find_device(struct impl *impl, uint32_t id) { struct device *dev; /* Find the device in the list */ spa_list_for_each(dev, &impl->monitor.device_list, link) { if (dev->id == id) return dev; } return NULL; } static int monitor_object_info(gpointer data, uint32_t id, const struct spa_monitor_object_info *info) { struct impl *impl = data; struct device *dev = NULL; /* Find the device */ dev = find_device(impl, id); if (info) { /* Just update the device if it already exits, otherwise create it */ if (dev) update_device(impl, dev, info); else if (!create_device(impl, id, info)) return -ENOMEM; } else { /* Just remove the device if it already exists, otherwise return error */ if (dev) destroy_device(impl, dev); else return -ENODEV; } return 0; } static const struct spa_monitor_callbacks monitor_callbacks = { SPA_VERSION_MONITOR_CALLBACKS, .object_info = monitor_object_info, }; static void start_monitor (WpCore *core, WpRemoteState state, gpointer data) { struct impl *impl = data; struct spa_handle *handle; int res; void *iface; /* Load the monitor handle */ handle = pw_core_load_spa_handle (wp_core_get_pw_core (core), SPA_NAME_API_ALSA_MONITOR, NULL); g_return_if_fail (handle); /* Get the handle interface */ res = spa_handle_get_interface(handle, SPA_TYPE_INTERFACE_Monitor, &iface); if (res < 0) { g_critical ("module-pw-alsa-udev cannot get monitor interface"); pw_unload_spa_handle(handle); return; } /* Init the monitor data */ impl->monitor.handle = handle; impl->monitor.monitor = iface; spa_list_init(&impl->monitor.device_list); /* Set the monitor callbacks */ spa_monitor_set_callbacks(impl->monitor.monitor, &monitor_callbacks, impl); } static void module_destroy (gpointer data) { struct impl *impl = data; /* Set to NULL as we don't own the reference */ impl->module = NULL; /* Destroy the registered endpoints table */ g_hash_table_unref(impl->registered_endpoints); impl->registered_endpoints = NULL; g_clear_pointer (&impl->streams, g_variant_unref); /* Clean up */ g_slice_free (struct impl, impl); } void wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args) { struct impl *impl; /* Create the module data */ impl = g_slice_new0(struct impl); impl->module = module; impl->registered_endpoints = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)g_object_unref); impl->streams = g_variant_lookup_value (args, "streams", G_VARIANT_TYPE ("as")); /* Set destroy callback for impl */ wp_module_set_destroy_callback (module, module_destroy, impl); /* Add the spa lib */ pw_core_add_spa_lib (wp_core_get_pw_core (core), "api.alsa.*", "alsa/libspa-alsa"); /* Start the monitor when the connected callback is triggered */ g_signal_connect(core, "remote-state-changed::connected", (GCallback) start_monitor, impl); /* Register the global addded/removed callbacks */ g_signal_connect(core, "remote-global-added::node", (GCallback) on_node_added, impl); g_signal_connect(core, "remote-global-removed::node", (GCallback) on_node_removed, impl); }