diff --git a/modules/meson.build b/modules/meson.build index 9b7b2d5ba6e6dbd7c185914032689bd922931328..662142eda9bb61190fa4d4f9e68ed9fc717651fa 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -54,7 +54,8 @@ shared_library( 'wireplumber-module-pw-audio-softdsp-endpoint', [ 'module-pw-audio-softdsp-endpoint.c', - ], + 'module-pw-audio-softdsp-endpoint/dsp.c', + ], c_args : [common_c_args, '-DG_LOG_DOMAIN="m-pw-audio-softdsp-endpoint"'], install : true, install_dir : wireplumber_module_dir, diff --git a/modules/module-mixer.c b/modules/module-mixer.c index b9e995866dcd1898d8a57c545baeab153c940a79..5a5153f2974d3ba420679463deaab711d4782815 100644 --- a/modules/module-mixer.c +++ b/modules/module-mixer.c @@ -11,10 +11,10 @@ G_DECLARE_FINAL_TYPE (WpMixerEndpoint, mixer_endpoint, WP, MIXER_ENDPOINT, WpEndpoint) -static const char * streams[] = { - "Master" +enum { + PROP_0, + PROP_STREAMS, }; -#define N_STREAMS (sizeof (streams) / sizeof (const char *)) enum { CONTROL_VOLUME = 0, @@ -28,7 +28,7 @@ enum { struct group { WpMixerEndpoint *mixer; - const gchar *name; + gchar *name; guint32 mixer_stream_id; GWeakRef backend; @@ -38,7 +38,8 @@ struct group struct _WpMixerEndpoint { WpEndpoint parent; - struct group groups[N_STREAMS]; + GVariant *streams; + GArray *groups; }; G_DEFINE_TYPE (WpMixerEndpoint, mixer_endpoint, WP_TYPE_ENDPOINT) @@ -104,14 +105,15 @@ policy_changed (WpPolicyManager *mgr, WpMixerEndpoint * self) int i; g_autoptr (WpCore) core = wp_endpoint_get_core (WP_ENDPOINT (self)); - for (i = 0; i < N_STREAMS; i++) { - group_find_backend (&self->groups[i], core); + for (i = 0; i < self->groups->len; i++) { + group_find_backend (&g_array_index (self->groups, struct group, i), core); } } static void mixer_endpoint_init (WpMixerEndpoint * self) { + self->groups = g_array_new (FALSE, TRUE, sizeof (struct group)); } static void @@ -120,18 +122,27 @@ mixer_endpoint_constructed (GObject * object) WpMixerEndpoint *self = WP_MIXER_ENDPOINT (object); g_autoptr (WpCore) core = NULL; g_autoptr (WpPolicyManager) policymgr = NULL; + struct group empty_group = {0}; GVariantDict d; + GVariantIter iter; gint i; + gchar *stream; core = wp_endpoint_get_core (WP_ENDPOINT (self)); policymgr = wp_policy_manager_get_instance (core); g_signal_connect_object (policymgr, "policy-changed", (GCallback) policy_changed, self, 0); - for (i = 0; i < N_STREAMS; i++) { + g_variant_iter_init (&iter, self->streams); + for (i = 0; g_variant_iter_next (&iter, "s", &stream); i++) { + struct group *group; + + g_array_append_val (self->groups, empty_group); + group = &g_array_index (self->groups, struct group, i); + g_variant_dict_init (&d, NULL); g_variant_dict_insert (&d, "id", "u", i); - g_variant_dict_insert (&d, "name", "s", streams[i]); + g_variant_dict_insert (&d, "name", "s", stream); wp_endpoint_register_stream (WP_ENDPOINT (self), g_variant_dict_end (&d)); g_variant_dict_init (&d, NULL); @@ -151,12 +162,12 @@ mixer_endpoint_constructed (GObject * object) g_variant_dict_insert (&d, "default-value", "b", FALSE); wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d)); - self->groups[i].mixer = self; - self->groups[i].name = streams[i]; - self->groups[i].mixer_stream_id = i; - g_weak_ref_init (&self->groups[i].backend, NULL); + group->mixer = self; + group->name = stream; + group->mixer_stream_id = i; + g_weak_ref_init (&group->backend, NULL); - group_find_backend (&self->groups[i], core); + group_find_backend (group, core); } G_OBJECT_CLASS (mixer_endpoint_parent_class)->constructed (object); @@ -168,16 +179,37 @@ mixer_endpoint_finalize (GObject * object) WpMixerEndpoint *self = WP_MIXER_ENDPOINT (object); gint i; - for (i = 0; i < N_STREAMS; i++) { - g_autoptr (WpEndpoint) backend = g_weak_ref_get (&self->groups[i].backend); + for (i = 0; i < self->groups->len; i++) { + struct group *group = &g_array_index (self->groups, struct group, i); + g_autoptr (WpEndpoint) backend = g_weak_ref_get (&group->backend); if (backend) - g_signal_handlers_disconnect_by_data (backend, &self->groups[i]); - g_weak_ref_clear (&self->groups[i].backend); + g_signal_handlers_disconnect_by_data (backend, group); + g_weak_ref_clear (&group->backend); + g_free (group->name); } + g_clear_pointer (&self->groups, g_array_unref); + g_clear_pointer (&self->streams, g_variant_unref); + G_OBJECT_CLASS (mixer_endpoint_parent_class)->finalize (object); } +static void +mixer_endpoint_set_property (GObject * object, guint property_id, + const GValue * value, GParamSpec * pspec) +{ + WpMixerEndpoint *self = WP_MIXER_ENDPOINT (object); + + switch (property_id) { + case PROP_STREAMS: + self->streams = g_value_dup_variant (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + static GVariant * mixer_endpoint_get_control_value (WpEndpoint * ep, guint32 control_id) { @@ -189,12 +221,12 @@ mixer_endpoint_get_control_value (WpEndpoint * ep, guint32 control_id) stream_id = control_id / N_CONTROLS; control_id = control_id % N_CONTROLS; - if (stream_id >= N_STREAMS) { + if (stream_id >= self->groups->len) { g_warning ("Mixer:%p Invalid stream id %u", self, stream_id); return NULL; } - group = &self->groups[stream_id]; + group = &g_array_index (self->groups, struct group, stream_id); backend = g_weak_ref_get (&group->backend); /* if there is no backend, return the default value */ @@ -228,12 +260,12 @@ mixer_endpoint_set_control_value (WpEndpoint * ep, guint32 control_id, stream_id = control_id / N_CONTROLS; control_id = control_id % N_CONTROLS; - if (stream_id >= N_STREAMS) { + if (stream_id >= self->groups->len) { g_warning ("Mixer:%p Invalid stream id %u", self, stream_id); return FALSE; } - group = &self->groups[stream_id]; + group = &g_array_index (self->groups, struct group, stream_id); backend = g_weak_ref_get (&group->backend); if (!backend) { @@ -251,20 +283,29 @@ mixer_endpoint_class_init (WpMixerEndpointClass * klass) GObjectClass *object_class = (GObjectClass *) klass; WpEndpointClass *endpoint_class = (WpEndpointClass *) klass; + object_class->set_property = mixer_endpoint_set_property; object_class->constructed = mixer_endpoint_constructed; object_class->finalize = mixer_endpoint_finalize; endpoint_class->get_control_value = mixer_endpoint_get_control_value; endpoint_class->set_control_value = mixer_endpoint_set_control_value; + + 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_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); } static void -remote_connected (WpRemote *remote, WpRemoteState state, WpCore *core) +remote_connected (WpRemote *remote, WpRemoteState state, GVariant *streams) { + g_autoptr (WpCore) core = wp_remote_get_core (remote); g_autoptr (WpEndpoint) ep = g_object_new (mixer_endpoint_get_type (), "core", core, "name", "Mixer", "media-class", "Mixer/Audio", + "streams", streams, NULL); wp_endpoint_register (ep); } @@ -273,10 +314,14 @@ void wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args) { WpRemote *remote; + GVariant *streams; remote = wp_core_get_global (core, WP_GLOBAL_REMOTE_PIPEWIRE); g_return_if_fail (remote != NULL); - g_signal_connect (remote, "state-changed::connected", - (GCallback) remote_connected, core); + streams = g_variant_lookup_value (args, "streams", G_VARIANT_TYPE ("as")); + + g_signal_connect_data (remote, "state-changed::connected", + (GCallback) remote_connected, streams, (GClosureNotify) g_variant_unref, + 0); } diff --git a/modules/module-pw-alsa-udev.c b/modules/module-pw-alsa-udev.c index a353cad4636d2ce6d5e971748d93be3a11426647..ab29eaed9dda8105568541841cbc6ae4c6320da2 100644 --- a/modules/module-pw-alsa-udev.c +++ b/modules/module-pw-alsa-udev.c @@ -20,6 +20,7 @@ struct impl WpModule *module; WpRemotePipewire *remote_pipewire; GHashTable *registered_endpoints; + GVariant *streams; }; static void @@ -73,6 +74,8 @@ on_node_added(WpRemotePipewire *rp, guint id, guint parent_id, gconstpointer p, "media-class", g_variant_new_string (media_class)); g_variant_builder_add (&b, "{sv}", "global-id", g_variant_new_uint32 (id)); + g_variant_builder_add (&b, "{sv}", + "streams", impl->streams); endpoint_props = g_variant_builder_end (&b); /* Create the endpoint async */ @@ -110,6 +113,8 @@ module_destroy (gpointer data) 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); } @@ -134,6 +139,8 @@ wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args) impl->remote_pipewire = rp; 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); diff --git a/modules/module-pw-audio-softdsp-endpoint.c b/modules/module-pw-audio-softdsp-endpoint.c index 49a5c6c0d066cbacb927262103136c133926d8f8..496e130029673dcdf7c291393cc65e4098e57d6e 100644 --- a/modules/module-pw-audio-softdsp-endpoint.c +++ b/modules/module-pw-audio-softdsp-endpoint.c @@ -19,15 +19,22 @@ #include <spa/pod/builder.h> #include <spa/param/props.h> +#include "module-pw-audio-softdsp-endpoint/dsp.h" + #define MIN_QUANTUM_SIZE 64 #define MAX_QUANTUM_SIZE 1024 +#define CONTROL_SELECTED 0 struct _WpPwAudioSoftdspEndpoint { WpEndpoint parent; - /* The global-id this endpoint refers to */ + /* Properties */ guint global_id; + GVariant *streams; + + guint stream_count; + gboolean selected; /* The task to signal the endpoint is initialized */ GTask *init_task; @@ -35,40 +42,22 @@ struct _WpPwAudioSoftdspEndpoint /* The remote pipewire */ WpRemotePipewire *remote_pipewire; - /* Handler */ - gulong proxy_dsp_done_handler_id; - - /* temporary method to select which endpoint - * is going to be the default input/output */ - gboolean selected; - /* Direction */ enum pw_direction direction; /* Proxies */ WpProxyNode *proxy_node; WpProxyPort *proxy_port; - WpProxyNode *proxy_dsp; - GPtrArray *proxies_dsp_port; - - /* Volume */ - gfloat master_volume; - gboolean master_mute; - /* DSP */ - struct spa_hook dsp_listener; - struct pw_proxy *link_proxy; + /* Audio Dsp */ + WpPwAudioDsp *converter; + GPtrArray *dsps; }; enum { PROP_0, PROP_GLOBAL_ID, -}; - -enum { - CONTROL_VOLUME = 0, - CONTROL_MUTE, - CONTROL_SELECTED, + PROP_STREAMS, }; static GAsyncInitableIface *wp_endpoint_parent_interface = NULL; @@ -82,174 +71,27 @@ G_DEFINE_TYPE_WITH_CODE (WpPwAudioSoftdspEndpoint, endpoint, WP_TYPE_ENDPOINT, G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, wp_endpoint_async_initable_init)) -static void -proxies_dsp_port_foreach_func(gpointer data, gpointer user_data) -{ - GVariantBuilder *b = user_data; - g_variant_builder_add (b, "t", data); -} - static gboolean endpoint_prepare_link (WpEndpoint * ep, guint32 stream_id, WpEndpointLink * link, GVariant ** properties, GError ** error) { WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep); - const struct pw_node_info *dsp_info = NULL; - GVariantBuilder b, *b_ports; - GVariant *v_ports; - - /* Get the dsp info */ - dsp_info = wp_proxy_node_get_info(self->proxy_dsp); - g_return_val_if_fail (dsp_info, FALSE); - - /* Create a variant array with all the ports */ - b_ports = g_variant_builder_new (G_VARIANT_TYPE ("at")); - g_ptr_array_foreach(self->proxies_dsp_port, proxies_dsp_port_foreach_func, - b_ports); - v_ports = g_variant_builder_end (b_ports); - - /* Set the properties */ - g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT); - g_variant_builder_add (&b, "{sv}", "node-id", - g_variant_new_uint32 (dsp_info->id)); - g_variant_builder_add (&b, "{sv}", "ports", v_ports); - *properties = g_variant_builder_end (&b); - - return TRUE; -} + WpPwAudioDsp *stream = NULL; -static void -on_dsp_running(WpPwAudioSoftdspEndpoint *self) -{ - struct pw_properties *props; - const struct pw_node_info *node_info = NULL; - const struct pw_node_info *dsp_info = NULL; - - /* Return if the node has already been linked */ - g_return_if_fail (!self->link_proxy); - - /* Get the node info */ - node_info = wp_proxy_node_get_info(self->proxy_node); - g_return_if_fail (node_info); - - /* Get the dsp info */ - dsp_info = wp_proxy_node_get_info(self->proxy_dsp); - g_return_if_fail (dsp_info); - - /* Create new properties */ - props = pw_properties_new(NULL, NULL); - - /* Set the new properties */ - pw_properties_set(props, PW_LINK_PROP_PASSIVE, "true"); - if (self->direction == PW_DIRECTION_OUTPUT) { - pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", dsp_info->id); - pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1); - pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", node_info->id); - pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1); - } else { - pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", node_info->id); - pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1); - pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", dsp_info->id); - pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1); - } - - g_debug ("%p linking DSP to node", self); - - /* Create the link */ - self->link_proxy = wp_remote_pipewire_create_object(self->remote_pipewire, - "link-factory", PW_TYPE_INTERFACE_Link, &props->dict); - - /* Clean up */ - pw_properties_free(props); -} - -static void -on_dsp_idle (WpPwAudioSoftdspEndpoint *self) -{ - if (self->link_proxy != NULL) { - g_debug ("%p unlinking DSP from node", self); - pw_proxy_destroy (self->link_proxy); - self->link_proxy = NULL; - } -} - -static void -dsp_node_event_info (void *data, const struct pw_node_info *info) -{ - WpPwAudioSoftdspEndpoint *self = data; - - /* Handle the different states */ - switch (info->state) { - case PW_NODE_STATE_IDLE: - on_dsp_idle (self); - break; - case PW_NODE_STATE_RUNNING: - on_dsp_running (self); - break; - case PW_NODE_STATE_SUSPENDED: - break; - default: - break; - } -} + /* Make sure the stream Id is valid */ + g_return_val_if_fail(stream_id < self->dsps->len, FALSE); -static void -dsp_node_event_param (void *object, int seq, uint32_t id, - uint32_t index, uint32_t next, const struct spa_pod *param) -{ - WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (object); + /* Make sure the stream is valid */ + stream = g_ptr_array_index (self->dsps, stream_id); + g_return_val_if_fail(stream, FALSE); - switch (id) { - case SPA_PARAM_Props: - { - struct spa_pod_prop *prop; - struct spa_pod_object *obj = (struct spa_pod_object *) param; - float volume = self->master_volume; - bool mute = self->master_mute; - - SPA_POD_OBJECT_FOREACH(obj, prop) { - switch (prop->key) { - case SPA_PROP_volume: - spa_pod_get_float(&prop->value, &volume); - break; - case SPA_PROP_mute: - spa_pod_get_bool(&prop->value, &mute); - break; - default: - break; - } - } - - g_debug ("WpEndpoint:%p param event, vol:(%lf -> %f) mute:(%d -> %d)", - self, self->master_volume, volume, self->master_mute, mute); - - if (self->master_volume != volume) { - self->master_volume = volume; - wp_endpoint_notify_control_value (WP_ENDPOINT (self), CONTROL_VOLUME); - } - if (self->master_mute != mute) { - self->master_mute = mute; - wp_endpoint_notify_control_value (WP_ENDPOINT (self), CONTROL_MUTE); - } - - break; - } - default: - break; - } + /* Prepare the link */ + return wp_pw_audio_dsp_prepare_link (stream, properties, error); } -static const struct pw_node_proxy_events dsp_node_events = { - PW_VERSION_NODE_PROXY_EVENTS, - .info = dsp_node_event_info, - .param = dsp_node_event_param, -}; - static void -on_proxy_dsp_done(WpProxy *proxy, gpointer data) +finish_endpoint_creation(WpPwAudioSoftdspEndpoint *self) { - WpPwAudioSoftdspEndpoint *self = data; - /* Don't do anything if the endpoint has already been initialized */ if (!self->init_task) return; @@ -260,90 +102,79 @@ on_proxy_dsp_done(WpProxy *proxy, gpointer data) } static void -on_proxy_dsp_created(GObject *initable, GAsyncResult *res, gpointer data) +on_audio_dsp_stream_created(GObject *initable, GAsyncResult *res, gpointer data) { WpPwAudioSoftdspEndpoint *self = data; - struct pw_node_proxy *dsp_proxy = NULL; - const struct spa_audio_info_raw *port_format; - struct spa_audio_info_raw format; - uint8_t buf[1024]; - struct spa_pod_builder pod_builder = { 0, }; - struct spa_pod *param; - - /* Get the proxy dsp */ - self->proxy_dsp = wp_proxy_node_new_finish(initable, res, NULL); - g_return_if_fail (self->proxy_dsp); - - /* Add a custom dsp listener */ - dsp_proxy = wp_proxy_get_pw_proxy(WP_PROXY(self->proxy_dsp)); - g_return_if_fail (dsp_proxy); - pw_node_proxy_add_listener(dsp_proxy, &self->dsp_listener, - &dsp_node_events, self); - - /* Emit the props param */ - pw_node_proxy_enum_params (dsp_proxy, 0, SPA_PARAM_Props, 0, -1, NULL); - - /* Get the port format */ - port_format = wp_proxy_port_get_format(self->proxy_port); - g_return_if_fail (port_format); - format = *port_format; - - /* Build the param profile */ - spa_pod_builder_init(&pod_builder, buf, sizeof(buf)); - param = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format, &format); - param = spa_pod_builder_add_object(&pod_builder, - SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, - SPA_PARAM_PROFILE_direction, SPA_POD_Id(pw_direction_reverse(self->direction)), - SPA_PARAM_PROFILE_format, SPA_POD_Pod(param)); - - /* Set the param profile to emit the dsp ports */ - pw_node_proxy_set_param(dsp_proxy, SPA_PARAM_Profile, 0, param); + WpPwAudioDsp *dsp = NULL; + guint stream_id = 0; + g_autofree gchar *name = NULL; + + /* Get the stream */ + dsp = wp_pw_audio_dsp_new_finish(initable, res, NULL); + g_return_if_fail (dsp); + + /* Get the stream id */ + g_object_get (dsp, "id", &stream_id, "name", &name, NULL); + g_return_if_fail (stream_id >= 0); + + /* Set the streams */ + g_ptr_array_insert (self->dsps, stream_id, dsp); + + g_debug ("%s:%p Created stream %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 -emit_audio_dsp_node (WpPwAudioSoftdspEndpoint *self) +on_audio_dsp_converter_created(GObject *initable, GAsyncResult *res, + gpointer data) { - struct pw_properties *props; - const char *dsp_name = NULL; - struct pw_node_proxy *dsp_proxy = NULL; - const struct pw_node_info *node_info; - - /* Get the node info */ - node_info = wp_proxy_node_get_info(self->proxy_node); - g_return_if_fail (node_info); - - /* Get the properties */ - props = pw_properties_new_dict(node_info->props); - g_return_if_fail (props); - - /* Get the DSP name */ - dsp_name = pw_properties_get(props, "device.nick"); - if (!dsp_name) - dsp_name = node_info->name; - - /* Set the properties */ - pw_properties_set(props, "audio-dsp.name", dsp_name); - pw_properties_setf(props, "audio-dsp.direction", "%d", self->direction); - pw_properties_setf(props, "audio-dsp.maxbuffer", "%ld", - MAX_QUANTUM_SIZE * sizeof(float)); - - /* Create the proxy dsp async */ - dsp_proxy = wp_remote_pipewire_create_object(self->remote_pipewire, - "audio-dsp", PW_TYPE_INTERFACE_Node, &props->dict); - wp_proxy_node_new(pw_proxy_get_id((struct pw_proxy *)dsp_proxy), dsp_proxy, - on_proxy_dsp_created, self); - - /* Clean up */ - pw_properties_free(props); + WpPwAudioSoftdspEndpoint *self = data; + g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self)); + const struct pw_node_info *target = NULL; + const struct spa_audio_info_raw *format = NULL; + GVariantDict d; + GVariantIter iter; + const gchar *stream; + int i; + + /* Get the proxy dsp converter */ + self->converter = wp_pw_audio_dsp_new_finish(initable, res, NULL); + g_return_if_fail (self->converter); + + /* Get the target and format */ + target = wp_pw_audio_dsp_get_info (self->converter); + g_return_if_fail (target); + g_object_get (self->converter, "format", &format, NULL); + g_return_if_fail (format); + + /* Create the audio dsp streams */ + g_variant_iter_init (&iter, self->streams); + for (i = 0; g_variant_iter_next (&iter, "&s", &stream); i++) { + wp_pw_audio_dsp_new (WP_ENDPOINT(self), i, stream, self->direction, + FALSE, target, format, on_audio_dsp_stream_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 on_proxy_node_created(GObject *initable, GAsyncResult *res, gpointer data) { WpPwAudioSoftdspEndpoint *self = data; - GVariantDict d; + g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self)); g_autofree gchar *name = NULL; const struct spa_dict *props; + const struct pw_node_info *target = NULL; + const struct spa_audio_info_raw *format = NULL; /* Get the proxy node */ self->proxy_node = wp_proxy_node_new_finish(initable, res, NULL); @@ -358,38 +189,16 @@ on_proxy_node_created(GObject *initable, GAsyncResult *res, gpointer data) wp_proxy_node_get_info (self->proxy_node)->id); g_object_set (self, "name", name, NULL); - /* Emit the audio DSP node */ - emit_audio_dsp_node(self); - - g_variant_dict_init (&d, NULL); - g_variant_dict_insert (&d, "id", "u", 0); - g_variant_dict_insert (&d, "name", "s", "default"); - wp_endpoint_register_stream (WP_ENDPOINT (self), g_variant_dict_end (&d)); - - self->master_volume = 1.0; - g_variant_dict_init (&d, NULL); - g_variant_dict_insert (&d, "id", "u", CONTROL_VOLUME); - g_variant_dict_insert (&d, "name", "s", "volume"); - g_variant_dict_insert (&d, "type", "s", "d"); - g_variant_dict_insert (&d, "range", "(dd)", 0.0, 1.0); - g_variant_dict_insert (&d, "default-value", "d", self->master_volume); - wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d)); - - self->master_mute = FALSE; - g_variant_dict_init (&d, NULL); - g_variant_dict_insert (&d, "id", "u", CONTROL_MUTE); - g_variant_dict_insert (&d, "name", "s", "mute"); - g_variant_dict_insert (&d, "type", "s", "b"); - g_variant_dict_insert (&d, "default-value", "b", self->master_mute); - wp_endpoint_register_control (WP_ENDPOINT (self), g_variant_dict_end (&d)); - - 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)); + /* Create the converter proxy */ + target = wp_proxy_node_get_info (self->proxy_node); + g_return_if_fail (target); + format = wp_proxy_port_get_format (self->proxy_port); + g_return_if_fail (format); + /* TODO: For now we create convert as a stream because convert mode does not + * generate any ports, not sure why */ + wp_pw_audio_dsp_new (WP_ENDPOINT(self), WP_STREAM_ID_NONE, "master", + self->direction, TRUE, target, format, on_audio_dsp_converter_created, + self); } static void @@ -410,11 +219,16 @@ on_proxy_port_created(GObject *initable, GAsyncResult *res, gpointer data) } static void -handle_node_port(WpPwAudioSoftdspEndpoint *self, guint id, guint parent_id, - const struct spa_dict *props) +on_port_added(WpRemotePipewire *rp, guint id, guint parent_id, gconstpointer p, + gpointer d) { + WpPwAudioSoftdspEndpoint *self = d; struct pw_port_proxy *port_proxy = NULL; + /* Check if it is a node port and handle it */ + if (self->global_id != parent_id) + return; + /* Alsa nodes should have 1 port only, so make sure proxy_port is not set */ if (self->proxy_port != 0) return; @@ -426,74 +240,12 @@ handle_node_port(WpPwAudioSoftdspEndpoint *self, guint id, guint parent_id, wp_proxy_port_new(id, port_proxy, on_proxy_port_created, self); } -static void -on_proxy_dsp_port_created(GObject *initable, GAsyncResult *res, gpointer data) -{ - WpPwAudioSoftdspEndpoint *self = data; - WpProxyPort *proxy_dsp_port = NULL; - - /* Get the proxy dsp port */ - proxy_dsp_port = wp_proxy_port_new_finish(initable, res, NULL); - g_return_if_fail (proxy_dsp_port); - - /* Add the proxy dsp port to the array */ - g_return_if_fail (self->proxies_dsp_port); - g_ptr_array_add(self->proxies_dsp_port, proxy_dsp_port); - - /* Register a callback to know when all the dsp ports have been emitted */ - if (!self->proxy_dsp_done_handler_id) { - self->proxy_dsp_done_handler_id = g_signal_connect_object(self->proxy_dsp, - "done", (GCallback)on_proxy_dsp_done, self, 0); - wp_proxy_sync (WP_PROXY(self->proxy_dsp)); - } -} - -static void -handle_dsp_port(WpPwAudioSoftdspEndpoint *self, guint id, guint parent_id, - const struct spa_dict *props) -{ - struct pw_port_proxy *port_proxy = NULL; - - /* Create the proxy dsp port async */ - port_proxy = wp_remote_pipewire_proxy_bind (self->remote_pipewire, id, - PW_TYPE_INTERFACE_Port); - g_return_if_fail(port_proxy); - wp_proxy_port_new(id, port_proxy, on_proxy_dsp_port_created, self); -} - -static void -on_port_added(WpRemotePipewire *rp, guint id, guint parent_id, gconstpointer p, - gpointer d) -{ - WpPwAudioSoftdspEndpoint *self = d; - const struct spa_dict *props = p; - const struct pw_node_info *dsp_info = NULL; - - /* Check if it is a node port and handle it */ - if (self->global_id == parent_id) { - handle_node_port(self, id, parent_id, props); - return; - } - - /* Otherwise, check if it is a dsp port and handle it */ - if (!self->proxy_dsp) - return; - dsp_info = wp_proxy_node_get_info (self->proxy_dsp); - if (!dsp_info || dsp_info->id != parent_id) - return; - handle_dsp_port(self, id, parent_id, props); -} - static void endpoint_finalize (GObject * object) { WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (object); - /* Destroy the proxies port */ - if (self->proxies_dsp_port) { - g_ptr_array_free(self->proxies_dsp_port, TRUE); - self->proxies_dsp_port = NULL; - } + g_clear_pointer(&self->streams, g_variant_unref); /* Destroy the proxy node */ g_clear_object(&self->proxy_node); @@ -501,8 +253,11 @@ endpoint_finalize (GObject * object) /* Destroy the proxy port */ g_clear_object(&self->proxy_port); - /* Destroy the proxy dsp */ - g_clear_object(&self->proxy_dsp); + /* Destroy the proxy dsp converter */ + g_clear_object(&self->converter); + + /* Destroy all the proxy dsp streams */ + g_clear_pointer (&self->dsps, g_ptr_array_unref); /* Destroy the done task */ g_clear_object(&self->init_task); @@ -520,6 +275,9 @@ endpoint_set_property (GObject * object, guint property_id, case PROP_GLOBAL_ID: self->global_id = g_value_get_uint(value); break; + case PROP_STREAMS: + self->streams = g_value_dup_variant(value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -536,6 +294,9 @@ endpoint_get_property (GObject * object, guint property_id, case PROP_GLOBAL_ID: g_value_set_uint (value, self->global_id); break; + case PROP_STREAMS: + g_value_set_variant (value, self->streams); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; @@ -543,83 +304,52 @@ endpoint_get_property (GObject * object, guint property_id, } static GVariant * -endpoint_get_control_value (WpEndpoint * ep, guint32 control_id) +endpoint_get_control_value (WpEndpoint * ep, guint32 id) { WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep); + guint stream_id, control_id; + WpPwAudioDsp *stream = NULL; - switch (control_id) { - case CONTROL_VOLUME: - return g_variant_new_double (self->master_volume); - case CONTROL_MUTE: - return g_variant_new_boolean (self->master_mute); - case CONTROL_SELECTED: - return g_variant_new_boolean (self->selected); - default: - g_warning ("Unknown control id %u", control_id); - return NULL; - } + if (id == CONTROL_SELECTED) + return g_variant_new_boolean (self->selected); + + wp_pw_audio_dsp_id_decode (id, &stream_id, &control_id); + + /* Check if it is the master stream */ + if (stream_id == WP_STREAM_ID_NONE) + return wp_pw_audio_dsp_get_control_value (self->converter, control_id); + + /* Otherwise get the stream_id and control_id */ + g_return_val_if_fail (stream_id < self->dsps->len, NULL); + stream = g_ptr_array_index (self->dsps, stream_id); + g_return_val_if_fail (stream, NULL); + return wp_pw_audio_dsp_get_control_value (stream, control_id); } static gboolean -endpoint_set_control_value (WpEndpoint * ep, guint32 control_id, - GVariant * value) +endpoint_set_control_value (WpEndpoint * ep, guint32 id, GVariant * value) { WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep); - char buf[1024]; - struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); - struct pw_node_proxy *dsp_proxy = NULL; - float volume; - bool mute; - - /* Get the pipewire dsp proxy */ - g_return_val_if_fail (self->proxy_dsp, FALSE); - dsp_proxy = wp_proxy_get_pw_proxy (WP_PROXY(self->proxy_dsp)); - g_return_val_if_fail (dsp_proxy, FALSE); - - switch (control_id) { - case CONTROL_VOLUME: - volume = g_variant_get_double (value); - - g_debug("WpEndpoint:%p set volume control (%u) value, vol:%f", self, - control_id, volume); - - pw_node_proxy_set_param (dsp_proxy, - SPA_PARAM_Props, 0, - spa_pod_builder_add_object (&b, - SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, - SPA_PROP_volume, SPA_POD_Float(volume), - NULL)); - pw_node_proxy_enum_params (dsp_proxy, 0, SPA_PARAM_Props, 0, -1, - NULL); - break; - - case CONTROL_MUTE: - mute = g_variant_get_boolean (value); - - g_debug("WpEndpoint:%p set mute control (%u) value, mute:%d", self, - control_id, mute); - - pw_node_proxy_set_param (dsp_proxy, - SPA_PARAM_Props, 0, - spa_pod_builder_add_object (&b, - SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, - SPA_PROP_mute, SPA_POD_Bool(mute), - NULL)); - pw_node_proxy_enum_params (dsp_proxy, 0, SPA_PARAM_Props, 0, -1, - NULL); - break; - - case CONTROL_SELECTED: - self->selected = g_variant_get_boolean (value); - wp_endpoint_notify_control_value (ep, CONTROL_SELECTED); - break; - - default: - g_warning ("Unknown control id %u", control_id); - return FALSE; + guint stream_id, control_id; + WpPwAudioDsp *stream = NULL; + + if (id == CONTROL_SELECTED) { + self->selected = g_variant_get_boolean (value); + wp_endpoint_notify_control_value (ep, CONTROL_SELECTED); + return TRUE; } - return TRUE; + wp_pw_audio_dsp_id_decode (id, &stream_id, &control_id); + + /* Check if it is the master stream */ + if (stream_id == WP_STREAM_ID_NONE) + return wp_pw_audio_dsp_set_control_value (self->converter, control_id, value); + + /* Otherwise get the stream_id and control_id */ + g_return_val_if_fail (stream_id < self->dsps->len, FALSE); + stream = g_ptr_array_index (self->dsps, stream_id); + g_return_val_if_fail (stream, FALSE); + return wp_pw_audio_dsp_set_control_value (stream, control_id, value); } static void @@ -629,13 +359,11 @@ wp_endpoint_init_async (GAsyncInitable *initable, int io_priority, WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (initable); g_autoptr (WpCore) core = wp_endpoint_get_core(WP_ENDPOINT(self)); const gchar *media_class = wp_endpoint_get_media_class (WP_ENDPOINT (self)); + GVariantDict d; /* Create the async task */ self->init_task = g_task_new (initable, cancellable, callback, data); - /* Init the proxies_dsp_port array */ - self->proxies_dsp_port = g_ptr_array_new_full(4, (GDestroyNotify)g_object_unref); - /* Set the direction */ if (g_str_has_suffix (media_class, "Source")) self->direction = PW_DIRECTION_INPUT; @@ -650,6 +378,15 @@ wp_endpoint_init_async (GAsyncInitable *initable, int io_priority, g_signal_connect_object(self->remote_pipewire, "global-added::port", (GCallback)on_port_added, self, 0); + /* 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); @@ -670,6 +407,7 @@ wp_endpoint_async_initable_init (gpointer iface, gpointer iface_data) static void endpoint_init (WpPwAudioSoftdspEndpoint * self) { + self->dsps = g_ptr_array_new_with_free_func (g_object_unref); } static void @@ -691,6 +429,12 @@ endpoint_class_init (WpPwAudioSoftdspEndpointClass * klass) g_param_spec_uint ("global-id", "global-id", "The global Id this endpoint refers to", 0, G_MAXUINT, 0, 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)); } void @@ -700,6 +444,7 @@ endpoint_factory (WpFactory * factory, GType type, GVariant * properties, g_autoptr (WpCore) core = NULL; const gchar *media_class; guint global_id; + g_autoptr (GVariant) streams = NULL; /* Make sure the type is correct */ g_return_if_fail(type == WP_TYPE_ENDPOINT); @@ -713,6 +458,9 @@ endpoint_factory (WpFactory * factory, GType type, GVariant * properties, return; if (!g_variant_lookup (properties, "global-id", "u", &global_id)) return; + if (!(streams = g_variant_lookup_value (properties, "streams", + G_VARIANT_TYPE ("as")))) + return; /* Create and return the softdsp endpoint object */ g_async_initable_new_async ( @@ -720,6 +468,7 @@ endpoint_factory (WpFactory * factory, GType type, GVariant * properties, "core", core, "media-class", media_class, "global-id", global_id, + "streams", streams, NULL); } diff --git a/modules/module-pw-audio-softdsp-endpoint/dsp.c b/modules/module-pw-audio-softdsp-endpoint/dsp.c new file mode 100644 index 0000000000000000000000000000000000000000..0c0f6b18915a270e699d29f3ba9027f1b9dda812 --- /dev/null +++ b/modules/module-pw-audio-softdsp-endpoint/dsp.c @@ -0,0 +1,740 @@ +/* WirePlumber + * + * Copyright © 2019 Collabora Ltd. + * @author Julian Bouzas <julian.bouzas@collabora.com> + * + * SPDX-License-Identifier: MIT + */ + +#include <pipewire/pipewire.h> +#include <spa/param/audio/format-utils.h> +#include <spa/pod/builder.h> +#include <spa/param/props.h> + +#include "dsp.h" + +#define MIN_QUANTUM_SIZE 64 +#define MAX_QUANTUM_SIZE 1024 + +enum { + PROP_0, + PROP_ENDPOINT, + PROP_ID, + PROP_NAME, + PROP_DIRECTION, + PROP_CONVERT, + PROP_TARGET, + PROP_FORMAT, +}; + +enum { + CONTROL_VOLUME = 0, + CONTROL_MUTE, + N_CONTROLS, +}; + +struct _WpPwAudioDsp +{ + GObject parent; + + /* The task to signal the audio dsp is initialized */ + GTask *init_task; + + /* The remote pipewire */ + WpRemotePipewire *remote_pipewire; + + /* Props */ + GWeakRef endpoint; + guint id; + gchar *name; + enum pw_direction direction; + gboolean convert; + const struct pw_node_info *target; + const struct spa_audio_info_raw *format; + + /* All ports handled by the port added callback */ + GHashTable *handled_ports; + + /* Proxies */ + WpProxyNode *proxy; + GPtrArray *port_proxies; + struct pw_proxy *link_proxy; + + /* Listener */ + struct spa_hook listener; + + /* Volume */ + gfloat volume; + gboolean mute; +}; + +static void wp_pw_audio_dsp_async_initable_init (gpointer iface, + gpointer iface_data); + +G_DEFINE_TYPE_WITH_CODE (WpPwAudioDsp, wp_pw_audio_dsp, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, + wp_pw_audio_dsp_async_initable_init)) + +guint +wp_pw_audio_dsp_id_encode (guint stream_id, guint control_id) +{ + g_return_val_if_fail (control_id < N_CONTROLS, 0); + + /* encode NONE as 0 and everything else with +1 */ + /* NONE is MAX_UINT, so +1 will do the trick */ + stream_id += 1; + + /* Encode the stream and control Ids. The first ID is reserved + * for the "selected" control, registered in the endpoint */ + return 1 + (stream_id * N_CONTROLS) + control_id; +} + +void +wp_pw_audio_dsp_id_decode (guint id, guint *stream_id, guint *control_id) +{ + guint s_id, c_id; + + g_return_if_fail (id >= 1); + id -= 1; + + /* Decode the stream and control Ids */ + s_id = (id / N_CONTROLS) - 1; + c_id = id % N_CONTROLS; + + /* Set the output params */ + if (stream_id) + *stream_id = s_id; + if (control_id) + *control_id = c_id; +} + +static void +register_controls (WpPwAudioDsp * self) +{ + GVariantDict d; + g_autoptr (WpEndpoint) ep = g_weak_ref_get (&self->endpoint); + g_return_if_fail (ep); + + /* Register the volume control */ + g_variant_dict_init (&d, NULL); + g_variant_dict_insert (&d, "id", "u", + wp_pw_audio_dsp_id_encode (self->id, CONTROL_VOLUME)); + if (self->id != WP_STREAM_ID_NONE) + g_variant_dict_insert (&d, "stream-id", "u", self->id); + g_variant_dict_insert (&d, "name", "s", "volume"); + g_variant_dict_insert (&d, "type", "s", "d"); + g_variant_dict_insert (&d, "range", "(dd)", 0.0, 1.0); + g_variant_dict_insert (&d, "default-value", "d", self->volume); + wp_endpoint_register_control (ep, g_variant_dict_end (&d)); + + /* Register the mute control */ + g_variant_dict_init (&d, NULL); + g_variant_dict_insert (&d, "id", "u", + wp_pw_audio_dsp_id_encode (self->id, CONTROL_MUTE)); + if (self->id != WP_STREAM_ID_NONE) + g_variant_dict_insert (&d, "stream-id", "u", self->id); + g_variant_dict_insert (&d, "name", "s", "mute"); + g_variant_dict_insert (&d, "type", "s", "b"); + g_variant_dict_insert (&d, "default-value", "b", self->mute); + wp_endpoint_register_control (ep, g_variant_dict_end (&d)); +} + +static void +on_audio_dsp_port_created(GObject *initable, GAsyncResult *res, + gpointer data) +{ + WpPwAudioDsp *self = data; + WpProxyPort *port_proxy = NULL; + + /* Get the proxy port */ + port_proxy = wp_proxy_port_new_finish(initable, res, NULL); + g_return_if_fail (port_proxy); + + /* Add the proxy port to the array */ + g_return_if_fail (self->port_proxies); + g_ptr_array_add(self->port_proxies, port_proxy); +} + +static void +handled_ports_foreach_func (gpointer key, gpointer value, gpointer data) +{ + WpPwAudioDsp *self = data; + const struct pw_node_info *dsp_info = NULL; + struct pw_port_proxy *port_proxy = NULL; + const guint id = GPOINTER_TO_INT (key); + const guint parent_id = GPOINTER_TO_INT (value); + + /* Get the dsp info */ + g_return_if_fail (self->proxy); + dsp_info = wp_proxy_node_get_info(self->proxy); + g_return_if_fail (dsp_info); + + /* Skip ports that are not owned by this DSP */ + if (dsp_info->id != parent_id) + return; + + /* Create the audio dsp port async */ + port_proxy = wp_remote_pipewire_proxy_bind (self->remote_pipewire, id, + PW_TYPE_INTERFACE_Port); + g_return_if_fail(port_proxy); + wp_proxy_port_new(id, port_proxy, on_audio_dsp_port_created, self); +} + +static void +on_audio_dsp_done(WpProxy *proxy, gpointer data) +{ + WpPwAudioDsp *self = data; + + g_return_if_fail (self->proxy); + + /* Don't do anything if the endpoint has already been initialized */ + if (!self->init_task) + return; + + /* Create the proxis and sync to trigger this function again */ + if (self->port_proxies->len == 0) { + g_hash_table_foreach (self->handled_ports, handled_ports_foreach_func, self); + wp_proxy_sync (WP_PROXY(self->proxy)); + return; + } + + /* Register the controls */ + register_controls (self); + + /* Finish the creation of the audio dsp */ + g_task_return_boolean (self->init_task, TRUE); + g_clear_object(&self->init_task); +} + +static void +on_audio_dsp_port_added(WpRemotePipewire *rp, guint id, guint parent_id, + gconstpointer p, gpointer d) +{ + WpPwAudioDsp *self = d; + + /* Add the port to the map if it is not already there */ + if (!g_hash_table_contains (self->handled_ports, GUINT_TO_POINTER (id))) + g_hash_table_insert (self->handled_ports, GUINT_TO_POINTER(id), + GUINT_TO_POINTER(parent_id)); +} + +static void +on_audio_dsp_running(WpPwAudioDsp *self) +{ + struct pw_properties *props; + const struct pw_node_info *dsp_info = NULL; + + /* Return if the node has already been linked */ + if (self->link_proxy) + return; + + /* Get the dsp info */ + dsp_info = wp_proxy_node_get_info(self->proxy); + g_return_if_fail (dsp_info); + + /* Create new properties */ + props = pw_properties_new(NULL, NULL); + + /* Set the new properties */ + pw_properties_set(props, PW_LINK_PROP_PASSIVE, "true"); + if (self->direction == PW_DIRECTION_OUTPUT) { + pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", dsp_info->id); + pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1); + pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", self->target->id); + pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1); + } else { + pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", self->target->id); + pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1); + pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", dsp_info->id); + pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1); + } + + g_debug ("%p linking DSP to node", self); + + /* Create the link */ + self->link_proxy = wp_remote_pipewire_create_object(self->remote_pipewire, + "link-factory", PW_TYPE_INTERFACE_Link, &props->dict); + + /* Clean up */ + pw_properties_free(props); +} + +static void +on_audio_dsp_idle (WpPwAudioDsp *self) +{ + if (self->link_proxy != NULL) { + pw_proxy_destroy (self->link_proxy); + self->link_proxy = NULL; + } +} + +static void +audio_dsp_event_info (void *data, const struct pw_node_info *info) +{ + WpPwAudioDsp *self = data; + + /* Handle the different states */ + switch (info->state) { + case PW_NODE_STATE_IDLE: + on_audio_dsp_idle (self); + break; + case PW_NODE_STATE_RUNNING: + on_audio_dsp_running (self); + break; + case PW_NODE_STATE_SUSPENDED: + break; + default: + break; + } +} + +static void +audio_dsp_event_param (void *object, int seq, uint32_t id, + uint32_t index, uint32_t next, const struct spa_pod *param) +{ + WpPwAudioDsp *self = WP_PW_AUDIO_DSP (object); + g_autoptr (WpEndpoint) ep = g_weak_ref_get (&self->endpoint); + + switch (id) { + case SPA_PARAM_Props: + { + struct spa_pod_prop *prop; + struct spa_pod_object *obj = (struct spa_pod_object *) param; + float volume = self->volume; + bool mute = self->mute; + + SPA_POD_OBJECT_FOREACH(obj, prop) { + switch (prop->key) { + case SPA_PROP_volume: + spa_pod_get_float(&prop->value, &volume); + break; + case SPA_PROP_mute: + spa_pod_get_bool(&prop->value, &mute); + break; + default: + break; + } + } + + g_debug ("WpPwAudioDsp:%p param event, vol:(%lf -> %f) mute:(%d -> %d)", + self, self->volume, volume, self->mute, mute); + + if (self->volume != volume) { + self->volume = volume; + wp_endpoint_notify_control_value (ep, + wp_pw_audio_dsp_id_encode (self->id, CONTROL_VOLUME)); + } + if (self->mute != mute) { + self->mute = mute; + wp_endpoint_notify_control_value (ep, + wp_pw_audio_dsp_id_encode (self->id, CONTROL_MUTE)); + } + + break; + } + default: + break; + } +} + +static const struct pw_node_proxy_events audio_dsp_proxy_events = { + PW_VERSION_NODE_PROXY_EVENTS, + .info = audio_dsp_event_info, + .param = audio_dsp_event_param, +}; + +static void +on_audio_dsp_proxy_created(GObject *initable, GAsyncResult *res, + gpointer data) +{ + WpPwAudioDsp *self = data; + struct pw_node_proxy *pw_proxy = NULL; + struct spa_audio_info_raw format; + uint8_t buf[1024]; + struct spa_pod_builder pod_builder = { 0, }; + struct spa_pod *param; + + /* Get the audio dsp proxy */ + self->proxy = wp_proxy_node_new_finish(initable, res, NULL); + g_return_if_fail (self->proxy); + + /* Add a custom dsp listener */ + pw_proxy = wp_proxy_get_pw_proxy(WP_PROXY(self->proxy)); + g_return_if_fail (pw_proxy); + pw_node_proxy_add_listener(pw_proxy, &self->listener, + &audio_dsp_proxy_events, self); + + /* Emit the props param */ + pw_node_proxy_enum_params (pw_proxy, 0, SPA_PARAM_Props, 0, -1, NULL); + + if (!self->convert) { + /* Get the port format */ + g_return_if_fail (self->format); + format = *self->format; + + /* Emit the ports */ + spa_pod_builder_init(&pod_builder, buf, sizeof(buf)); + param = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format, &format); + param = spa_pod_builder_add_object(&pod_builder, + SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile, + SPA_PARAM_PROFILE_direction, SPA_POD_Id(pw_direction_reverse(self->direction)), + SPA_PARAM_PROFILE_format, SPA_POD_Pod(param)); + pw_node_proxy_set_param(pw_proxy, SPA_PARAM_Profile, 0, param); + } + + /* Register a callback to know when all the dsp ports have been emitted */ + g_signal_connect_object(self->proxy, "done", (GCallback)on_audio_dsp_done, + self, 0); + wp_proxy_sync (WP_PROXY(self->proxy)); +} + +static void +wp_pw_audio_dsp_finalize (GObject * object) +{ + WpPwAudioDsp *self = WP_PW_AUDIO_DSP (object); + + /* Props */ + g_weak_ref_clear (&self->endpoint); + g_free (self->name); + + /* Destroy the init task */ + g_clear_object(&self->init_task); + + /* Destroy the handled ports map */ + g_hash_table_unref(self->handled_ports); + self->handled_ports = NULL; + + /* Destroy the proxy dsp */ + g_clear_object(&self->proxy); + + /* Destroy the proxies port */ + if (self->port_proxies) { + g_ptr_array_free(self->port_proxies, TRUE); + self->port_proxies = NULL; + } + + G_OBJECT_CLASS (wp_pw_audio_dsp_parent_class)->finalize (object); +} + +static void +wp_pw_audio_dsp_set_property (GObject * object, guint property_id, + const GValue * value, GParamSpec * pspec) +{ + WpPwAudioDsp *self = WP_PW_AUDIO_DSP (object); + + switch (property_id) { + case PROP_ENDPOINT: + g_weak_ref_set (&self->endpoint, g_value_get_object (value)); + break; + case PROP_ID: + self->id = g_value_get_uint(value); + break; + case PROP_NAME: + self->name = g_value_dup_string (value); + break; + case PROP_DIRECTION: + self->direction = g_value_get_uint(value); + break; + case PROP_CONVERT: + self->convert = g_value_get_boolean(value); + break; + case PROP_TARGET: + self->target = g_value_get_pointer(value); + break; + case PROP_FORMAT: + self->format = g_value_get_pointer(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_pw_audio_dsp_get_property (GObject * object, guint property_id, + GValue * value, GParamSpec * pspec) +{ + WpPwAudioDsp *self = WP_PW_AUDIO_DSP (object); + + switch (property_id) { + case PROP_ENDPOINT: + g_value_take_object (value, g_weak_ref_get (&self->endpoint)); + break; + case PROP_ID: + g_value_set_uint (value, self->id); + break; + case PROP_NAME: + g_value_set_string (value, self->name); + break; + case PROP_DIRECTION: + g_value_set_uint (value, self->direction); + break; + case PROP_CONVERT: + g_value_set_boolean (value, self->convert); + break; + case PROP_TARGET: + g_value_set_pointer (value, (gpointer)self->target); + break; + case PROP_FORMAT: + g_value_set_pointer (value, (gpointer)self->format); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +wp_pw_audio_dsp_init_async (GAsyncInitable *initable, int io_priority, + GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data) +{ + WpPwAudioDsp *self = WP_PW_AUDIO_DSP (initable); + struct pw_properties *props; + struct pw_node_proxy *proxy; + + /* Set the remote pipewire */ + g_autoptr (WpEndpoint) ep = g_weak_ref_get (&self->endpoint); + g_return_if_fail(ep); + g_autoptr (WpCore) wp_core = wp_endpoint_get_core(ep); + g_return_if_fail(wp_core); + self->remote_pipewire = + wp_core_get_global (wp_core, WP_GLOBAL_REMOTE_PIPEWIRE); + g_return_if_fail(self->remote_pipewire); + + /* Create the async task */ + self->init_task = g_task_new (initable, cancellable, callback, data); + + /* Init the handled ports map */ + self->handled_ports = g_hash_table_new (g_direct_hash, g_direct_equal); + + /* Init the list of port proxies */ + self->port_proxies = g_ptr_array_new_full(4, (GDestroyNotify)g_object_unref); + + /* Set the default volume */ + self->volume = 1.0; + self->mute = FALSE; + + /* Create the properties */ + props = pw_properties_new_dict(self->target->props); + g_return_if_fail (props); + + /* Set the properties */ + pw_properties_set(props, "audio-dsp.name", + self->name ? self->name : "Audio-DSP"); + pw_properties_set(props, "audio-dsp.mode", self->convert ? "convert" : NULL); + pw_properties_setf(props, "audio-dsp.direction", "%d", self->direction); + pw_properties_setf(props, "audio-dsp.maxbuffer", "%ld", + MAX_QUANTUM_SIZE * sizeof(float)); + + /* Register a port_added callback */ + g_signal_connect_object(self->remote_pipewire, "global-added::port", + (GCallback)on_audio_dsp_port_added, self, 0); + + /* Create the proxy async */ + proxy = wp_remote_pipewire_create_object(self->remote_pipewire, + "audio-dsp", PW_TYPE_INTERFACE_Node, &props->dict); + wp_proxy_node_new(pw_proxy_get_id((struct pw_proxy *)proxy), proxy, + on_audio_dsp_proxy_created, self); + + /* Clean up */ + pw_properties_free(props); +} + +static gboolean +wp_pw_audio_dsp_init_finish (GAsyncInitable *initable, GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, initable), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +wp_pw_audio_dsp_async_initable_init (gpointer iface, gpointer iface_data) +{ + GAsyncInitableIface *ai_iface = iface; + + ai_iface->init_async = wp_pw_audio_dsp_init_async; + ai_iface->init_finish = wp_pw_audio_dsp_init_finish; +} + +static void +wp_pw_audio_dsp_init (WpPwAudioDsp * self) +{ +} + +static void +wp_pw_audio_dsp_class_init (WpPwAudioDspClass * klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + + object_class->finalize = wp_pw_audio_dsp_finalize; + object_class->set_property = wp_pw_audio_dsp_set_property; + object_class->get_property = wp_pw_audio_dsp_get_property; + + /* Install the properties */ + g_object_class_install_property (object_class, PROP_ENDPOINT, + g_param_spec_object ("endpoint", "endpoint", + "The endpoint this audio DSP belongs to", WP_TYPE_ENDPOINT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_ID, + g_param_spec_uint ("id", "id", "The Id of the audio DSP", 0, G_MAXUINT, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_NAME, + g_param_spec_string ("name", "name", "The name of the audio DSP", NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_DIRECTION, + g_param_spec_uint ("direction", "direction", + "The direction of the audio DSP", 0, 1, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_CONVERT, + g_param_spec_boolean ("convert", "convert", + "Whether the DSP is only in convert mode or not", FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_TARGET, + g_param_spec_pointer ("target", "target", + "The target node info of the audio DSP", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_FORMAT, + g_param_spec_pointer ("format", "format", + "The format of the audio DSP ports", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); +} + +void +wp_pw_audio_dsp_new (WpEndpoint *endpoint, guint id, const char *name, + enum pw_direction direction, gboolean convert, + const struct pw_node_info *target, const struct spa_audio_info_raw *format, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_async_initable_new_async ( + wp_pw_audio_dsp_get_type (), G_PRIORITY_DEFAULT, NULL, + callback, user_data, + "endpoint", endpoint, + "id", id, + "name", name, + "direction", direction, + "convert", convert, + "target", target, + "format", format, + NULL); +} + +WpPwAudioDsp * +wp_pw_audio_dsp_new_finish (GObject *initable, GAsyncResult *res, + GError **error) +{ + GAsyncInitable *ai = G_ASYNC_INITABLE(initable); + return WP_PW_AUDIO_DSP(g_async_initable_new_finish(ai, res, error)); +} + +const struct pw_node_info * +wp_pw_audio_dsp_get_info (WpPwAudioDsp * self) +{ + return wp_proxy_node_get_info(self->proxy); +} + +static void +port_proxies_foreach_func(gpointer data, gpointer user_data) +{ + GVariantBuilder *b = user_data; + g_variant_builder_add (b, "t", data); +} + +gboolean +wp_pw_audio_dsp_prepare_link (WpPwAudioDsp * self, GVariant ** properties, + GError ** error) { + const struct pw_node_info *info = NULL; + GVariantBuilder b, *b_ports; + GVariant *v_ports; + + /* Get the proxy node info */ + info = wp_proxy_node_get_info(self->proxy); + g_return_val_if_fail (info, FALSE); + + /* Create a variant array with all the ports */ + b_ports = g_variant_builder_new (G_VARIANT_TYPE ("at")); + g_ptr_array_foreach(self->port_proxies, port_proxies_foreach_func, + b_ports); + v_ports = g_variant_builder_end (b_ports); + + /* Set the properties */ + g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&b, "{sv}", "node-id", + g_variant_new_uint32 (info->id)); + g_variant_builder_add (&b, "{sv}", "ports", v_ports); + *properties = g_variant_builder_end (&b); + + return TRUE; +} + +GVariant * +wp_pw_audio_dsp_get_control_value (WpPwAudioDsp * self, guint32 control_id) +{ + switch (control_id) { + case CONTROL_VOLUME: + return g_variant_new_double (self->volume); + case CONTROL_MUTE: + return g_variant_new_boolean (self->mute); + default: + g_warning ("Unknown control id %u", control_id); + return NULL; + } +} + +gboolean +wp_pw_audio_dsp_set_control_value (WpPwAudioDsp * self, guint32 control_id, + GVariant * value) +{ + char buf[1024]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buf, sizeof(buf)); + struct pw_node_proxy *pw_proxy = NULL; + float volume; + bool mute; + + /* Get the pipewire dsp proxy */ + g_return_val_if_fail (self->proxy, FALSE); + pw_proxy = wp_proxy_get_pw_proxy (WP_PROXY(self->proxy)); + g_return_val_if_fail (pw_proxy, FALSE); + + switch (control_id) { + case CONTROL_VOLUME: + volume = g_variant_get_double (value); + + g_debug("WpPwAudioDsp:%p set volume control (%u) value, vol:%f", self, + control_id, volume); + + pw_node_proxy_set_param (pw_proxy, + SPA_PARAM_Props, 0, + spa_pod_builder_add_object (&b, + SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_volume, SPA_POD_Float(volume), + NULL)); + pw_node_proxy_enum_params (pw_proxy, 0, SPA_PARAM_Props, 0, -1, + NULL); + break; + + case CONTROL_MUTE: + mute = g_variant_get_boolean (value); + + g_debug("WpPwAudioDsp:%p set mute control (%u) value, mute:%d", self, + control_id, mute); + + pw_node_proxy_set_param (pw_proxy, + SPA_PARAM_Props, 0, + spa_pod_builder_add_object (&b, + SPA_TYPE_OBJECT_Props, SPA_PARAM_Props, + SPA_PROP_mute, SPA_POD_Bool(mute), + NULL)); + pw_node_proxy_enum_params (pw_proxy, 0, SPA_PARAM_Props, 0, -1, + NULL); + break; + + default: + g_warning ("Unknown control id %u", control_id); + return FALSE; + } + + return TRUE; +} diff --git a/modules/module-pw-audio-softdsp-endpoint/dsp.h b/modules/module-pw-audio-softdsp-endpoint/dsp.h new file mode 100644 index 0000000000000000000000000000000000000000..5d7c6f44cebfb5ee318058983a24f5d0b452f12d --- /dev/null +++ b/modules/module-pw-audio-softdsp-endpoint/dsp.h @@ -0,0 +1,36 @@ +/* WirePlumber + * + * Copyright © 2019 Collabora Ltd. + * @author Julian Bouzas <julian.bouzas@collabora.com> + * + * SPDX-License-Identifier: MIT + */ + +#include <gio/gio.h> +#include <wp/wp.h> + +#ifndef __WP_PW_AUDIO_DSP_H__ +#define __WP_PW_AUDIO_DSP_H__ + +G_DECLARE_FINAL_TYPE (WpPwAudioDsp, wp_pw_audio_dsp, + WP_PW, AUDIO_DSP, GObject) + +guint wp_pw_audio_dsp_id_encode (guint stream_id, guint control_id); +void wp_pw_audio_dsp_id_decode (guint id, guint *stream_id, guint *control_id); + +void wp_pw_audio_dsp_new (WpEndpoint *endpoint, guint id, const char *name, + enum pw_direction direction, gboolean convert, + const struct pw_node_info *target, const struct spa_audio_info_raw *format, + GAsyncReadyCallback callback, gpointer user_data); +WpPwAudioDsp * wp_pw_audio_dsp_new_finish (GObject *initable, GAsyncResult *res, + GError **error); + +const struct pw_node_info *wp_pw_audio_dsp_get_info (WpPwAudioDsp * self); +gboolean wp_pw_audio_dsp_prepare_link (WpPwAudioDsp * self, + GVariant ** properties, GError ** error); +GVariant * wp_pw_audio_dsp_get_control_value (WpPwAudioDsp * self, + guint32 control_id); +gboolean wp_pw_audio_dsp_set_control_value (WpPwAudioDsp * self, + guint32 control_id, GVariant * value); + +#endif diff --git a/modules/module-simple-policy.c b/modules/module-simple-policy.c index 312f604fd7df852c6c6c0be00283b3b7473cd31d..433819a5ff0f56755c5d14d9d7e66d178db84b2a 100644 --- a/modules/module-simple-policy.c +++ b/modules/module-simple-policy.c @@ -316,44 +316,45 @@ simple_policy_find_endpoint (WpPolicy *policy, GVariant *props, if (!ptr_array) return NULL; - /* TODO: for now we statically return the first stream - * we should be looking into the media.role eventually */ - g_variant_lookup (props, "media.role", "&s", &role); - if (!g_strcmp0 (action, "mixer") && !g_strcmp0 (role, "Master")) - *stream_id = WP_STREAM_ID_NONE; - else - *stream_id = 0; - /* Find and return the "selected" endpoint */ - /* FIXME: fix the endpoint API, this is terrible */ for (i = 0; i < ptr_array->len; i++) { ep = g_ptr_array_index (ptr_array, i); - GVariantIter iter; - g_autoptr (GVariant) controls = NULL; g_autoptr (GVariant) value = NULL; - const gchar *name; guint id; - controls = wp_endpoint_list_controls (ep); - g_variant_iter_init (&iter, controls); - while ((value = g_variant_iter_next_value (&iter))) { - if (!g_variant_lookup (value, "name", "&s", &name) - || !g_str_equal (name, "selected")) { - g_variant_unref (value); - continue; - } - g_variant_lookup (value, "id", "u", &id); - g_variant_unref (value); - } + id = wp_endpoint_find_control (ep, WP_STREAM_ID_NONE, "selected"); + if (id == WP_CONTROL_ID_NONE) + continue; value = wp_endpoint_get_control_value (ep, id); - if (value && g_variant_get_boolean (value)) - return g_object_ref (ep); + if (value && g_variant_get_boolean (value)) { + g_object_ref (ep); + goto select_stream; + } } /* If not found, return the first endpoint */ - return (ptr_array->len > 1) ? + ep = (ptr_array->len > 1) ? g_object_ref (g_ptr_array_index (ptr_array, 0)) : NULL; + +select_stream: + g_variant_lookup (props, "media.role", "&s", &role); + if (!g_strcmp0 (action, "mixer") && !g_strcmp0 (role, "Master")) + *stream_id = WP_STREAM_ID_NONE; + else if (ep) { + /* the default role is "Multimedia" */ + if (!role) + role = "Multimedia"; + *stream_id = wp_endpoint_find_stream (ep, role); + + /* role not found, try the first stream */ + if (*stream_id == WP_STREAM_ID_NONE) { + g_warning ("role '%s' not found in endpoint", role); + *stream_id = 0; + } + } + + return ep; } static void diff --git a/src/wireplumber.conf b/src/wireplumber.conf index 2fef0590d067855a946bcf5af96c502e6746e8c6..999e0e064a259035f0c556ca13de5d9fcdddd2a1 100644 --- a/src/wireplumber.conf +++ b/src/wireplumber.conf @@ -9,11 +9,20 @@ load-module C libwireplumber-module-client-permissions load-module C libwireplumber-module-pw-audio-softdsp-endpoint # Endpoint that provides high-level volume controls for the AGL mixer -load-module C libwireplumber-module-mixer +# The streams specified here are the ones that will appear in the mixer. +# They must match the stream names in the alsa-udev module, +# except for "Master", which is treated specially. +load-module C libwireplumber-module-mixer { + "streams": <["Master", "Multimedia", "Navigation", "Communication", "Emergency"]> +} # Monitors the ALSA devices that are discovered via udev # and creates softdsp-endopints for each one of them -load-module C libwireplumber-module-pw-alsa-udev +# The streams specified here are the ones that will be available for linking +# clients. Currently, they are matched against the client's role string. +load-module C libwireplumber-module-pw-alsa-udev { + "streams": <["Multimedia", "Navigation", "Communication", "Emergency"]> +} # Implements linking clients to devices and maintains # information about the devices to be used.