diff --git a/lib/wp/session-item.c b/lib/wp/session-item.c index d8849d66da3d78dc15b4e1b1a340c8fa28244363..0f536b6f14148b7f806057738e5995a6f55d0bc6 100644 --- a/lib/wp/session-item.c +++ b/lib/wp/session-item.c @@ -193,7 +193,7 @@ wp_session_item_clear_flag (WpSessionItem * self, WpSiFlags flag) * * mv (optional variant): optionally, an additional variant * This is provided to allow extensions. * - * Returns: (transfer full): the configuration description + * Returns: (transfer floating): the configuration description */ GVariant * wp_session_item_get_config_spec (WpSessionItem * self) @@ -208,7 +208,7 @@ wp_session_item_get_config_spec (WpSessionItem * self) /** * wp_session_item_configure: (virtual configure) * @self: the session item - * @args: (transfer floating): the configuration options to set + * @args: (transfer none): the configuration options to set * (`a{sv}` dictionary, mapping option names to values) * * Returns: %TRUE on success, %FALSE if the options could not be set @@ -229,7 +229,7 @@ wp_session_item_configure (WpSessionItem * self, GVariant * args) * wp_session_item_get_configuration: (virtual get_configuration) * @self: the session item * - * Returns: (transfer full): the active configuration, as a `a{sv}` dictionary + * Returns: (transfer floating): the active configuration, as a `a{sv}` dictionary */ GVariant * wp_session_item_get_configuration (WpSessionItem * self) diff --git a/lib/wp/session-item.h b/lib/wp/session-item.h index 793a4d990f38b9b529acfc0dfd3e9c22674a9b1f..e7da76df119ed5311be5389a78f034f8d8ce21e3 100644 --- a/lib/wp/session-item.h +++ b/lib/wp/session-item.h @@ -54,16 +54,11 @@ typedef enum { /** * WpSiConfigOptionFlags: * @WP_SI_CONFIG_OPTION_WRITEABLE: the option can be set externally - * @WP_SI_CONFIG_OPTION_REQUIRED: the option is required to complete activation - * @WP_SI_CONFIG_OPTION_PROVIDED: the value of this option can be provided - * by the implementation if it is not set externally; this can be used to - * have a "default fallback" value or to report immutable configuration - * that is discovered from an underlying layer (ex. hardware properties) + * @WP_SI_CONFIG_OPTION_REQUIRED: the option is required to activate the item */ typedef enum { WP_SI_CONFIG_OPTION_WRITEABLE = (1<<0), WP_SI_CONFIG_OPTION_REQUIRED = (1<<1), - WP_SI_CONFIG_OPTION_PROVIDED = (1<<2), } WpSiConfigOptionFlags; /** diff --git a/lib/wp/si-interfaces.c b/lib/wp/si-interfaces.c index cba647276ca59fd44304ea5d66a1bd2dd76f52ad..a4c2506a126b28a2d95dac7ffd70f6e99931836d 100644 --- a/lib/wp/si-interfaces.c +++ b/lib/wp/si-interfaces.c @@ -89,7 +89,7 @@ wp_si_endpoint_get_priority (WpSiEndpoint * self) * wp_si_endpoint_get_properties: (virtual get_properties) * @self: the session item * - * Returns: (transfer full): the properties of the endpoint + * Returns: (transfer full) (nullable): the properties of the endpoint */ WpProperties * wp_si_endpoint_get_properties (WpSiEndpoint * self) @@ -218,7 +218,7 @@ wp_si_stream_get_name (WpSiStream * self) * wp_si_stream_get_properties: (virtual get_properties) * @self: the session item * - * Returns: (transfer full): the properties of the stream + * Returns: (transfer full) (nullable): the properties of the stream */ WpProperties * wp_si_stream_get_properties (WpSiStream * self) diff --git a/modules/meson.build b/modules/meson.build index 3b3fabd3ec66a2aa8514690ccea2100dc3551f70..2fc7f890905f21b822c287e3d08c6ba916faaa7e 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -113,3 +113,15 @@ shared_library( install_dir : wireplumber_module_dir, dependencies : [wp_dep, pipewire_dep], ) + +shared_library( + 'wireplumber-module-si-adapter', + [ + 'module-si-adapter.c', + 'module-pipewire/algorithms.c', + ], + c_args : [common_c_args, '-DG_LOG_DOMAIN="m-si-adapter"'], + install : true, + install_dir : wireplumber_module_dir, + dependencies : [wp_dep, pipewire_dep], +) diff --git a/modules/module-si-adapter.c b/modules/module-si-adapter.c new file mode 100644 index 0000000000000000000000000000000000000000..8cbf1ff4b986835c93cd0bd34c311713c4b9efe8 --- /dev/null +++ b/modules/module-si-adapter.c @@ -0,0 +1,542 @@ +/* WirePlumber + * + * Copyright © 2020 Collabora Ltd. + * @author George Kiagiadakis <george.kiagiadakis@collabora.com> + * + * SPDX-License-Identifier: MIT + */ + +#include <wp/wp.h> +#include <pipewire/pipewire.h> +#include <pipewire/extensions/session-manager.h> + +#include <spa/pod/builder.h> +#include <spa/param/format-utils.h> +#include <spa/param/audio/raw.h> +#include <spa/param/audio/format-utils.h> +#include <spa/param/props.h> + +#include "module-pipewire/algorithms.h" + +enum { + STEP_VERIFY_CONFIG = WP_TRANSITION_STEP_CUSTOM_START, + STEP_CHOOSE_FORMAT, + STEP_CONFIGURE_PORTS, + STEP_GET_PORTS, +}; + +struct _WpSiAdapter +{ + WpSessionItem parent; + + /* configuration */ + WpNode *node; + gchar name[96]; + gchar media_class[32]; + gchar role[32]; + guint priority; + gboolean control_port; + gboolean monitor; + WpDirection direction; + struct spa_audio_info_raw format; + + WpObjectManager *ports_om; +}; + +static void si_adapter_multi_endpoint_init (WpSiMultiEndpointInterface * iface); +static void si_adapter_endpoint_init (WpSiEndpointInterface * iface); +static void si_adapter_stream_init (WpSiStreamInterface * iface); + +G_DECLARE_FINAL_TYPE(WpSiAdapter, si_adapter, WP, SI_ADAPTER, WpSessionItem) +G_DEFINE_TYPE_WITH_CODE (WpSiAdapter, si_adapter, WP_TYPE_SESSION_ITEM, + G_IMPLEMENT_INTERFACE (WP_TYPE_SI_MULTI_ENDPOINT, si_adapter_multi_endpoint_init) + G_IMPLEMENT_INTERFACE (WP_TYPE_SI_ENDPOINT, si_adapter_endpoint_init) + G_IMPLEMENT_INTERFACE (WP_TYPE_SI_STREAM, si_adapter_stream_init)) + +static void +si_adapter_init (WpSiAdapter * self) +{ +} + +static void +si_adapter_finalize (GObject * object) +{ + WpSiAdapter *self = WP_SI_ADAPTER (object); + + g_clear_object (&self->node); + + G_OBJECT_CLASS (si_adapter_parent_class)->finalize (object); +} + +static void +si_adapter_reset (WpSessionItem * item) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + + g_clear_object (&self->ports_om); + wp_session_item_clear_flag (item, WP_SI_FLAG_CONFIGURED); + + WP_SESSION_ITEM_CLASS (si_adapter_parent_class)->reset (item); +} + +static GVariant * +si_adapter_get_config_spec (WpSessionItem * item) +{ + GVariantBuilder b; + + g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&b, "(ssymv)", "node", "t", + WP_SI_CONFIG_OPTION_WRITEABLE | WP_SI_CONFIG_OPTION_REQUIRED, NULL); + g_variant_builder_add (&b, "(ssymv)", "name", "s", + WP_SI_CONFIG_OPTION_WRITEABLE, NULL); + g_variant_builder_add (&b, "(ssymv)", "media-class", "s", + WP_SI_CONFIG_OPTION_WRITEABLE, NULL); + g_variant_builder_add (&b, "(ssymv)", "role", "s", + WP_SI_CONFIG_OPTION_WRITEABLE, NULL); + g_variant_builder_add (&b, "(ssymv)", "priority", "u", + WP_SI_CONFIG_OPTION_WRITEABLE, NULL); + g_variant_builder_add (&b, "(ssymv)", "enable-control-port", "b", + WP_SI_CONFIG_OPTION_WRITEABLE, NULL); + g_variant_builder_add (&b, "(ssymv)", "enable-monitor", "b", + WP_SI_CONFIG_OPTION_WRITEABLE, NULL); + g_variant_builder_add (&b, "(ssymv)", "direction", "y", 0, NULL); + g_variant_builder_add (&b, "(ssymv)", "channels", "u", 0, NULL); + return g_variant_builder_end (&b); +} + +static GVariant * +si_adapter_get_configuration (WpSessionItem * item) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + GVariantBuilder b; + + /* Set the properties */ + g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&b, "{sv}", + "node", g_variant_new_uint64 ((guint64) self->node)); + g_variant_builder_add (&b, "{sv}", + "name", g_variant_new_string (self->name)); + g_variant_builder_add (&b, "{sv}", + "media-class", g_variant_new_string (self->media_class)); + g_variant_builder_add (&b, "{sv}", + "role", g_variant_new_string (self->role)); + g_variant_builder_add (&b, "{sv}", + "priority", g_variant_new_uint32 (self->priority)); + g_variant_builder_add (&b, "{sv}", + "enable-control-port", g_variant_new_boolean (self->control_port)); + g_variant_builder_add (&b, "{sv}", + "enable-monitor", g_variant_new_boolean (self->monitor)); + g_variant_builder_add (&b, "{sv}", + "direction", g_variant_new_byte (self->direction)); + g_variant_builder_add (&b, "{sv}", + "channels", g_variant_new_uint32 (self->format.channels)); + return g_variant_builder_end (&b); +} + +static gboolean +si_adapter_configure (WpSessionItem * item, GVariant * args) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + guint64 node_i; + const gchar *tmp_str; + g_autoptr (WpProperties) props = NULL; + + if (wp_session_item_get_flags (item) & (WP_SI_FLAG_ACTIVATING | WP_SI_FLAG_ACTIVE)) + return FALSE; + + /* reset previous config */ + g_clear_object (&self->node); + self->name[0] = '\0'; + self->media_class[0] = '\0'; + self->role[0] = '\0'; + self->priority = 0; + self->control_port = FALSE; + self->monitor = FALSE; + self->direction = WP_DIRECTION_INPUT; + + if (!g_variant_lookup (args, "node", "t", &node_i)) + return FALSE; + + g_return_val_if_fail (WP_IS_NODE (GUINT_TO_POINTER (node_i)), FALSE); + + self->node = g_object_ref (GUINT_TO_POINTER (node_i)); + props = wp_proxy_get_properties (WP_PROXY (self->node)); + + if (g_variant_lookup (args, "name", "&s", &tmp_str)) { + strncpy (self->name, tmp_str, sizeof (self->name) - 1); + } else { + tmp_str = wp_properties_get (props, PW_KEY_NODE_DESCRIPTION); + if (G_UNLIKELY (!tmp_str)) + tmp_str = wp_properties_get (props, PW_KEY_NODE_NAME); + if (G_LIKELY (tmp_str)) + strncpy (self->name, tmp_str, sizeof (self->name) - 1); + } + + if (g_variant_lookup (args, "media-class", "&s", &tmp_str)) { + strncpy (self->media_class, tmp_str, sizeof (self->media_class) - 1); + } else { + tmp_str = wp_properties_get (props, PW_KEY_MEDIA_CLASS); + if (G_LIKELY (tmp_str)) + strncpy (self->media_class, tmp_str, sizeof (self->media_class) - 1); + } + + if (g_variant_lookup (args, "role", "&s", &tmp_str)) { + strncpy (self->role, tmp_str, sizeof (self->role) - 1); + } else { + tmp_str = wp_properties_get (props, PW_KEY_MEDIA_ROLE); + if (tmp_str) + strncpy (self->role, tmp_str, sizeof (self->role) - 1); + } + + if (strstr (self->media_class, "Source") || + strstr (self->media_class, "Output")) + self->direction = WP_DIRECTION_OUTPUT; + + g_variant_lookup (args, "priority", "u", &self->priority); + g_variant_lookup (args, "enable-control-port", "b", &self->control_port); + g_variant_lookup (args, "enable-monitor", "b", &self->monitor); + + return TRUE; +} + +static guint +si_adapter_get_next_step (WpSessionItem * item, + WpTransition * transition, guint step) +{ + switch (step) { + case WP_TRANSITION_STEP_NONE: + return STEP_VERIFY_CONFIG; + + case STEP_VERIFY_CONFIG: + case STEP_CHOOSE_FORMAT: + case STEP_CONFIGURE_PORTS: + return step + 1; + + case STEP_GET_PORTS: + return WP_TRANSITION_STEP_NONE; + + default: + return WP_TRANSITION_STEP_ERROR; + } +} + +static void +on_node_enum_format_done (WpProxy *proxy, GAsyncResult *res, + WpTransition * transition) +{ + WpSiAdapter *self = wp_transition_get_source_object (transition); + g_autoptr (GPtrArray) formats = NULL; + g_autoptr (GError) error = NULL; + + formats = wp_proxy_enum_params_collect_finish (proxy, res, &error); + if (error) { + wp_transition_return_error (transition, g_steal_pointer (&error)); + return; + } + + if (!choose_sensible_raw_audio_format (formats, &self->format)) { + uint32_t media_type, media_subtype; + struct spa_pod *param; + + g_warning ("failed to choose a sensible audio format"); + + /* fall back to spa_pod_fixate */ + if (formats->len == 0 || + !(param = g_ptr_array_index (formats, 0)) || + spa_format_parse (param, &media_type, &media_subtype) < 0 || + media_type != SPA_MEDIA_TYPE_audio || + media_subtype != SPA_MEDIA_SUBTYPE_raw) { + wp_transition_return_error (transition, + g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "node does not support audio/raw format")); + return; + } + + spa_pod_fixate (param); + spa_format_audio_raw_parse (param, &self->format); + } + + wp_session_item_set_flag (WP_SESSION_ITEM (self), WP_SI_FLAG_CONFIGURED); + wp_transition_advance (transition); +} + +static void +on_ports_configuration_done (WpCore * core, GAsyncResult * res, + WpTransition * transition) +{ + g_autoptr (GError) error = NULL; + if (!wp_core_sync_finish (core, res, &error)) { + wp_transition_return_error (transition, g_steal_pointer (&error)); + return; + } + + wp_transition_advance (transition); +} + +static void +on_ports_changed (WpObjectManager *om, WpTransition * transition) +{ + WpSiAdapter *self = wp_transition_get_source_object (transition); + + g_debug ("%s:%p port config done", G_OBJECT_TYPE_NAME (self), self); + + wp_transition_advance (transition); +} + +static void +si_adapter_execute_step (WpSessionItem * item, WpTransition * transition, + guint step) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + + switch (step) { + case STEP_VERIFY_CONFIG: + if (!self->node) { + wp_transition_return_error (transition, + g_error_new (WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT, + "si-adapter: node was not set on the configuration")); + } + wp_transition_advance (transition); + break; + + case STEP_CHOOSE_FORMAT: + wp_proxy_enum_params_collect (WP_PROXY (self->node), + SPA_PARAM_EnumFormat, 0, -1, NULL, NULL, + (GAsyncReadyCallback) on_node_enum_format_done, transition); + break; + + case STEP_CONFIGURE_PORTS: { + uint8_t buf[1024]; + struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT (buf, sizeof(buf)); + struct spa_pod *param; + + /* set the chosen device/client format on the node */ + param = spa_format_audio_raw_build (&pod_builder, SPA_PARAM_Format, + &self->format); + wp_proxy_set_param (WP_PROXY (self->node), SPA_PARAM_Format, 0, param); + + /* now choose the DSP format: keep the chanels but use F32 plannar @ 48K */ + self->format.format = SPA_AUDIO_FORMAT_F32P; + self->format.rate = 48000; + + param = spa_format_audio_raw_build (&pod_builder, + SPA_PARAM_Format, &self->format); + param = spa_pod_builder_add_object (&pod_builder, + SPA_TYPE_OBJECT_ParamPortConfig, SPA_PARAM_PortConfig, + SPA_PARAM_PORT_CONFIG_direction, SPA_POD_Id(self->direction), + SPA_PARAM_PORT_CONFIG_mode, SPA_POD_Id(SPA_PARAM_PORT_CONFIG_MODE_dsp), + SPA_PARAM_PORT_CONFIG_monitor, SPA_POD_Bool(self->monitor), + SPA_PARAM_PORT_CONFIG_control, SPA_POD_Bool(self->control_port), + SPA_PARAM_PORT_CONFIG_format, SPA_POD_Pod(param)); + + wp_proxy_set_param (WP_PROXY (self->node), SPA_PARAM_PortConfig, 0, param); + + g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (self->node)); + wp_core_sync (core, NULL, + (GAsyncReadyCallback) on_ports_configuration_done, transition); + break; + } + case STEP_GET_PORTS: { + GVariantBuilder b; + self->ports_om = wp_object_manager_new (); + + /* set a constraint: the port's "node.id" must match + the stream's underlying node id */ + g_variant_builder_init (&b, G_VARIANT_TYPE ("aa{sv}")); + g_variant_builder_open (&b, G_VARIANT_TYPE_VARDICT); + g_variant_builder_add (&b, "{sv}", "type", + g_variant_new_int32 (WP_OBJECT_MANAGER_CONSTRAINT_PW_GLOBAL_PROPERTY)); + g_variant_builder_add (&b, "{sv}", "name", + g_variant_new_string (PW_KEY_NODE_ID)); + g_variant_builder_add (&b, "{sv}", "value", + g_variant_new_take_string (g_strdup_printf ("%u", + wp_proxy_get_bound_id (WP_PROXY (self->node))))); + g_variant_builder_close (&b); + + /* declare interest on ports with this constraint */ + wp_object_manager_add_interest (self->ports_om, WP_TYPE_PORT, + g_variant_builder_end (&b), WP_PROXY_FEATURES_STANDARD); + + g_signal_connect_object (self->ports_om, "objects-changed", + (GCallback) on_ports_changed, transition, 0); + + /* install the object manager */ + g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (self->node)); + wp_core_install_object_manager (core, self->ports_om); + break; + } + default: + g_return_if_reached (); + } +} + +static void +si_adapter_class_init (WpSiAdapterClass * klass) +{ + GObjectClass *object_class = (GObjectClass *) klass; + WpSessionItemClass *si_class = (WpSessionItemClass *) klass; + + object_class->finalize = si_adapter_finalize; + + si_class->get_config_spec = si_adapter_get_config_spec; + si_class->configure = si_adapter_configure; + si_class->get_configuration = si_adapter_get_configuration; + si_class->get_next_step = si_adapter_get_next_step; + si_class->execute_step = si_adapter_execute_step; + si_class->reset = si_adapter_reset; +} + +static guint +si_adapter_get_n_endpoints (WpSiMultiEndpoint * item) +{ + return 1; +} + +static WpSiEndpoint * +si_adapter_get_endpoint (WpSiMultiEndpoint * item, guint index) +{ + g_return_val_if_fail (index == 0, NULL); + return WP_SI_ENDPOINT (item); +} + +static void +si_adapter_multi_endpoint_init (WpSiMultiEndpointInterface * iface) +{ + iface->get_n_endpoints = si_adapter_get_n_endpoints; + iface->get_endpoint = si_adapter_get_endpoint; +} + +static const gchar * +si_adapter_get_name (WpSiEndpoint * item) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + return self->name; +} + +static const gchar * +si_adapter_get_media_class (WpSiEndpoint * item) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + return self->media_class; +} + +static const gchar * +si_adapter_get_role (WpSiEndpoint * item) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + return self->role; +} + +static WpDirection +si_adapter_get_direction (WpSiEndpoint * item) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + return self->direction; +} + +static guint +si_adapter_get_priority (WpSiEndpoint * item) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + return self->priority; +} + +static WpProperties * +si_adapter_get_properties (WpSiEndpoint * item) +{ + WpSiAdapter *self = WP_SI_ADAPTER (item); + g_autoptr (WpProperties) node_props = NULL; + WpProperties *result; + + result = wp_properties_new_empty (); + + /* copy useful properties from the node */ + node_props = wp_proxy_get_properties (WP_PROXY (self->node)); + wp_properties_copy_keys (node_props, result, + PW_KEY_DEVICE_ID, + NULL); + + /* associate with the node */ + wp_properties_setf (result, PW_KEY_NODE_ID, "%d", + wp_proxy_get_bound_id (WP_PROXY (self->node))); + + /* propagate the device icon, if this is a device */ + const gchar *icon = wp_properties_get (node_props, PW_KEY_DEVICE_ICON_NAME); + if (icon) + wp_properties_set (result, PW_KEY_ENDPOINT_ICON_NAME, icon); + + /* endpoint.client.id: the id of the client that created the node + * Not to be confused with client.id, which will also be set on the endpoint + * to the id of the client object that creates the endpoint (wireplumber) */ + wp_properties_set (result, PW_KEY_ENDPOINT_CLIENT_ID, + wp_properties_get (node_props, PW_KEY_CLIENT_ID)); + + return result; +} + +static guint +si_adapter_get_n_streams (WpSiEndpoint * item) +{ + return 1; +} + +static WpSiStream * +si_adapter_get_stream (WpSiEndpoint * item, guint index) +{ + g_return_val_if_fail (index == 0, NULL); + return WP_SI_STREAM (item); +} + +static void +si_adapter_endpoint_init (WpSiEndpointInterface * iface) +{ + iface->get_name = si_adapter_get_name; + iface->get_media_class = si_adapter_get_media_class; + iface->get_role = si_adapter_get_role; + iface->get_direction = si_adapter_get_direction; + iface->get_priority = si_adapter_get_priority; + iface->get_properties = si_adapter_get_properties; + iface->get_n_streams = si_adapter_get_n_streams; + iface->get_stream = si_adapter_get_stream; +} + +static const gchar * +si_adapter_get_stream_name (WpSiStream * self) +{ + return "default"; +} + +static WpProperties * +si_adapter_get_stream_properties (WpSiStream * self) +{ + return NULL; +} + +static WpSiEndpoint * +si_adapter_get_stream_parent_endpoint (WpSiStream * self) +{ + return WP_SI_ENDPOINT (self); +} + +static void +si_adapter_stream_init (WpSiStreamInterface * iface) +{ + iface->get_name = si_adapter_get_stream_name; + iface->get_properties = si_adapter_get_stream_properties; + iface->get_parent_endpoint = si_adapter_get_stream_parent_endpoint; +} + +static void +si_adapter_factory (WpFactory * factory, GType type, + GVariant * properties, GAsyncReadyCallback ready, gpointer user_data) +{ + WpSessionItem *item = g_object_new (si_adapter_get_type (), NULL); + wp_session_item_configure (item, properties); + //TODO: return +} + +WP_PLUGIN_EXPORT void +wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args) +{ + wp_factory_new (core, "si-adapter", si_adapter_factory); +}