wireplumber/modules/module-pipewire/audio-softdsp-endpoint.c
George Kiagiadakis f360aed978 sofdsp-endpoint: run audioconvert in merge+split mode and use a new linking algorithm
Running audioconvert in merge+split mode is the only way to make this work with
the adapter, since the adapter does not support passing multiple channels on
a single port right now, and if it does at some point, it will be without a
mixing node on the port, which means we will not be able to mix multiple
audioconvert nodes on the same adapter. In the future we need to consider
writing a lighter volume node with multiple channels support to replace
audioconvert.

The new linking algorithm now takes into account the channel positions and makes
sure to link the correct channels together. Also, it avoids passing the port
proxies inside the GVariants, thus making the algorithm a bit more generic
and easier to unit test.
2019-10-02 21:27:44 +03:00

452 lines
14 KiB
C

/* WirePlumber
*
* Copyright © 2019 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
/**
* module-pw-audio-softdsp-endpoint provides a WpEndpoint implementation
* that wraps an audio device node in pipewire and plugs a DSP node, as well
* as optional merger+volume nodes that are used as entry points for the
* various streams that this endpoint may have
*/
#include <wp/wp.h>
#include <pipewire/pipewire.h>
#include <spa/param/audio/format-utils.h>
#include <spa/pod/builder.h>
#include <spa/param/props.h>
#include "audio-softdsp-endpoint/stream.h"
#include "audio-softdsp-endpoint/adapter.h"
#include "audio-softdsp-endpoint/convert.h"
#define MIN_QUANTUM_SIZE 64
#define MAX_QUANTUM_SIZE 1024
#define CONTROL_SELECTED 0
struct _WpPwAudioSoftdspEndpoint
{
WpEndpoint parent;
/* Properties */
WpProxyNode *proxy_node;
GVariant *streams;
char *role;
guint stream_count;
gboolean selected;
/* The task to signal the endpoint is initialized */
GTask *init_task;
/* Audio Streams */
WpAudioStream *adapter;
GPtrArray *converters;
};
enum {
PROP_0,
PROP_PROXY_NODE,
PROP_STREAMS,
PROP_ROLE,
};
static GAsyncInitableIface *wp_endpoint_parent_interface = NULL;
static void wp_endpoint_async_initable_init (gpointer iface,
gpointer iface_data);
G_DECLARE_FINAL_TYPE (WpPwAudioSoftdspEndpoint, endpoint,
WP_PW, AUDIO_SOFTDSP_ENDPOINT, WpEndpoint)
G_DEFINE_TYPE_WITH_CODE (WpPwAudioSoftdspEndpoint, endpoint, WP_TYPE_ENDPOINT,
G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
wp_endpoint_async_initable_init))
typedef GObject* (*WpObjectNewFinishFunc)(GObject *initable, GAsyncResult *res,
GError **error);
static GObject *
object_safe_new_finish(WpPwAudioSoftdspEndpoint * self, GObject *initable,
GAsyncResult *res, WpObjectNewFinishFunc new_finish_func)
{
g_autoptr (GObject) object = NULL;
GError *error = NULL;
/* Return NULL if we are already aborting */
if (!self->init_task)
return NULL;
/* Get the object */
object = G_OBJECT (new_finish_func (initable, res, &error));
if (error) {
g_warning ("WpPwAudioSoftdspEndpoint:%p Aborting construction", self);
g_task_return_error (self->init_task, error);
g_clear_object (&self->init_task);
return NULL;
}
return g_steal_pointer (&object);
}
static gboolean
endpoint_prepare_link (WpEndpoint * ep, guint32 stream_id,
WpEndpointLink * link, GVariant ** properties, GError ** error)
{
WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep);
WpAudioStream *stream = NULL;
/* Link with the adapter if stream id is none */
if (stream_id == WP_STREAM_ID_NONE)
return wp_audio_stream_prepare_link (self->adapter, properties, error);
/* Make sure the stream Id is valid */
g_return_val_if_fail(stream_id < self->converters->len, FALSE);
/* Make sure the stream is valid */
stream = g_ptr_array_index (self->converters, stream_id);
g_return_val_if_fail(stream, FALSE);
/* Prepare the link */
return wp_audio_stream_prepare_link (stream, properties, error);
}
static void
finish_endpoint_creation(WpPwAudioSoftdspEndpoint *self)
{
/* Don't do anything if the endpoint has already been initialized */
if (!self->init_task)
return;
/* Finish the creation of the endpoint */
g_task_return_boolean (self->init_task, TRUE);
g_clear_object(&self->init_task);
}
static void
on_audio_convert_created(GObject *initable, GAsyncResult *res, gpointer data)
{
WpPwAudioSoftdspEndpoint *self = data;
WpAudioStream *convert = NULL;
guint stream_id = 0;
g_autofree gchar *name = NULL;
/* Get the audio convert */
convert = WP_AUDIO_STREAM (object_safe_new_finish (self, initable, res,
(WpObjectNewFinishFunc)wp_audio_stream_new_finish));
if (!convert)
return;
/* Get the stream id */
g_object_get (convert, "id", &stream_id, "name", &name, NULL);
g_return_if_fail (stream_id >= 0);
/* Set the streams */
g_ptr_array_insert (self->converters, stream_id, convert);
g_debug ("%s:%p Created audio convert %u %s", G_OBJECT_TYPE_NAME (self), self,
stream_id, name);
/* Finish the endpoint creation when all the streams are created */
if (--self->stream_count == 0)
finish_endpoint_creation(self);
}
static void
on_audio_adapter_created(GObject *initable, GAsyncResult *res,
gpointer data)
{
WpPwAudioSoftdspEndpoint *self = data;
enum pw_direction direction = wp_endpoint_get_direction(WP_ENDPOINT(self));
g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self));
g_autoptr (WpProperties) props = NULL;
const struct spa_audio_info_raw *format;
g_autofree gchar *name = NULL;
GVariantDict d;
GVariantIter iter;
const gchar *stream;
int i;
/* Get the proxy adapter */
self->adapter = WP_AUDIO_STREAM (object_safe_new_finish (self, initable,
res, (WpObjectNewFinishFunc)wp_audio_stream_new_finish));
if (!self->adapter)
return;
props = wp_proxy_node_get_properties (self->proxy_node);
/* Give a proper name to this endpoint based on adapter properties */
if (0 == g_strcmp0(wp_properties_get (props, "device.api"), "alsa")) {
name = g_strdup_printf ("%s on %s (%s / node %s)",
wp_properties_get (props, "api.alsa.pcm.name"),
wp_properties_get (props, "api.alsa.card.name"),
wp_properties_get (props, "api.alsa.path"),
wp_properties_get (props, PW_KEY_NODE_ID));
g_object_set (self, "name", name, NULL);
}
/* Set the role */
self->role = g_strdup (wp_properties_get (props, PW_KEY_MEDIA_ROLE));
/* Just finish if no streams need to be created */
if (!self->streams) {
finish_endpoint_creation (self);
return;
}
/* Get the adapter format */
format = wp_audio_adapter_get_format (WP_AUDIO_ADAPTER (self->adapter));
g_return_if_fail (format);
/* Create the audio converters */
g_variant_iter_init (&iter, self->streams);
for (i = 0; g_variant_iter_next (&iter, "&s", &stream); i++) {
wp_audio_convert_new (WP_ENDPOINT(self), i, stream, direction,
self->adapter, format, on_audio_convert_created, self);
/* Register the stream */
g_variant_dict_init (&d, NULL);
g_variant_dict_insert (&d, "id", "u", i);
g_variant_dict_insert (&d, "name", "s", stream);
wp_endpoint_register_stream (WP_ENDPOINT (self), g_variant_dict_end (&d));
}
self->stream_count = i;
}
static void
endpoint_finalize (GObject * object)
{
WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (object);
g_clear_pointer(&self->streams, g_variant_unref);
/* Destroy the proxy adapter */
g_clear_object(&self->adapter);
/* Destroy all the converters */
g_clear_pointer (&self->converters, g_ptr_array_unref);
/* Destroy the done task */
g_clear_object(&self->init_task);
g_clear_object(&self->proxy_node);
g_free (self->role);
G_OBJECT_CLASS (endpoint_parent_class)->finalize (object);
}
static void
endpoint_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec)
{
WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (object);
switch (property_id) {
case PROP_PROXY_NODE:
self->proxy_node = g_value_dup_object (value);
break;
case PROP_STREAMS:
self->streams = g_value_dup_variant(value);
break;
case PROP_ROLE:
g_free (self->role);
self->role = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
endpoint_get_property (GObject * object, guint property_id,
GValue * value, GParamSpec * pspec)
{
WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (object);
switch (property_id) {
case PROP_PROXY_NODE:
g_value_set_object (value, self->proxy_node);
break;
case PROP_STREAMS:
g_value_set_variant (value, self->streams);
break;
case PROP_ROLE:
g_value_set_string (value, self->role);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static GVariant *
endpoint_get_control_value (WpEndpoint * ep, guint32 id)
{
WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep);
guint stream_id, control_id;
WpAudioStream *stream = NULL;
if (id == CONTROL_SELECTED)
return g_variant_new_boolean (self->selected);
wp_audio_stream_id_decode (id, &stream_id, &control_id);
/* Check if it is the adapter (master) */
if (stream_id == WP_STREAM_ID_NONE)
return wp_audio_stream_get_control_value (self->adapter, control_id);
/* Otherwise get the stream_id and control_id */
g_return_val_if_fail (stream_id < self->converters->len, NULL);
stream = g_ptr_array_index (self->converters, stream_id);
g_return_val_if_fail (stream, NULL);
return wp_audio_stream_get_control_value (stream, control_id);
}
static gboolean
endpoint_set_control_value (WpEndpoint * ep, guint32 id, GVariant * value)
{
WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep);
guint stream_id, control_id;
WpAudioStream *stream = NULL;
if (id == CONTROL_SELECTED) {
self->selected = g_variant_get_boolean (value);
wp_endpoint_notify_control_value (ep, CONTROL_SELECTED);
return TRUE;
}
wp_audio_stream_id_decode (id, &stream_id, &control_id);
/* Check if it is the adapter (master) */
if (stream_id == WP_STREAM_ID_NONE)
return wp_audio_stream_set_control_value (self->adapter, control_id, value);
/* Otherwise get the stream_id and control_id */
g_return_val_if_fail (stream_id < self->converters->len, FALSE);
stream = g_ptr_array_index (self->converters, stream_id);
g_return_val_if_fail (stream, FALSE);
return wp_audio_stream_set_control_value (stream, control_id, value);
}
static void
wp_endpoint_init_async (GAsyncInitable *initable, int io_priority,
GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data)
{
WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (initable);
enum pw_direction direction = wp_endpoint_get_direction(WP_ENDPOINT(self));
g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self));
GVariantDict d;
/* Create the async task */
self->init_task = g_task_new (initable, cancellable, callback, data);
/* Create the adapter proxy */
wp_audio_adapter_new (WP_ENDPOINT(self), WP_STREAM_ID_NONE, "master",
direction, self->proxy_node, FALSE, on_audio_adapter_created, self);
/* Register the selected control */
self->selected = FALSE;
g_variant_dict_init (&d, NULL);
g_variant_dict_insert (&d, "id", "u", CONTROL_SELECTED);
g_variant_dict_insert (&d, "name", "s", "selected");
g_variant_dict_insert (&d, "type", "s", "b");
g_variant_dict_insert (&d, "default-value", "b", self->selected);
wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d));
/* Call the parent interface */
wp_endpoint_parent_interface->init_async (initable, io_priority, cancellable,
callback, data);
}
static void
wp_endpoint_async_initable_init (gpointer iface, gpointer iface_data)
{
GAsyncInitableIface *ai_iface = iface;
/* Set the parent interface */
wp_endpoint_parent_interface = g_type_interface_peek_parent (iface);
/* Only set the init_async */
ai_iface->init_async = wp_endpoint_init_async;
}
static void
endpoint_init (WpPwAudioSoftdspEndpoint * self)
{
self->converters = g_ptr_array_new_with_free_func (g_object_unref);
}
static void
endpoint_class_init (WpPwAudioSoftdspEndpointClass * klass)
{
GObjectClass *object_class = (GObjectClass *) klass;
WpEndpointClass *endpoint_class = (WpEndpointClass *) klass;
object_class->finalize = endpoint_finalize;
object_class->set_property = endpoint_set_property;
object_class->get_property = endpoint_get_property;
endpoint_class->prepare_link = endpoint_prepare_link;
endpoint_class->get_control_value = endpoint_get_control_value;
endpoint_class->set_control_value = endpoint_set_control_value;
/* Instal the properties */
g_object_class_install_property (object_class, PROP_PROXY_NODE,
g_param_spec_object ("proxy-node", "proxy-node",
"The node this endpoint refers to", WP_TYPE_PROXY_NODE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_STREAMS,
g_param_spec_variant ("streams", "streams",
"The stream names for the streams to create",
G_VARIANT_TYPE ("as"), NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
g_object_class_install_property (object_class, PROP_ROLE,
g_param_spec_string ("role", "role", "The role of the wrapped node", NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}
void
audio_softdsp_endpoint_factory (WpFactory * factory, GType type, GVariant * properties,
GAsyncReadyCallback ready, gpointer user_data)
{
g_autoptr (WpCore) core = NULL;
const gchar *name, *media_class;
guint direction;
guint64 node;
g_autoptr (GVariant) streams = NULL;
/* Make sure the type is correct */
g_return_if_fail(type == WP_TYPE_ENDPOINT);
/* Get the Core */
core = wp_factory_get_core(factory);
g_return_if_fail (core);
/* Get the properties */
if (!g_variant_lookup (properties, "name", "&s", &name))
return;
if (!g_variant_lookup (properties, "media-class", "&s", &media_class))
return;
if (!g_variant_lookup (properties, "direction", "u", &direction))
return;
if (!g_variant_lookup (properties, "proxy-node", "t", &node))
return;
streams = g_variant_lookup_value (properties, "streams",
G_VARIANT_TYPE ("as"));
/* Create and return the softdsp endpoint object */
g_async_initable_new_async (
endpoint_get_type (), G_PRIORITY_DEFAULT, NULL, ready, user_data,
"core", core,
"name", name,
"media-class", media_class,
"direction", direction,
"proxy-node", (gpointer) node,
"streams", streams,
NULL);
}