Files
wireplumber/modules/module-pw-alsa-udev.c
George Kiagiadakis a63f2bb99b lib/wp: merge both WpRemote & WpRemotePipewire in WpCore
In practice we always create a remote and connect to pipewire.
Any other scenario is invalid, therefore, it is not justified
to be confused with so many classes for such small functionality.
This simplifies a lot the modules code.

Also, this commit exposes the pw_core and pw_remote objects
out of WpCore. This is in practice useful when dealing with low-level
pw and spa factories, which are used in the monitors. Let's not
add API wrappers for everything... Bindings will never use this
functionality anyway, since it depends on low level pipewire C API.
2019-09-07 17:55:46 +03:00

563 lines
14 KiB
C

/* WirePlumber
*
* Copyright © 2019 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* 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 <spa/utils/keys.h>
#include <spa/utils/names.h>
#include <spa/monitor/monitor.h>
#include <pipewire/pipewire.h>
#include <wp/wp.h>
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);
}